Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e378131e authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Do not require reauth when turning on profile with unified challenge"

parents 334eedc5 a3c71a14
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -89,4 +89,6 @@ interface ILockSettings {
            in List<WrappedApplicationKey> applicationKeys);
    void closeSession(in String sessionId);
    boolean hasSecureLockScreen();
    boolean tryUnlockWithCachedUnifiedChallenge(int userId);
    void removeCachedUnifiedChallenge(int userId);
}
+31 −2
Original line number Diff line number Diff line
@@ -55,10 +55,10 @@ import android.util.SparseLongArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;

import libcore.util.HexEncoding;

import com.google.android.collect.Lists;

import libcore.util.HexEncoding;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.MessageDigest;
@@ -1755,4 +1755,33 @@ public class LockPatternUtils {
        return FRP_CREDENTIAL_ENABLED && context.getResources().getBoolean(
                com.android.internal.R.bool.config_enableCredentialFactoryResetProtection);
    }

    /**
     * Attempt to rederive the unified work challenge for the specified profile user and unlock the
     * user. If successful, this would allow the user to leave quiet mode automatically without
     * additional user authentication.
     *
     * This is made possible by the framework storing an encrypted copy of the unified challenge
     * auth-bound to the primary user's lockscreen. As long as the primery user has unlocked
     * recently (7 days), the framework will be able to decrypt it and plug the secret into the
     * unlock flow.
     *
     * @return {@code true} if automatic unlocking is successful, {@code false} otherwise.
     */
    public boolean tryUnlockWithCachedUnifiedChallenge(int userId) {
        try {
            return getLockSettings().tryUnlockWithCachedUnifiedChallenge(userId);
        } catch (RemoteException re) {
            return false;
        }
    }

    /** Remove cached unified profile challenge, for testing and CTS usage. */
    public void removeCachedUnifiedChallenge(int userId) {
        try {
            getLockSettings().removeCachedUnifiedChallenge(userId);
        } catch (RemoteException re) {
            re.rethrowFromSystemServer();
        }
    }
}
+57 −12
Original line number Diff line number Diff line
@@ -139,6 +139,7 @@ import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
@@ -224,6 +225,7 @@ public class LockSettingsService extends ILockSettings.Stub {
    private final KeyStore mKeyStore;

    private final RecoverableKeyStoreManager mRecoverableKeyStoreManager;
    private ManagedProfilePasswordCache mManagedProfilePasswordCache;

    private final RebootEscrowManager mRebootEscrowManager;

@@ -387,6 +389,7 @@ public class LockSettingsService extends ILockSettings.Stub {
            setLockCredentialInternal(unifiedProfilePassword, managedUserPassword, managedUserId,
                    /* isLockTiedToParent= */ true);
            tieProfileLockToParent(managedUserId, unifiedProfilePassword);
            mManagedProfilePasswordCache.storePassword(managedUserId, unifiedProfilePassword);
        }
    }

@@ -527,6 +530,16 @@ public class LockSettingsService extends ILockSettings.Stub {
                int defaultValue) {
            return Settings.Global.getInt(contentResolver, keyName, defaultValue);
        }

        public @NonNull ManagedProfilePasswordCache getManagedProfilePasswordCache() {
            try {
                java.security.KeyStore ks = java.security.KeyStore.getInstance("AndroidKeyStore");
                ks.load(null);
                return new ManagedProfilePasswordCache(ks, getUserManager());
            } catch (Exception e) {
                throw new IllegalStateException("Cannot load keystore", e);
            }
        }
    }

    public LockSettingsService(Context context) {
@@ -560,6 +573,7 @@ public class LockSettingsService extends ILockSettings.Stub {
        mStrongAuthTracker.register(mStrongAuth);

        mSpManager = injector.getSyntheticPasswordManager(mStorage);
        mManagedProfilePasswordCache = injector.getManagedProfilePasswordCache();

        mRebootEscrowManager = injector.getRebootEscrowManager(new RebootEscrowCallbacks(),
                mStorage);
@@ -706,7 +720,8 @@ public class LockSettingsService extends ILockSettings.Stub {
    private void ensureProfileKeystoreUnlocked(int userId) {
        final KeyStore ks = KeyStore.getInstance();
        if (ks.state(userId) == KeyStore.State.LOCKED
                && tiedManagedProfileReadyToUnlock(mUserManager.getUserInfo(userId))) {
                && mUserManager.getUserInfo(userId).isManagedProfile()
                && hasUnifiedChallenge(userId)) {
            Slog.i(TAG, "Managed profile got unlocked, will unlock its keystore");
            // If boot took too long and the password in vold got expired, parent keystore will
            // be still locked, we ignore this case since the user will be prompted to unlock
@@ -1302,6 +1317,7 @@ public class LockSettingsService extends ILockSettings.Stub {
        LockscreenCredential credential = LockscreenCredential.createManagedPassword(
                decryptionResult);
        Arrays.fill(decryptionResult, (byte) 0);
        mManagedProfilePasswordCache.storePassword(userId, credential);
        return credential;
    }

@@ -1381,12 +1397,25 @@ public class LockSettingsService extends ILockSettings.Stub {
        }

        for (UserInfo profile : mUserManager.getProfiles(userId)) {
            if (profile.id == userId) continue;
            if (!profile.isManagedProfile()) continue;

            if (hasUnifiedChallenge(profile.id)) {
                if (mUserManager.isUserRunning(profile.id)) {
                    // Unlock managed profile with unified lock
            if (tiedManagedProfileReadyToUnlock(profile)) {
                    // Must pass the challenge on for resetLockout, so it's not over-written, which
                    // causes LockSettingsService to revokeChallenge inappropriately.
                    unlockChildProfile(profile.id, false /* ignoreUserNotAuthenticated */,
                            challengeType, challenge, resetLockouts);
                } else {
                    try {
                        // Profile not ready for unlock yet, but decrypt the unified challenge now
                        // so it goes into the cache
                        getDecryptedPasswordForTiedProfile(profile.id);
                    } catch (GeneralSecurityException | IOException e) {
                        Slog.d(TAG, "Cache work profile password failed", e);
                    }
                }
            }
            // Now we have unlocked the parent user and attempted to unlock the profile we should
            // show notifications if the profile is still locked.
@@ -1417,11 +1446,9 @@ public class LockSettingsService extends ILockSettings.Stub {
        }
    }

    private boolean tiedManagedProfileReadyToUnlock(UserInfo userInfo) {
        return userInfo.isManagedProfile()
                && !getSeparateProfileChallengeEnabledInternal(userInfo.id)
                && mStorage.hasChildProfileLock(userInfo.id)
                && mUserManager.isUserRunning(userInfo.id);
    private boolean hasUnifiedChallenge(int userId) {
        return !getSeparateProfileChallengeEnabledInternal(userId)
                && mStorage.hasChildProfileLock(userId);
    }

    private Map<Integer, LockscreenCredential> getDecryptedPasswordsForAllTiedProfiles(int userId) {
@@ -2233,6 +2260,7 @@ public class LockSettingsService extends ILockSettings.Stub {

        final KeyStore ks = KeyStore.getInstance();
        ks.onUserRemoved(userId);
        mManagedProfilePasswordCache.removePassword(userId);

        gateKeeperClearSecureUserId(userId);
        if (unknownUser || mUserManager.getUserInfo(userId).isManagedProfile()) {
@@ -2783,6 +2811,7 @@ public class LockSettingsService extends ILockSettings.Stub {
        synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords);

        setUserPasswordMetrics(credential, userId);
        mManagedProfilePasswordCache.removePassword(userId);

        if (profilePasswords != null) {
            for (Map.Entry<Integer, LockscreenCredential> entry : profilePasswords.entrySet()) {
@@ -3097,6 +3126,22 @@ public class LockSettingsService extends ILockSettings.Stub {
        return true;
    }

    @Override
    public boolean tryUnlockWithCachedUnifiedChallenge(int userId) {
        try (LockscreenCredential cred = mManagedProfilePasswordCache.retrievePassword(userId)) {
            if (cred == null) {
                return false;
            }
            return doVerifyCredential(cred, CHALLENGE_NONE, 0, userId, null /* progressCallback */)
                    .getResponseCode() == VerifyCredentialResponse.RESPONSE_OK;
        }
    }

    @Override
    public void removeCachedUnifiedChallenge(int userId) {
        mManagedProfilePasswordCache.removePassword(userId);
    }

    static String timestampToString(long timestamp) {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(timestamp));
    }
+18 −3
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ class LockSettingsShellCommand extends ShellCommand {
    private static final String COMMAND_SET_DISABLED = "set-disabled";
    private static final String COMMAND_VERIFY = "verify";
    private static final String COMMAND_GET_DISABLED = "get-disabled";
    private static final String COMMAND_REMOVE_CACHE = "remove-cache";
    private static final String COMMAND_HELP = "help";

    private int mCurrentUserId;
@@ -76,6 +77,15 @@ class LockSettingsShellCommand extends ShellCommand {
                        return -1;
                }
            }
            switch (cmd) {
                // Commands that do not require authentication go here.
                case COMMAND_REMOVE_CACHE:
                    runRemoveCache();
                    return 0;
                case COMMAND_HELP:
                    onHelp();
                    return 0;
            }
            if (!checkCredential()) {
                return -1;
            }
@@ -105,9 +115,6 @@ class LockSettingsShellCommand extends ShellCommand {
                case COMMAND_GET_DISABLED:
                    runGetDisabled();
                    break;
                case COMMAND_HELP:
                    onHelp();
                    break;
                default:
                    getErrPrintWriter().println("Unknown command: " + cmd);
                    break;
@@ -163,6 +170,9 @@ class LockSettingsShellCommand extends ShellCommand {
            pw.println("  verify [--old <CREDENTIAL>] [--user USER_ID]");
            pw.println("    Verifies the lock credentials.");
            pw.println("");
            pw.println("  remove-cache [--user USER_ID]");
            pw.println("    Removes cached unified challenge for the managed profile.");
            pw.println("");
        }
    }

@@ -322,4 +332,9 @@ class LockSettingsShellCommand extends ShellCommand {
            return true;
        }
    }

    private void runRemoveCache() {
        mLockPatternUtils.removeCachedUnifiedChallenge(mCurrentUserId);
        getOutPrintWriter().println("Password cached removed for user " + mCurrentUserId);
    }
}
+191 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.locksettings;

import android.annotation.Nullable;
import android.content.pm.UserInfo;
import android.os.UserHandle;
import android.os.UserManager;
import android.security.keystore.AndroidKeyStoreSpi;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.UserNotAuthenticatedException;
import android.util.Slog;
import android.util.SparseArray;

import com.android.internal.widget.LockscreenCredential;

import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

/**
 * Caches *unified* work challenge for user 0's managed profiles. Only user 0's profile is supported
 * at the moment because the cached credential is encrypted using a keystore key auth-bound to
 * user 0: this is to match how unified work challenge is similarly auth-bound to its parent user's
 * lockscreen credential normally. It's possible to extend this class to support managed profiles
 * for secondary users, that will require generating auth-bound keys to their corresponding parent
 * user though (which {@link KeyGenParameterSpec} does not support right now).
 *
 * <p> The cache is filled whenever the managed profile's unified challenge is created or derived
 * (as part of the parent user's credential verification flow). It's removed when the profile is
 * deleted or a (separate) lockscreen credential is explicitly set on the profile. There is also
 * an ADB command to evict the cache "cmd lock_settings remove-cache --user X", to assist
 * development and testing.

 * <p> The encrypted credential is stored in-memory only so the cache does not persist across
 * reboots.
 */
public class ManagedProfilePasswordCache {

    private static final String TAG = "ManagedProfilePasswordCache";
    private static final int KEY_LENGTH = 256;
    private static final int CACHE_TIMEOUT_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);

    private final SparseArray<byte[]> mEncryptedPasswords = new SparseArray<>();
    private final KeyStore mKeyStore;
    private final UserManager mUserManager;

    public ManagedProfilePasswordCache(KeyStore keyStore, UserManager userManager) {
        mKeyStore = keyStore;
        mUserManager = userManager;
    }

    /**
     * Encrypt and store the password in the cache. Does NOT overwrite existing password cache
     * if one for the given user already exists.
     */
    public void storePassword(int userId, LockscreenCredential password) {
        synchronized (mEncryptedPasswords) {
            if (mEncryptedPasswords.contains(userId)) {
                return;
            }
            UserInfo parent = mUserManager.getProfileParent(userId);
            if (parent == null || parent.id != UserHandle.USER_SYSTEM) {
                // Since the cached password is encrypted using a keystore key auth-bound to user 0,
                // only support caching password for user 0's profile.
                return;
            }
            String keyName = getEncryptionKeyName(userId);
            KeyGenerator generator;
            SecretKey key;
            try {
                generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
                        AndroidKeyStoreSpi.NAME);
                generator.init(new KeyGenParameterSpec.Builder(
                        keyName, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                        .setKeySize(KEY_LENGTH)
                        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                        // Generate auth-bound key to user 0 (since we the caller is user 0)
                        .setUserAuthenticationRequired(true)
                        .setUserAuthenticationValidityDurationSeconds(CACHE_TIMEOUT_SECONDS)
                        // Only accessible after user 0's keyguard is unlocked
                        .setUnlockedDeviceRequired(true)
                        .build());
                key = generator.generateKey();
            } catch (GeneralSecurityException e) {
                Slog.e(TAG, "Cannot generate key", e);
                return;
            }

            Cipher cipher;
            try {
                cipher = Cipher.getInstance("AES/GCM/NoPadding");
                cipher.init(Cipher.ENCRYPT_MODE, key);
                byte[] ciphertext = cipher.doFinal(password.getCredential());
                byte[] iv = cipher.getIV();
                byte[] block = Arrays.copyOf(iv, ciphertext.length + iv.length);
                System.arraycopy(ciphertext, 0, block, iv.length, ciphertext.length);
                mEncryptedPasswords.put(userId, block);
            } catch (GeneralSecurityException e) {
                Slog.d(TAG, "Cannot encrypt", e);
            }
        }
    }

    /** Attempt to retrieve the password for the given user. Returns {@code null} if it's not in the
     * cache or if decryption fails.
     */
    public @Nullable LockscreenCredential retrievePassword(int userId) {
        synchronized (mEncryptedPasswords) {
            byte[] block = mEncryptedPasswords.get(userId);
            if (block == null) {
                return null;
            }
            Key key;
            try {
                key = mKeyStore.getKey(getEncryptionKeyName(userId), null);
            } catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
                Slog.d(TAG, "Cannot get key", e);
                return null;
            }
            if (key == null) {
                return null;
            }
            byte[] iv = Arrays.copyOf(block, 12);
            byte[] ciphertext = Arrays.copyOfRange(block, 12, block.length);
            byte[] credential;
            try {
                Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
                cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
                credential = cipher.doFinal(ciphertext);
            } catch (UserNotAuthenticatedException e) {
                Slog.i(TAG, "Device not unlocked for more than 7 days");
                return null;
            } catch (GeneralSecurityException e) {
                Slog.d(TAG, "Cannot decrypt", e);
                return null;
            }
            LockscreenCredential result = LockscreenCredential.createManagedPassword(credential);
            Arrays.fill(credential, (byte) 0);
            return result;
        }
    }

    /** Remove the given user's password from cache, if one exists. */
    public void removePassword(int userId) {
        synchronized (mEncryptedPasswords) {
            String keyName = getEncryptionKeyName(userId);
            try {
                if (mKeyStore.containsAlias(keyName)) {
                    mKeyStore.deleteEntry(keyName);
                }
            } catch (KeyStoreException e) {
                Slog.d(TAG, "Cannot delete key", e);
            }
            if (mEncryptedPasswords.contains(userId)) {
                Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0);
                mEncryptedPasswords.remove(userId);
            }
        }
    }

    private static String getEncryptionKeyName(int userId) {
        return "com.android.server.locksettings.unified_profile_cache_" + userId;
    }
}
Loading