From a3c71a1460704e19628e6b841c262be6fc0eabd5 Mon Sep 17 00:00:00 2001 From: Rubin Xu Date: Wed, 4 Dec 2019 15:25:02 +0000 Subject: [PATCH] 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 --- .../internal/widget/ILockSettings.aidl | 2 + .../internal/widget/LockPatternUtils.java | 33 ++- .../locksettings/LockSettingsService.java | 69 +++++-- .../LockSettingsShellCommand.java | 21 +- .../ManagedProfilePasswordCache.java | 191 ++++++++++++++++++ .../android/server/pm/UserManagerService.java | 4 +- .../LockSettingsServiceTestable.java | 6 + 7 files changed, 307 insertions(+), 19 deletions(-) create mode 100644 services/core/java/com/android/server/locksettings/ManagedProfilePasswordCache.java diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl index de02eb0903918..e24e982bc9a8b 100644 --- a/core/java/com/android/internal/widget/ILockSettings.aidl +++ b/core/java/com/android/internal/widget/ILockSettings.aidl @@ -89,4 +89,6 @@ interface ILockSettings { in List applicationKeys); void closeSession(in String sessionId); boolean hasSecureLockScreen(); + boolean tryUnlockWithCachedUnifiedChallenge(int userId); + void removeCachedUnifiedChallenge(int userId); } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index f37a46811b81c..864429c989894 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -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(); + } + } } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index baae4ea502d53..1f4048f821e5a 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -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)) { - // 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); + if (profile.id == userId) continue; + if (!profile.isManagedProfile()) continue; + + if (hasUnifiedChallenge(profile.id)) { + if (mUserManager.isUserRunning(profile.id)) { + // Unlock managed profile with unified lock + // 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 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 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)); } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java index 4943c25b1f182..7b767b86f0d49 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java @@ -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 ] [--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); + } } diff --git a/services/core/java/com/android/server/locksettings/ManagedProfilePasswordCache.java b/services/core/java/com/android/server/locksettings/ManagedProfilePasswordCache.java new file mode 100644 index 0000000000000..d38ee678c7aab --- /dev/null +++ b/services/core/java/com/android/server/locksettings/ManagedProfilePasswordCache.java @@ -0,0 +1,191 @@ +/* + * 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). + * + *

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. + + *

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 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; + } +} diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 5511a54de2cd4..0cb8f49540be2 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -932,6 +932,7 @@ public class UserManagerService extends IUserManager.Stub { userId, true /* enableQuietMode */, target, callingPackage); return true; } + mLockPatternUtils.tryUnlockWithCachedUnifiedChallenge(userId); boolean needToShowConfirmCredential = mLockPatternUtils.isSecure(userId) && !StorageManager.isUserKeyUnlocked(userId); @@ -942,8 +943,7 @@ public class UserManagerService extends IUserManager.Stub { showConfirmCredentialToDisableQuietMode(userId, target); return false; } - setQuietModeEnabled( - userId, false /* enableQuietMode */, target, callingPackage); + setQuietModeEnabled(userId, false /* enableQuietMode */, target, callingPackage); return true; } finally { Binder.restoreCallingIdentity(identity); diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java index 271e8e27aed42..1ff451b50c427 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java @@ -147,6 +147,12 @@ public class LockSettingsServiceTestable extends LockSettingsService { public RecoverableKeyStoreManager getRecoverableKeyStoreManager(KeyStore keyStore) { return mRecoverableKeyStoreManager; } + + @Override + public ManagedProfilePasswordCache getManagedProfilePasswordCache() { + return mock(ManagedProfilePasswordCache.class); + } + } public MockInjector mInjector;