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

Commit a3c71a14 authored by Rubin Xu's avatar Rubin Xu
Browse files

Do not require reauth when turning on profile with unified challenge

Encrypt a copy of the profile unified challenge with an auth-bound key
of 7-day timeout, so that the framework can rederive the challenge as
long as the user has unlocked the device in the last 7 days.

Bug: 72178550
Test: atest com.android.server.locksettings
Test: atest com.android.cts.devicepolicy.QuietModeHostsideTest

Change-Id: Icb1335ebce2bd1a29f5a7111b61536578fccedd5
parent 4a8fc13d
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