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
This commit is contained in:
Rubin Xu
2019-12-04 15:25:02 +00:00
parent 4a8fc13dd9
commit a3c71a1460
7 changed files with 307 additions and 19 deletions

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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<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));
}

View File

@@ -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);
}
}

View File

@@ -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).
*
* <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;
}
}

View File

@@ -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);

View File

@@ -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;