Loading core/java/com/android/internal/widget/ILockSettings.aidl +2 −0 Original line number Diff line number Diff line Loading @@ -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); } core/java/com/android/internal/widget/LockPatternUtils.java +31 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); } } } services/core/java/com/android/server/locksettings/LockSettingsService.java +57 −12 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -387,6 +389,7 @@ public class LockSettingsService extends ILockSettings.Stub { setLockCredentialInternal(unifiedProfilePassword, managedUserPassword, managedUserId, /* isLockTiedToParent= */ true); tieProfileLockToParent(managedUserId, unifiedProfilePassword); mManagedProfilePasswordCache.storePassword(managedUserId, unifiedProfilePassword); } } Loading Loading @@ -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) { Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -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; } Loading Loading @@ -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. Loading Loading @@ -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) { Loading Loading @@ -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()) { Loading Loading @@ -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()) { Loading Loading @@ -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)); } Loading services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java +18 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -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; Loading Loading @@ -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(""); } } Loading Loading @@ -322,4 +332,9 @@ class LockSettingsShellCommand extends ShellCommand { return true; } } private void runRemoveCache() { mLockPatternUtils.removeCachedUnifiedChallenge(mCurrentUserId); getOutPrintWriter().println("Password cached removed for user " + mCurrentUserId); } } services/core/java/com/android/server/locksettings/ManagedProfilePasswordCache.java 0 → 100644 +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
core/java/com/android/internal/widget/ILockSettings.aidl +2 −0 Original line number Diff line number Diff line Loading @@ -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); }
core/java/com/android/internal/widget/LockPatternUtils.java +31 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); } } }
services/core/java/com/android/server/locksettings/LockSettingsService.java +57 −12 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -387,6 +389,7 @@ public class LockSettingsService extends ILockSettings.Stub { setLockCredentialInternal(unifiedProfilePassword, managedUserPassword, managedUserId, /* isLockTiedToParent= */ true); tieProfileLockToParent(managedUserId, unifiedProfilePassword); mManagedProfilePasswordCache.storePassword(managedUserId, unifiedProfilePassword); } } Loading Loading @@ -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) { Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -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; } Loading Loading @@ -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. Loading Loading @@ -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) { Loading Loading @@ -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()) { Loading Loading @@ -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()) { Loading Loading @@ -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)); } Loading
services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java +18 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -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; Loading Loading @@ -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(""); } } Loading Loading @@ -322,4 +332,9 @@ class LockSettingsShellCommand extends ShellCommand { return true; } } private void runRemoveCache() { mLockPatternUtils.removeCachedUnifiedChallenge(mCurrentUserId); getOutPrintWriter().println("Password cached removed for user " + mCurrentUserId); } }
services/core/java/com/android/server/locksettings/ManagedProfilePasswordCache.java 0 → 100644 +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; } }