diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index ef6e6c2bda10a..4bf58b9d95c1f 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -147,6 +147,10 @@ public class LockPatternUtils { public static final String PROFILE_KEY_NAME_ENCRYPT = "profile_key_name_encrypt_"; public static final String PROFILE_KEY_NAME_DECRYPT = "profile_key_name_decrypt_"; + public static final String SYNTHETIC_PASSWORD_KEY_PREFIX = "synthetic_password_"; + + public static final String SYNTHETIC_PASSWORD_HANDLE_KEY = "sp-handle"; + public static final String SYNTHETIC_PASSWORD_ENABLED_KEY = "enable-sp"; private final Context mContext; private final ContentResolver mContentResolver; @@ -1559,6 +1563,14 @@ public class LockPatternUtils { break; } } - }; + } + } + + public void enableSyntheticPassword() { + setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1L, UserHandle.USER_SYSTEM); + } + + public boolean isSyntheticPasswordEnabled() { + return getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM) != 0; } } diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java index a073d8ea9c5bf..c10bcf0de7170 100644 --- a/services/core/java/com/android/server/LockSettingsService.java +++ b/services/core/java/com/android/server/LockSettingsService.java @@ -20,6 +20,8 @@ import static android.Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE; import static android.Manifest.permission.READ_CONTACTS; import static android.content.Context.KEYGUARD_SERVICE; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; +import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY; +import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY; import android.annotation.UserIdInt; import android.app.ActivityManager; @@ -61,6 +63,7 @@ import android.os.storage.StorageManager; import android.provider.Settings; import android.provider.Settings.Secure; import android.provider.Settings.SettingNotFoundException; +import android.security.GateKeeper; import android.security.KeyStore; import android.security.keystore.AndroidKeyStoreProvider; import android.security.keystore.KeyProperties; @@ -79,6 +82,8 @@ import com.android.internal.widget.ILockSettings; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.VerifyCredentialResponse; import com.android.server.LockSettingsStorage.CredentialHash; +import com.android.server.SyntheticPasswordManager.AuthenticationResult; +import com.android.server.SyntheticPasswordManager.AuthenticationToken; import libcore.util.HexEncoding; @@ -86,6 +91,7 @@ import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -145,12 +151,14 @@ public class LockSettingsService extends ILockSettings.Stub { private boolean mFirstCallToVold; protected IGateKeeperService mGateKeeperService; + private SyntheticPasswordManager mSpManager; + /** * The UIDs that are used for system credential storage in keystore. */ private static final int[] SYSTEM_CREDENTIAL_UIDS = { Process.WIFI_UID, Process.VPN_UID, - Process.ROOT_UID, Process.SYSTEM_UID }; + Process.ROOT_UID }; // This class manages life cycle events for encrypted users on File Based Encryption (FBE) // devices. The most basic of these is to show/hide notifications about missing features until @@ -335,6 +343,14 @@ public class LockSettingsService extends ILockSettings.Stub { } return null; } + + public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) { + return new SyntheticPasswordManager(storage); + } + + public int binderGetCallingUid() { + return Binder.getCallingUid(); + } } public LockSettingsService(Context context) { @@ -365,6 +381,8 @@ public class LockSettingsService extends ILockSettings.Stub { mUserManager = injector.getUserManager(); mStrongAuthTracker = injector.getStrongAuthTracker(); mStrongAuthTracker.register(mStrongAuth); + + mSpManager = injector.getSyntheticPasswordManager(mStorage); } /** @@ -801,17 +819,42 @@ public class LockSettingsService extends ILockSettings.Stub { @Override public boolean havePassword(int userId) throws RemoteException { + synchronized (mSpManager) { + if (isSyntheticPasswordBasedCredentialLocked(userId)) { + long handle = getSyntheticPasswordHandleLocked(userId); + return mSpManager.getCredentialType(handle, userId) == + LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; + } + } // Do we need a permissions check here? return mStorage.hasPassword(userId); } @Override public boolean havePattern(int userId) throws RemoteException { + synchronized (mSpManager) { + if (isSyntheticPasswordBasedCredentialLocked(userId)) { + long handle = getSyntheticPasswordHandleLocked(userId); + return mSpManager.getCredentialType(handle, userId) == + LockPatternUtils.CREDENTIAL_TYPE_PATTERN; + } + } // Do we need a permissions check here? return mStorage.hasPattern(userId); } private boolean isUserSecure(int userId) { + synchronized (mSpManager) { + try { + if (isSyntheticPasswordBasedCredentialLocked(userId)) { + long handle = getSyntheticPasswordHandleLocked(userId); + return mSpManager.getCredentialType(handle, userId) != + LockPatternUtils.CREDENTIAL_TYPE_NONE; + } + } catch (RemoteException e) { + // fall through + } + } return mStorage.hasCredential(userId); } @@ -1021,6 +1064,13 @@ public class LockSettingsService extends ILockSettings.Stub { private void setLockCredentialInternal(String credential, int credentialType, String savedCredential, int userId) throws RemoteException { + synchronized (mSpManager) { + if (isSyntheticPasswordBasedCredentialLocked(userId)) { + spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential, + userId); + return; + } + } if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) { if (credential != null) { Slog.wtf(TAG, "CredentialType is none, but credential is non-null."); @@ -1061,7 +1111,16 @@ public class LockSettingsService extends ILockSettings.Stub { savedCredential = null; } } - + synchronized (mSpManager) { + if (shouldMigrateToSyntheticPasswordLocked(userId)) { + initializeSyntheticPasswordLocked(currentHandle.hash, savedCredential, + currentHandle.type, userId); + spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential, + userId); + return; + } + } + if (DEBUG) Slog.d(TAG, "setLockCredentialInternal: user=" + userId); byte[] enrolledHandle = enrollCredential(currentHandle.hash, savedCredential, credential, userId); if (enrolledHandle != null) { @@ -1189,6 +1248,11 @@ public class LockSettingsService extends ILockSettings.Stub { return hash; } + private void setAuthlessUserKeyProtection(int userId, byte[] key) throws RemoteException { + if (DEBUG) Slog.d(TAG, "setAuthlessUserKeyProtectiond: user=" + userId); + addUserKeyAuth(userId, null, key); + } + private void setUserKeyProtection(int userId, String credential, VerifyCredentialResponse vcr) throws RemoteException { if (DEBUG) Slog.d(TAG, "setUserKeyProtection: user=" + userId); @@ -1320,7 +1384,16 @@ public class LockSettingsService extends ILockSettings.Stub { if (TextUtils.isEmpty(credential)) { throw new IllegalArgumentException("Credential can't be null or empty"); } - + synchronized (mSpManager) { + if (isSyntheticPasswordBasedCredentialLocked(userId)) { + VerifyCredentialResponse response = spBasedDoVerifyCredentialLocked(credential, + credentialType, hasChallenge, challenge, userId, progressCallback); + if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) { + mStrongAuth.reportSuccessfulStrongAuthUnlock(userId); + } + return response; + } + } CredentialHash storedHash = mStorage.readCredentialHash(userId); if (storedHash.type != credentialType) { Slog.wtf(TAG, "doVerifyCredential type mismatch with stored credential??" @@ -1456,8 +1529,8 @@ public class LockSettingsService extends ILockSettings.Stub { notifyActivePasswordMetricsAvailable(credential, userId); unlockKeystore(credential, userId); - Slog.i(TAG, "Unlocking user " + userId + - " with token length " + response.getPayload().length); + Slog.i(TAG, "Unlocking user " + userId + " with token length " + + response.getPayload().length); unlockUser(userId, response.getPayload(), secretFromCredential(credential)); if (isManagedProfileWithSeparatedLock(userId)) { @@ -1467,6 +1540,15 @@ public class LockSettingsService extends ILockSettings.Stub { } if (shouldReEnroll) { setLockCredentialInternal(credential, storedHash.type, credential, userId); + } else { + // Now that we've cleared of all required GK migration, let's do the final + // migration to synthetic password. + synchronized (mSpManager) { + if (shouldMigrateToSyntheticPasswordLocked(userId)) { + initializeSyntheticPasswordLocked(storedHash.hash, credential, + storedHash.type, userId); + } + } } } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) { if (response.getTimeout() > 0) { @@ -1697,7 +1779,7 @@ public class LockSettingsService extends ILockSettings.Stub { } } - private synchronized IGateKeeperService getGateKeeperService() + protected synchronized IGateKeeperService getGateKeeperService() throws RemoteException { if (mGateKeeperService != null) { return mGateKeeperService; @@ -1713,4 +1795,267 @@ public class LockSettingsService extends ILockSettings.Stub { Slog.e(TAG, "Unable to acquire GateKeeperService"); return null; } + + /** + * Precondition: vold and keystore unlocked. + * + * Create new synthetic password, set up synthetic password blob protected by the supplied + * user credential, and make the newly-created SP blob active. + * + * The invariant under a synthetic password is: + * 1. If user credential exists, then both vold and keystore and protected with keys derived + * from the synthetic password. + * 2. If user credential does not exist, vold and keystore protection are cleared. This is to + * make it consistent with current behaviour. It also allows ActivityManager to call + * unlockUser() with empty secret. + * 3. Once a user is migrated to have synthetic password, its value will never change, no matter + * whether the user changes his lockscreen PIN or clear/reset it. When the user clears its + * lockscreen PIN, we still maintain the existing synthetic password in a password blob + * protected by a default PIN. The only exception is when the DPC performs an untrusted + * credential change, in which case we have no way to derive the existing synthetic password + * and has to create a new one. + * 4. The user SID is linked with synthetic password, but its cleared/re-created when the user + * clears/re-creates his lockscreen PIN. + * + * + * Different cases of calling this method: + * 1. credentialHash != null + * This implies credential != null, a new SP blob will be provisioned, and existing SID + * migrated to associate with the new SP. + * This happens during a normal migration case when the user currently has password. + * + * 2. credentialhash == null and credential == null + * A new SP blob and a new SID will be created, while the user has no credentials. + * This can happens when we are activating an escrow token on a unsecured device, during + * which we want to create the SP structure with an empty user credential. + * + * 3. credentialhash == null and credential != null + * This is the untrusted credential reset, OR the user sets a new lockscreen password + * FOR THE FIRST TIME on a SP-enabled device. New credential and new SID will be created + */ + private AuthenticationToken initializeSyntheticPasswordLocked(byte[] credentialHash, + String credential, int credentialType, int userId) throws RemoteException { + Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId); + AuthenticationToken auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(), + credentialHash, credential, userId); + if (auth == null) { + Slog.wtf(TAG, "initializeSyntheticPasswordLocked returns null auth token"); + return null; + } + long handle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(), + credential, credentialType, auth, userId); + if (credential != null) { + if (credentialHash == null) { + // Since when initializing SP, we didn't provide an existing password handle + // for it to migrate SID, we need to create a new SID for the user. + mSpManager.newSidForUser(getGateKeeperService(), auth, userId); + } + mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId); + setAuthlessUserKeyProtection(userId, auth.deriveDiskEncryptionKey()); + setKeystorePassword(auth.deriveKeyStorePassword(), userId); + } else { + clearUserKeyProtection(userId); + setKeystorePassword(null, userId); + getGateKeeperService().clearSecureUserId(userId); + } + fixateNewestUserKeyAuth(userId); + setLong(SYNTHETIC_PASSWORD_HANDLE_KEY, handle, userId); + return auth; + } + + private long getSyntheticPasswordHandleLocked(int userId) { + try { + return getLong(SYNTHETIC_PASSWORD_HANDLE_KEY, 0, userId); + } catch (RemoteException e) { + return SyntheticPasswordManager.DEFAULT_HANDLE; + } + } + + private boolean isSyntheticPasswordBasedCredentialLocked(int userId) throws RemoteException { + long handle = getSyntheticPasswordHandleLocked(userId); + // This is a global setting + long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM); + return enabled != 0 && handle != SyntheticPasswordManager.DEFAULT_HANDLE; + } + + private boolean shouldMigrateToSyntheticPasswordLocked(int userId) throws RemoteException { + long handle = getSyntheticPasswordHandleLocked(userId); + // This is a global setting + long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM); + return enabled != 0 && handle == SyntheticPasswordManager.DEFAULT_HANDLE; + } + + private void enableSyntheticPasswordLocked() throws RemoteException { + setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1, UserHandle.USER_SYSTEM); + } + + private VerifyCredentialResponse spBasedDoVerifyCredentialLocked(String userCredential, int + credentialType, boolean hasChallenge, long challenge, int userId, + ICheckCredentialProgressCallback progressCallback) throws RemoteException { + if (DEBUG) Slog.d(TAG, "spBasedDoVerifyCredentialLocked: user=" + userId); + if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) { + userCredential = null; + } + long handle = getSyntheticPasswordHandleLocked(userId); + AuthenticationResult authResult = mSpManager.unwrapPasswordBasedSyntheticPassword( + getGateKeeperService(), handle, userCredential, userId); + + VerifyCredentialResponse response = authResult.gkResponse; + if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) { + // credential has matched + // perform verifyChallenge with synthetic password which generates the real auth + // token for the current user + response = mSpManager.verifyChallenge(getGateKeeperService(), authResult.authToken, + challenge, userId); + if (response.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK) { + Slog.wtf(TAG, "verifyChallenge with SP failed."); + return VerifyCredentialResponse.ERROR; + } + if (progressCallback != null) { + progressCallback.onCredentialVerified(); + } + notifyActivePasswordMetricsAvailable(userCredential, userId); + unlockKeystore(authResult.authToken.deriveKeyStorePassword(), userId); + + final byte[] secret = authResult.authToken.deriveDiskEncryptionKey(); + Slog.i(TAG, "Unlocking user " + userId + " with secret only, length " + secret.length); + unlockUser(userId, null, secret); + + if (isManagedProfileWithSeparatedLock(userId)) { + TrustManager trustManager = + (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE); + trustManager.setDeviceLockedForUser(userId, false); + } + } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) { + if (response.getTimeout() > 0) { + requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId); + } + } + + return response; + } + + /** + * Change the user's lockscreen password by creating a new SP blob and update the handle, based + * on an existing authentication token. Even though a new SP blob is created, the underlying + * synthetic password is never changed. + * + * When clearing credential, we keep the SP unchanged, but clear its password handle so its + * SID is gone. We also clear password from (software-based) keystore and vold, which will be + * added back when new password is set in future. + */ + private long setLockCredentialWithAuthTokenLocked(String credential, int credentialType, + AuthenticationToken auth, int userId) throws RemoteException { + if (DEBUG) Slog.d(TAG, "setLockCredentialWithAuthTokenLocked: user=" + userId); + long newHandle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(), + credential, credentialType, auth, userId); + final Map profilePasswords; + if (credential != null) { + // // not needed by synchronizeUnifiedWorkChallengeForProfiles() + profilePasswords = null; + + if (mSpManager.hasSidForUser(userId)) { + // We are changing password of a secured device, nothing more needed as + // createPasswordBasedSyntheticPassword has already taken care of maintaining + // the password handle and SID unchanged. + + //refresh auth token + mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId); + } else { + // A new password is set on a previously-unsecured device, we need to generate + // a new SID, and re-add keys to vold and keystore. + mSpManager.newSidForUser(getGateKeeperService(), auth, userId); + mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId); + setAuthlessUserKeyProtection(userId, auth.deriveDiskEncryptionKey()); + fixateNewestUserKeyAuth(userId); + setKeystorePassword(auth.deriveKeyStorePassword(), userId); + } + } else { + // Cache all profile password if they use unified work challenge. This will later be + // used to clear the profile's password in synchronizeUnifiedWorkChallengeForProfiles() + profilePasswords = getDecryptedPasswordsForAllTiedProfiles(userId); + + // we are clearing password of a secured device, so need to nuke SID as well. + mSpManager.clearSidForUser(userId); + getGateKeeperService().clearSecureUserId(userId); + // Clear key from vold so ActivityManager can just unlock the user with empty secret + // during boot. + clearUserKeyProtection(userId); + fixateNewestUserKeyAuth(userId); + setKeystorePassword(null, userId); + } + setLong(SYNTHETIC_PASSWORD_HANDLE_KEY, newHandle, userId); + synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords); + return newHandle; + } + + private void spBasedSetLockCredentialInternalLocked(String credential, int credentialType, + String savedCredential, int userId) throws RemoteException { + if (DEBUG) Slog.d(TAG, "spBasedSetLockCredentialInternalLocked: user=" + userId); + if (isManagedProfileWithUnifiedLock(userId)) { + // get credential from keystore when managed profile has unified lock + try { + savedCredential = getDecryptedPasswordForTiedProfile(userId); + } catch (FileNotFoundException e) { + Slog.i(TAG, "Child profile key not found"); + } catch (UnrecoverableKeyException | InvalidKeyException | KeyStoreException + | NoSuchAlgorithmException | NoSuchPaddingException + | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException | CertificateException | IOException e) { + Slog.e(TAG, "Failed to decrypt child profile key", e); + } + } + long handle = getSyntheticPasswordHandleLocked(userId); + AuthenticationToken auth = mSpManager.unwrapPasswordBasedSyntheticPassword( + getGateKeeperService(), handle, savedCredential, userId).authToken; + if (auth != null) { + // We are performing a trusted credential change i.e. a correct existing credential + // is provided + setLockCredentialWithAuthTokenLocked(credential, credentialType, auth, userId); + mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId); + } else { + // We are performing an untrusted credential change i.e. by DevicePolicyManager. + // So provision a new SP and SID. This would invalidate existing escrow tokens. + // Still support this for now but this flow will be removed in the next release. + + Slog.w(TAG, "Untrusted credential change invoked"); + initializeSyntheticPasswordLocked(null, credential, credentialType, userId); + synchronizeUnifiedWorkChallengeForProfiles(userId, null); + mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId); + } + notifyActivePasswordMetricsAvailable(credential, userId); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args){ + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + + pw.println("Permission Denial: can't dump LockSettingsService from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + synchronized (this) { + pw.println("Current lock settings service state:"); + pw.println(String.format("SP Enabled = %b", + mLockPatternUtils.isSyntheticPasswordEnabled())); + + List users = mUserManager.getUsers(); + for (int user = 0; user < users.size(); user++) { + final int userId = users.get(user).id; + pw.println(" User " + userId); + pw.println(String.format(" SP Handle = %x", + getSyntheticPasswordHandleLocked(userId))); + try { + pw.println(String.format(" SID = %x", + getGateKeeperService().getSecureUserId(userId))); + } catch (RemoteException e) { + // ignore. + } + } + } + } + } diff --git a/services/core/java/com/android/server/LockSettingsShellCommand.java b/services/core/java/com/android/server/LockSettingsShellCommand.java index 1ab53035a3ee7..91bd98eb5f5fa 100644 --- a/services/core/java/com/android/server/LockSettingsShellCommand.java +++ b/services/core/java/com/android/server/LockSettingsShellCommand.java @@ -22,12 +22,9 @@ import static com.android.internal.widget.LockPatternUtils.stringToPattern; import android.app.ActivityManager; import android.content.Context; -import android.os.Binder; -import android.os.Process; import android.os.RemoteException; import android.os.ShellCommand; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternUtils.RequestThrottledException; @@ -37,6 +34,7 @@ class LockSettingsShellCommand extends ShellCommand { private static final String COMMAND_SET_PIN = "set-pin"; private static final String COMMAND_SET_PASSWORD = "set-password"; private static final String COMMAND_CLEAR = "clear"; + private static final String COMMAND_SP = "sp"; private int mCurrentUserId; private final LockPatternUtils mLockPatternUtils; @@ -71,6 +69,9 @@ class LockSettingsShellCommand extends ShellCommand { case COMMAND_CLEAR: runClear(); break; + case COMMAND_SP: + runEnableSp(); + break; default: getErrPrintWriter().println("Unknown command: " + cmd); break; @@ -92,6 +93,8 @@ class LockSettingsShellCommand extends ShellCommand { while ((opt = getNextOption()) != null) { if ("--old".equals(opt)) { mOld = getNextArgRequired(); + } else if ("--user".equals(opt)) { + mCurrentUserId = Integer.parseInt(getNextArgRequired()); } else { getErrPrintWriter().println("Unknown option: " + opt); throw new IllegalArgumentException(); @@ -100,6 +103,15 @@ class LockSettingsShellCommand extends ShellCommand { mNew = getNextArg(); } + private void runEnableSp() { + if (mNew != null) { + mLockPatternUtils.enableSyntheticPassword(); + getOutPrintWriter().println("Synthetic password enabled"); + } + getOutPrintWriter().println(String.format("SP Enabled = %b", + mLockPatternUtils.isSyntheticPasswordEnabled())); + } + private void runSetPattern() throws RemoteException { mLockPatternUtils.saveLockPattern(stringToPattern(mNew), mOld, mCurrentUserId); getOutPrintWriter().println("Pattern set to '" + mNew + "'"); diff --git a/services/core/java/com/android/server/LockSettingsStorage.java b/services/core/java/com/android/server/LockSettingsStorage.java index 385b1cf4e4489..f5bae7c14d975 100644 --- a/services/core/java/com/android/server/LockSettingsStorage.java +++ b/services/core/java/com/android/server/LockSettingsStorage.java @@ -66,6 +66,8 @@ class LockSettingsStorage { private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key"; private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key"; + private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/"; + private static final Object DEFAULT = new Object(); private final DatabaseHelper mOpenHelper; @@ -412,8 +414,7 @@ class LockSettingsStorage { } private String getLockCredentialFilePathForUser(int userId, String basename) { - String dataSystemDirectory = - android.os.Environment.getDataDirectory().getAbsolutePath() + + String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() + SYSTEM_DIRECTORY; if (userId == 0) { // Leave it in the same place for user 0 @@ -423,6 +424,40 @@ class LockSettingsStorage { } } + public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) { + writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data); + } + + public byte[] readSyntheticPasswordState(int userId, long handle, String name) { + return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name)); + } + + public void deleteSyntheticPasswordState(int userId, long handle, String name, boolean secure) { + String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name); + File file = new File(path); + if (file.exists()) { + //TODO: (b/34600579) invoke secdiscardable + file.delete(); + mCache.putFile(path, null); + } + } + + @VisibleForTesting + protected File getSyntheticPasswordDirectoryForUser(int userId) { + return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY); + } + + @VisibleForTesting + protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle, + String name) { + File baseDir = getSyntheticPasswordDirectoryForUser(userId); + String baseName = String.format("%016x.%s", handle, name); + if (!baseDir.exists()) { + baseDir.mkdir(); + } + return new File(baseDir, baseName).getAbsolutePath(); + } + public void removeUser(int userId) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); @@ -446,15 +481,20 @@ class LockSettingsStorage { } } } else { - // Manged profile + // Managed profile removeChildProfileLock(userId); } + File spStateDir = getSyntheticPasswordDirectoryForUser(userId); try { db.beginTransaction(); db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null); db.setTransactionSuccessful(); mCache.removeUser(userId); + // The directory itself will be deleted as part of user deletion operation by the + // framework, so only need to purge cache here. + //TODO: (b/34600579) invoke secdiscardable + mCache.purgePath(spStateDir.getAbsolutePath()); } finally { db.endTransaction(); } @@ -619,6 +659,16 @@ class LockSettingsStorage { mVersion++; } + synchronized void purgePath(String path) { + for (int i = mCache.size() - 1; i >= 0; i--) { + CacheKey entry = mCache.keyAt(i); + if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) { + mCache.removeAt(i); + } + } + mVersion++; + } + synchronized void clear() { mCache.clear(); mVersion++; diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 32b7e4d1b5b44..041597168eaa8 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -2922,10 +2922,10 @@ class StorageManagerService extends IStorageManager.Stub waitForReady(); if (StorageManager.isFileEncryptedNativeOrEmulated()) { - // When a user has secure lock screen, require a challenge token to - // actually unlock. This check is mostly in place for emulation mode. - if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(token)) { - throw new IllegalStateException("Token required to unlock secure user " + userId); + // When a user has secure lock screen, require secret to actually unlock. + // This check is mostly in place for emulation mode. + if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(secret)) { + throw new IllegalStateException("Secret required to unlock secure user " + userId); } try { diff --git a/services/core/java/com/android/server/SyntheticPasswordCrypto.java b/services/core/java/com/android/server/SyntheticPasswordCrypto.java new file mode 100644 index 0000000000000..12d91c57cdada --- /dev/null +++ b/services/core/java/com/android/server/SyntheticPasswordCrypto.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2017 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; + +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyProtection; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class SyntheticPasswordCrypto { + private static final int PROFILE_KEY_IV_SIZE = 12; + private static final int AES_KEY_LENGTH = 32; // 256-bit AES key + private static final byte[] APPLICATION_ID_PERSONALIZATION = "application-id".getBytes(); + // Time between the user credential is verified with GK and the decryption of synthetic password + // under the auth-bound key. This should always happen one after the other, but give it 15 + // seconds just to be sure. + private static final int USER_AUTHENTICATION_VALIDITY = 15; + + private static byte[] decrypt(SecretKey key, byte[] blob) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + if (blob == null) { + return null; + } + byte[] iv = Arrays.copyOfRange(blob, 0, PROFILE_KEY_IV_SIZE); + byte[] ciphertext = Arrays.copyOfRange(blob, PROFILE_KEY_IV_SIZE, blob.length); + Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE); + cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + return cipher.doFinal(ciphertext); + } + + private static byte[] encrypt(SecretKey key, byte[] blob) + throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + if (blob == null) { + return null; + } + Cipher cipher = Cipher.getInstance( + KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + + KeyProperties.ENCRYPTION_PADDING_NONE); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] ciphertext = cipher.doFinal(blob); + byte[] iv = cipher.getIV(); + if (iv.length != PROFILE_KEY_IV_SIZE) { + throw new RuntimeException("Invalid iv length: " + iv.length); + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(iv); + outputStream.write(ciphertext); + return outputStream.toByteArray(); + } + + public static byte[] encrypt(byte[] keyBytes, byte[] personalisation, byte[] message) { + byte[] keyHash = personalisedHash(personalisation, keyBytes); + SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(keyHash, AES_KEY_LENGTH), + KeyProperties.KEY_ALGORITHM_AES); + try { + return encrypt(key, message); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException + | IllegalBlockSizeException | BadPaddingException | IOException e) { + e.printStackTrace(); + return null; + } + } + + public static byte[] decrypt(byte[] keyBytes, byte[] personalisation, byte[] ciphertext) { + byte[] keyHash = personalisedHash(personalisation, keyBytes); + SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(keyHash, AES_KEY_LENGTH), + KeyProperties.KEY_ALGORITHM_AES); + try { + return decrypt(key, ciphertext); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException + | IllegalBlockSizeException | BadPaddingException + | InvalidAlgorithmParameterException e) { + e.printStackTrace(); + return null; + } + } + + public static byte[] decryptBlob(String keyAlias, byte[] blob, byte[] applicationId) { + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + SecretKey decryptionKey = (SecretKey) keyStore.getKey(keyAlias, null); + byte[] intermediate = decrypt(applicationId, APPLICATION_ID_PERSONALIZATION, blob); + return decrypt(decryptionKey, intermediate); + } catch (CertificateException | IOException | BadPaddingException + | IllegalBlockSizeException + | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException + | InvalidKeyException | UnrecoverableKeyException + | InvalidAlgorithmParameterException e) { + e.printStackTrace(); + throw new RuntimeException("Failed to decrypt blob", e); + } + } + + public static byte[] createBlob(String keyAlias, byte[] data, byte[] applicationId, long sid) { + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES); + keyGenerator.init(new SecureRandom()); + SecretKey secretKey = keyGenerator.generateKey(); + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + KeyProtection.Builder builder = new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE); + if (sid != 0) { + builder.setUserAuthenticationRequired(true) + .setBoundToSpecificSecureUserId(sid) + .setUserAuthenticationValidityDurationSeconds(USER_AUTHENTICATION_VALIDITY); + } + keyStore.setEntry(keyAlias, + new KeyStore.SecretKeyEntry(secretKey), + builder.build()); + byte[] intermediate = encrypt(secretKey, data); + return encrypt(applicationId, APPLICATION_ID_PERSONALIZATION, intermediate); + + } catch (CertificateException | IOException | BadPaddingException + | IllegalBlockSizeException + | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException + | InvalidKeyException e) { + e.printStackTrace(); + throw new RuntimeException("Failed to encrypt blob", e); + } + } + + public static void destroyBlobKey(String keyAlias) { + KeyStore keyStore; + try { + keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + keyStore.deleteEntry(keyAlias); + } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException + | IOException e) { + e.printStackTrace(); + } + } + + protected static byte[] personalisedHash(byte[] personalisation, byte[]... message) { + try { + final int PADDING_LENGTH = 128; + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + if (personalisation.length > PADDING_LENGTH) { + throw new RuntimeException("Personalisation too long"); + } + // Personalize the hash + // Pad it to the block size of the hash function + personalisation = Arrays.copyOf(personalisation, PADDING_LENGTH); + digest.update(personalisation); + for (byte[] data : message) { + digest.update(data); + } + return digest.digest(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("NoSuchAlgorithmException for SHA-512", e); + } + } +} diff --git a/services/core/java/com/android/server/SyntheticPasswordManager.java b/services/core/java/com/android/server/SyntheticPasswordManager.java new file mode 100644 index 0000000000000..0449d2007b0d4 --- /dev/null +++ b/services/core/java/com/android/server/SyntheticPasswordManager.java @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2017 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.RemoteException; +import android.service.gatekeeper.GateKeeperResponse; +import android.service.gatekeeper.IGateKeeperService; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.VerifyCredentialResponse; + +import libcore.util.HexEncoding; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + + +/** + * A class that maintains the wrapping of synthetic password by user credentials or escrow tokens. + * It's (mostly) a pure storage for synthetic passwords, providing APIs to creating and destroying + * synthetic password blobs which are wrapped by user credentials or escrow tokens. + * + * Here is the assumptions it makes: + * Each user has one single synthetic password at any time. + * The SP has an associated password handle, which binds to the SID for that user. The password + * handle is persisted by SyntheticPasswordManager internally. + * If the user credential is null, it's treated as if the credential is DEFAULT_PASSWORD + */ +public class SyntheticPasswordManager { + private static final String SP_BLOB_NAME = "spblob"; + private static final String SP_E0_NAME = "e0"; + private static final String SP_P1_NAME = "p1"; + private static final String SP_HANDLE_NAME = "handle"; + private static final String SECDISCARDABLE_NAME = "secdis"; + private static final int SECDISCARDABLE_LENGTH = 16 * 1024; + private static final String PASSWORD_DATA_NAME = "pwd"; + + public static final long DEFAULT_HANDLE = 0; + private static final String DEFAULT_PASSWORD = "default-password"; + + private static final byte SYNTHETIC_PASSWORD_VERSION = 1; + private static final byte SYNTHETIC_PASSWORD_PASSWORD_BASED = 0; + + // 256-bit synthetic password + private static final byte SYNTHETIC_PASSWORD_LENGTH = 256 / 8; + + private static final int PASSWORD_SCRYPT_N = 13; + private static final int PASSWORD_SCRYPT_R = 3; + private static final int PASSWORD_SCRYPT_P = 1; + private static final int PASSWORD_SALT_LENGTH = 16; + private static final int PASSWORD_TOKEN_LENGTH = 32; + private static final String TAG = "SyntheticPasswordManager"; + + private static final byte[] PERSONALISATION_SECDISCARDABLE = "secdiscardable-transform".getBytes(); + private static final byte[] PERSONALIZATION_KEY_STORE_PASSWORD = "keystore-password".getBytes(); + private static final byte[] PERSONALIZATION_USER_GK_AUTH = "user-gk-authentication".getBytes(); + private static final byte[] PERSONALIZATION_SP_GK_AUTH = "sp-gk-authentication".getBytes(); + private static final byte[] PERSONALIZATION_FBE_KEY = "fbe-key".getBytes(); + private static final byte[] PERSONALIZATION_SP_SPLIT = "sp-split".getBytes(); + private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes(); + + static class AuthenticationResult { + public AuthenticationToken authToken; + public VerifyCredentialResponse gkResponse; + } + + static class AuthenticationToken { + /* + * Here is the relationship between all three fields: + * P0 and P1 are two randomly-generated blocks. P1 is stored on disk but P0 is not. + * syntheticPassword = hash(P0 || P1) + * E0 = P0 encrypted under syntheticPassword, stored on disk. + */ + private @Nullable byte[] E0; + private @Nullable byte[] P1; + private @NonNull String syntheticPassword; + + public String deriveKeyStorePassword() { + return bytesToHex(SyntheticPasswordCrypto.personalisedHash( + PERSONALIZATION_KEY_STORE_PASSWORD, syntheticPassword.getBytes())); + } + + public byte[] deriveGkPassword() { + return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_SP_GK_AUTH, + syntheticPassword.getBytes()); + } + + public byte[] deriveDiskEncryptionKey() { + return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_FBE_KEY, + syntheticPassword.getBytes()); + } + + public void initialize(byte[] P0, byte[] P1) { + this.P1 = P1; + this.syntheticPassword = String.valueOf(HexEncoding.encode( + SyntheticPasswordCrypto.personalisedHash( + PERSONALIZATION_SP_SPLIT, P0, P1))); + this.E0 = SyntheticPasswordCrypto.encrypt(this.syntheticPassword.getBytes(), + PERSONALIZATION_E0, P0); + } + + protected static AuthenticationToken create() { + AuthenticationToken result = new AuthenticationToken(); + result.initialize(secureRandom(SYNTHETIC_PASSWORD_LENGTH), + secureRandom(SYNTHETIC_PASSWORD_LENGTH)); + return result; + } + + public byte[] computeP0() { + if (E0 == null) { + return null; + } + return SyntheticPasswordCrypto.decrypt(syntheticPassword.getBytes(), PERSONALIZATION_E0, + E0); + } + } + + static class PasswordData { + byte scryptN; + byte scryptR; + byte scryptP; + public int passwordType; + byte[] salt; + public byte[] passwordHandle; + + public static PasswordData create(int passwordType) { + PasswordData result = new PasswordData(); + result.scryptN = PASSWORD_SCRYPT_N; + result.scryptR = PASSWORD_SCRYPT_R; + result.scryptP = PASSWORD_SCRYPT_P; + result.passwordType = passwordType; + result.salt = secureRandom(PASSWORD_SALT_LENGTH); + return result; + } + + public static PasswordData fromBytes(byte[] data) { + PasswordData result = new PasswordData(); + ByteBuffer buffer = ByteBuffer.allocate(data.length); + buffer.put(data, 0, data.length); + buffer.flip(); + result.passwordType = buffer.getInt(); + result.scryptN = buffer.get(); + result.scryptR = buffer.get(); + result.scryptP = buffer.get(); + int saltLen = buffer.getInt(); + result.salt = new byte[saltLen]; + buffer.get(result.salt); + int handleLen = buffer.getInt(); + result.passwordHandle = new byte[handleLen]; + buffer.get(result.passwordHandle); + return result; + } + + public byte[] toBytes() { + ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 3 * Byte.BYTES + + Integer.BYTES + salt.length + Integer.BYTES + passwordHandle.length); + buffer.putInt(passwordType); + buffer.put(scryptN); + buffer.put(scryptR); + buffer.put(scryptP); + buffer.putInt(salt.length); + buffer.put(salt); + buffer.putInt(passwordHandle.length); + buffer.put(passwordHandle); + return buffer.array(); + } + } + + private LockSettingsStorage mStorage; + + public SyntheticPasswordManager(LockSettingsStorage storage) { + mStorage = storage; + } + + + public int getCredentialType(long handle, int userId) { + byte[] passwordData = loadState(PASSWORD_DATA_NAME, handle, userId); + if (passwordData == null) { + Log.w(TAG, "getCredentialType: encountered empty password data for user " + userId); + return LockPatternUtils.CREDENTIAL_TYPE_NONE; + } + return PasswordData.fromBytes(passwordData).passwordType; + } + + /** + * Initializing a new Authentication token, possibly from an existing credential and hash. + * + * The authentication token would bear a randomly-generated synthetic password. + * + * This method has the side effect of rebinding the SID of the given user to the + * newly-generated SP. + * + * If the existing credential hash is non-null, the existing SID mill be migrated so + * the synthetic password in the authentication token will produce the same SID + * (the corresponding synthetic password handle is persisted by SyntheticPasswordManager + * in a per-user data storage. + * + * If the existing credential hash is null, it means the given user should have no SID so + * SyntheticPasswordManager will nuke any SP handle previously persisted. In this case, + * the supplied credential parameter is also ignored. + * + * Also saves the escrow information necessary to re-generate the synthetic password under + * an escrow scheme. This information can be removed with {@link #destroyEscrowData} if + * password escrow should be disabled completely on the given user. + * + */ + public AuthenticationToken newSyntheticPasswordAndSid(IGateKeeperService gatekeeper, + byte[] hash, String credential, int userId) throws RemoteException { + AuthenticationToken result = AuthenticationToken.create(); + GateKeeperResponse response; + if (hash != null) { + response = gatekeeper.enroll(userId, hash, credential.getBytes(), + result.deriveGkPassword()); + if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) { + Log.w(TAG, "Fail to migrate SID, assuming no SID, user " + userId); + clearSidForUser(userId); + } else { + saveSyntheticPasswordHandle(response.getPayload(), userId); + } + } else { + clearSidForUser(userId); + } + saveEscrowData(result, userId); + return result; + } + + /** + * Enroll a new password handle and SID for the given synthetic password and persist it on disk. + * Used when adding password to previously-unsecured devices. + */ + public void newSidForUser(IGateKeeperService gatekeeper, AuthenticationToken authToken, + int userId) throws RemoteException { + GateKeeperResponse response = gatekeeper.enroll(userId, null, null, + authToken.deriveGkPassword()); + if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) { + Log.e(TAG, "Fail to create new SID for user " + userId); + return; + } + saveSyntheticPasswordHandle(response.getPayload(), userId); + } + + // Nuke the SP handle (and as a result, its SID) for the given user. + public void clearSidForUser(int userId) { + destroyState(SP_HANDLE_NAME, true, DEFAULT_HANDLE, userId); + } + + public boolean hasSidForUser(int userId) { + return hasState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId); + } + + // if null, it means there is no SID associated with the user + // This can happen if the user is migrated to SP but currently + // do not have a lockscreen password. + private byte[] loadSyntheticPasswordHandle(int userId) { + return loadState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId); + } + + private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) { + saveState(SP_HANDLE_NAME, spHandle, DEFAULT_HANDLE, userId); + } + + private boolean loadEscrowData(AuthenticationToken authToken, int userId) { + authToken.E0 = loadState(SP_E0_NAME, DEFAULT_HANDLE, userId); + authToken.P1 = loadState(SP_P1_NAME, DEFAULT_HANDLE, userId); + return authToken.E0 != null && authToken.P1 != null; + } + + private void saveEscrowData(AuthenticationToken authToken, int userId) { + saveState(SP_E0_NAME, authToken.E0, DEFAULT_HANDLE, userId); + saveState(SP_P1_NAME, authToken.P1, DEFAULT_HANDLE, userId); + } + + public void destroyEscrowData(int userId) { + destroyState(SP_E0_NAME, true, DEFAULT_HANDLE, userId); + destroyState(SP_P1_NAME, true, DEFAULT_HANDLE, userId); + } + + /** + * Create a new password based SP blob based on the supplied authentication token, such that + * a future successful authentication with unwrapPasswordBasedSyntheticPassword() would result + * in the same authentication token. + * + * This method only creates SP blob wrapping around the given synthetic password and does not + * handle logic around SID or SP handle. The caller should separately ensure that the user's SID + * is consistent with the device state by calling other APIs in this class. + * + * @see #newSidForUser + * @see #clearSidForUser + */ + public long createPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper, + String credential, int credentialType, AuthenticationToken authToken, int userId) + throws RemoteException { + if (credential == null || credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) { + credentialType = LockPatternUtils.CREDENTIAL_TYPE_NONE; + credential = DEFAULT_PASSWORD; + } + + long handle = generateHandle(); + PasswordData pwd = PasswordData.create(credentialType); + byte[] pwdToken = computePasswordToken(credential, pwd); + + GateKeeperResponse response = gatekeeper.enroll(fakeUid(userId), null, null, + passwordTokenToGkInput(pwdToken)); + if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) { + Log.e(TAG, "Fail to enroll user password when creating SP for user " + userId); + return 0; + } + pwd.passwordHandle = response.getPayload(); + long sid = sidFromPasswordHandle(pwd.passwordHandle); + saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId); + + byte[] applicationId = transformUnderSecdiscardable(pwdToken, + createSecdiscardable(handle, userId)); + createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED, authToken, + applicationId, sid, userId); + return handle; + } + + private void createSyntheticPasswordBlob(long handle, byte type, AuthenticationToken authToken, + byte[] applicationId, long sid, int userId) { + final byte[] secret = authToken.syntheticPassword.getBytes(); + byte[] content = createSPBlob(getHandleName(handle), secret, applicationId, sid); + byte[] blob = new byte[content.length + 1 + 1]; + blob[0] = SYNTHETIC_PASSWORD_VERSION; + blob[1] = type; + System.arraycopy(content, 0, blob, 2, content.length); + saveState(SP_BLOB_NAME, blob, handle, userId); + } + + /** + * Decrypt a synthetic password by supplying the user credential and corresponding password + * blob handle generated previously. If the decryption is successful, initiate a GateKeeper + * verification to referesh the SID & Auth token maintained by the system. + */ + public AuthenticationResult unwrapPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper, + long handle, String credential, int userId) throws RemoteException { + if (credential == null) { + credential = DEFAULT_PASSWORD; + } + AuthenticationResult result = new AuthenticationResult(); + PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, handle, userId)); + byte[] pwdToken = computePasswordToken(credential, pwd); + byte[] gkPwdToken = passwordTokenToGkInput(pwdToken); + + GateKeeperResponse response = gatekeeper.verifyChallenge(fakeUid(userId), 0L, + pwd.passwordHandle, gkPwdToken); + int responseCode = response.getResponseCode(); + if (responseCode == GateKeeperResponse.RESPONSE_OK) { + result.gkResponse = VerifyCredentialResponse.OK; + if (response.getShouldReEnroll()) { + GateKeeperResponse reenrollResponse = gatekeeper.enroll(fakeUid(userId), + pwd.passwordHandle, gkPwdToken, gkPwdToken); + if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) { + pwd.passwordHandle = reenrollResponse.getPayload(); + saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId); + } else { + Log.w(TAG, "Fail to re-enroll user password for user " + userId); + // continue the flow anyway + } + } + } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) { + result.gkResponse = new VerifyCredentialResponse(response.getTimeout()); + return result; + } else { + result.gkResponse = VerifyCredentialResponse.ERROR; + return result; + } + + + byte[] applicationId = transformUnderSecdiscardable(pwdToken, + loadSecdiscardable(handle, userId)); + result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED, + applicationId, userId); + + // Perform verifyChallenge to refresh auth tokens for GK if user password exists. + result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId); + return result; + } + + private AuthenticationToken unwrapSyntheticPasswordBlob(long handle, byte type, + byte[] applicationId, int userId) { + byte[] blob = loadState(SP_BLOB_NAME, handle, userId); + if (blob == null) { + return null; + } + if (blob[0] != SYNTHETIC_PASSWORD_VERSION) { + throw new RuntimeException("Unknown blob version"); + } + if (blob[1] != type) { + throw new RuntimeException("Invalid blob type"); + } + byte[] secret = decryptSPBlob(getHandleName(handle), + Arrays.copyOfRange(blob, 2, blob.length), applicationId); + if (secret == null) { + Log.e(TAG, "Fail to decrypt SP for user " + userId); + return null; + } + AuthenticationToken result = new AuthenticationToken(); + result.syntheticPassword = new String(secret); + return result; + } + + /** + * performs GK verifyChallenge and returns auth token, re-enrolling SP password handle + * if required. + * + * Normally performing verifyChallenge with an AuthenticationToken should always return + * RESPONSE_OK, since user authentication failures are detected earlier when trying to + * decrypt SP. + */ + public VerifyCredentialResponse verifyChallenge(IGateKeeperService gatekeeper, + @NonNull AuthenticationToken auth, long challenge, int userId) throws RemoteException { + byte[] spHandle = loadSyntheticPasswordHandle(userId); + if (spHandle == null) { + // There is no password handle associated with the given user, i.e. the user is not + // secured by lockscreen and has no SID, so just return here; + return null; + } + VerifyCredentialResponse result; + GateKeeperResponse response = gatekeeper.verifyChallenge(userId, challenge, + spHandle, auth.deriveGkPassword()); + int responseCode = response.getResponseCode(); + if (responseCode == GateKeeperResponse.RESPONSE_OK) { + result = new VerifyCredentialResponse(response.getPayload()); + if (response.getShouldReEnroll()) { + response = gatekeeper.enroll(userId, spHandle, + spHandle, auth.deriveGkPassword()); + if (response.getResponseCode() == GateKeeperResponse.RESPONSE_OK) { + spHandle = response.getPayload(); + saveSyntheticPasswordHandle(spHandle, userId); + // Call self again to re-verify with updated handle + return verifyChallenge(gatekeeper, auth, challenge, userId); + } else { + Log.w(TAG, "Fail to re-enroll SP handle for user " + userId); + // Fall through, return existing handle + } + } + } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) { + result = new VerifyCredentialResponse(response.getTimeout()); + } else { + result = VerifyCredentialResponse.ERROR; + } + return result; + } + + public boolean existsHandle(long handle, int userId) { + return hasState(SP_BLOB_NAME, handle, userId); + } + + public void destroyPasswordBasedSyntheticPassword(long handle, int userId) { + destroySyntheticPassword(handle, userId); + destroyState(SECDISCARDABLE_NAME, true, handle, userId); + destroyState(PASSWORD_DATA_NAME, true, handle, userId); + } + + private void destroySyntheticPassword(long handle, int userId) { + destroyState(SP_BLOB_NAME, true, handle, userId); + destroyState(SP_E0_NAME, true, handle, userId); + destroyState(SP_P1_NAME, true, handle, userId); + destroySPBlobKey(getHandleName(handle)); + } + + private byte[] transformUnderSecdiscardable(byte[] data, byte[] rawSecdiscardable) { + byte[] secdiscardable = SyntheticPasswordCrypto.personalisedHash( + PERSONALISATION_SECDISCARDABLE, rawSecdiscardable); + byte[] result = new byte[data.length + secdiscardable.length]; + System.arraycopy(data, 0, result, 0, data.length); + System.arraycopy(secdiscardable, 0, result, data.length, secdiscardable.length); + return result; + } + + private byte[] createSecdiscardable(long handle, int userId) { + byte[] data = secureRandom(SECDISCARDABLE_LENGTH); + saveState(SECDISCARDABLE_NAME, data, handle, userId); + return data; + } + + private byte[] loadSecdiscardable(long handle, int userId) { + return loadState(SECDISCARDABLE_NAME, handle, userId); + } + + private boolean hasState(String stateName, long handle, int userId) { + return !ArrayUtils.isEmpty(loadState(stateName, handle, userId)); + } + + private byte[] loadState(String stateName, long handle, int userId) { + return mStorage.readSyntheticPasswordState(userId, handle, stateName); + } + + private void saveState(String stateName, byte[] data, long handle, int userId) { + mStorage.writeSyntheticPasswordState(userId, handle, stateName, data); + } + + private void destroyState(String stateName, boolean secure, long handle, int userId) { + mStorage.deleteSyntheticPasswordState(userId, handle, stateName, secure); + } + + protected byte[] decryptSPBlob(String blobKeyName, byte[] blob, byte[] applicationId) { + return SyntheticPasswordCrypto.decryptBlob(blobKeyName, blob, applicationId); + } + + protected byte[] createSPBlob(String blobKeyName, byte[] data, byte[] applicationId, long sid) { + return SyntheticPasswordCrypto.createBlob(blobKeyName, data, applicationId, sid); + } + + protected void destroySPBlobKey(String keyAlias) { + SyntheticPasswordCrypto.destroyBlobKey(keyAlias); + } + + public static long generateHandle() { + SecureRandom rng = new SecureRandom(); + long result; + do { + result = rng.nextLong(); + } while (result == DEFAULT_HANDLE); + return result; + } + + private int fakeUid(int uid) { + return 100000 + uid; + } + + protected static byte[] secureRandom(int length) { + try { + return SecureRandom.getInstance("SHA1PRNG").generateSeed(length); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + + private String getHandleName(long handle) { + return String.format("%s%x", LockPatternUtils.SYNTHETIC_PASSWORD_KEY_PREFIX, handle); + } + + private byte[] computePasswordToken(String password, PasswordData data) { + return scrypt(password, data.salt, 1 << data.scryptN, 1 << data.scryptR, 1 << data.scryptP, + PASSWORD_TOKEN_LENGTH); + } + + private byte[] passwordTokenToGkInput(byte[] token) { + return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_USER_GK_AUTH, token); + } + + protected long sidFromPasswordHandle(byte[] handle) { + return nativeSidFromPasswordHandle(handle); + } + + protected byte[] scrypt(String password, byte[] salt, int N, int r, int p, int outLen) { + return nativeScrypt(password.getBytes(), salt, N, r, p, outLen); + } + + native long nativeSidFromPasswordHandle(byte[] handle); + native byte[] nativeScrypt(byte[] password, byte[] salt, int N, int r, int p, int outLen); + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + public static String bytesToHex(byte[] bytes) { + if (bytes == null) { + return "null"; + } + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk index eab5d8a48ba32..2c3cda55c9fe8 100644 --- a/services/core/jni/Android.mk +++ b/services/core/jni/Android.mk @@ -19,6 +19,7 @@ LOCAL_SRC_FILES += \ $(LOCAL_REL_DIR)/com_android_server_location_GnssLocationProvider.cpp \ $(LOCAL_REL_DIR)/com_android_server_power_PowerManagerService.cpp \ $(LOCAL_REL_DIR)/com_android_server_SerialService.cpp \ + $(LOCAL_REL_DIR)/com_android_server_SyntheticPasswordManager.cpp \ $(LOCAL_REL_DIR)/com_android_server_storage_AppFuseBridge.cpp \ $(LOCAL_REL_DIR)/com_android_server_SystemServer.cpp \ $(LOCAL_REL_DIR)/com_android_server_tv_TvUinputBridge.cpp \ @@ -33,12 +34,14 @@ LOCAL_SRC_FILES += \ LOCAL_C_INCLUDES += \ $(JNI_H_INCLUDE) \ + external/scrypt/lib/crypto \ frameworks/base/services \ frameworks/base/libs \ frameworks/base/libs/hwui \ frameworks/base/core/jni \ frameworks/native/services \ system/core/libappfuse/include \ + system/gatekeeper/include \ system/security/keystore/include \ $(call include-path-for, libhardware)/hardware \ $(call include-path-for, libhardware_legacy)/hardware_legacy \ @@ -50,6 +53,7 @@ LOCAL_SHARED_LIBRARIES += \ libappfuse \ libbinder \ libcutils \ + libcrypto \ liblog \ libhardware \ libhardware_legacy \ @@ -83,3 +87,5 @@ LOCAL_SHARED_LIBRARIES += \ android.hardware.tv.input@1.0 \ android.hardware.vibrator@1.0 \ android.hardware.vr@1.0 \ + +LOCAL_STATIC_LIBRARIES += libscrypt_static \ No newline at end of file diff --git a/services/core/jni/com_android_server_SyntheticPasswordManager.cpp b/services/core/jni/com_android_server_SyntheticPasswordManager.cpp new file mode 100644 index 0000000000000..a9f7b9fa4e6cc --- /dev/null +++ b/services/core/jni/com_android_server_SyntheticPasswordManager.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 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. + */ + +#define LOG_TAG "SyntheticPasswordManager" + +#include "JNIHelp.h" +#include "jni.h" + +#include +#include +#include +#include +#include +#include + + +extern "C" { +#include "crypto_scrypt.h" +} + +namespace android { + +static jlong android_server_SyntheticPasswordManager_nativeSidFromPasswordHandle(JNIEnv* env, jobject, jbyteArray handleArray) { + + jbyte* data = (jbyte*)env->GetPrimitiveArrayCritical(handleArray, NULL); + + if (data != NULL) { + const gatekeeper::password_handle_t *handle = + reinterpret_cast(data); + jlong sid = handle->user_id; + env->ReleasePrimitiveArrayCritical(handleArray, data, JNI_ABORT); + return sid; + } else { + return 0; + } +} + +static jbyteArray android_server_SyntheticPasswordManager_nativeScrypt(JNIEnv* env, jobject, jbyteArray password, jbyteArray salt, jint N, jint r, jint p, jint outLen) { + if (!password || !salt) { + return NULL; + } + + int passwordLen = env->GetArrayLength(password); + int saltLen = env->GetArrayLength(salt); + jbyteArray ret = env->NewByteArray(outLen); + + jbyte* passwordPtr = (jbyte*)env->GetByteArrayElements(password, NULL); + jbyte* saltPtr = (jbyte*)env->GetByteArrayElements(salt, NULL); + jbyte* retPtr = (jbyte*)env->GetByteArrayElements(ret, NULL); + + int rc = crypto_scrypt((const uint8_t *)passwordPtr, passwordLen, + (const uint8_t *)saltPtr, saltLen, N, r, p, (uint8_t *)retPtr, + outLen); + env->ReleaseByteArrayElements(password, passwordPtr, JNI_ABORT); + env->ReleaseByteArrayElements(salt, saltPtr, JNI_ABORT); + env->ReleaseByteArrayElements(ret, retPtr, 0); + + if (!rc) { + return ret; + } else { + SLOGE("scrypt failed"); + return NULL; + } +} + +static const JNINativeMethod sMethods[] = { + /* name, signature, funcPtr */ + {"nativeSidFromPasswordHandle", "([B)J", (void*)android_server_SyntheticPasswordManager_nativeSidFromPasswordHandle}, + {"nativeScrypt", "([B[BIIII)[B", (void*)android_server_SyntheticPasswordManager_nativeScrypt}, +}; + +int register_android_server_SyntheticPasswordManager(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/server/SyntheticPasswordManager", + sMethods, NELEM(sMethods)); +} + +} /* namespace android */ diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 6f505d51b15ce..899640e138e34 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -45,6 +45,7 @@ int register_android_server_tv_TvInputHal(JNIEnv* env); int register_android_server_PersistentDataBlockService(JNIEnv* env); int register_android_server_Watchdog(JNIEnv* env); int register_android_server_HardwarePropertiesManagerService(JNIEnv* env); +int register_android_server_SyntheticPasswordManager(JNIEnv* env); }; using namespace android; @@ -85,6 +86,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_Watchdog(env); register_android_server_HardwarePropertiesManagerService(env); register_android_server_storage_AppFuse(env); + register_android_server_SyntheticPasswordManager(env); return JNI_VERSION_1_4; } diff --git a/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java index c89d35c1d84ef..c6265bc768f3b 100644 --- a/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/BaseLockSettingsServiceTests.java @@ -134,5 +134,13 @@ public class BaseLockSettingsServiceTests extends AndroidTestCase { File storageDir = mStorage.mStorageDir; assertTrue(FileUtils.deleteContents(storageDir)); } + + protected static void assertArrayEquals(byte[] expected, byte[] actual) { + assertTrue(Arrays.equals(expected, actual)); + } + + protected static void assertArrayNotSame(byte[] expected, byte[] actual) { + assertFalse(Arrays.equals(expected, actual)); + } } diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java index 613ec0be09199..cfdb5b1fd4c1f 100644 --- a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTestable.java @@ -21,9 +21,11 @@ import static org.mockito.Mockito.mock; import android.app.IActivityManager; import android.content.Context; import android.os.Handler; +import android.os.Process; +import android.os.RemoteException; import android.os.storage.IStorageManager; import android.security.KeyStore; -import android.service.gatekeeper.IGateKeeperService; +import android.security.keystore.KeyPermanentlyInvalidatedException; import com.android.internal.widget.LockPatternUtils; @@ -38,16 +40,18 @@ public class LockSettingsServiceTestable extends LockSettingsService { private IActivityManager mActivityManager; private LockPatternUtils mLockPatternUtils; private IStorageManager mStorageManager; + private MockGateKeeperService mGatekeeper; public MockInjector(Context context, LockSettingsStorage storage, KeyStore keyStore, IActivityManager activityManager, LockPatternUtils lockPatternUtils, - IStorageManager storageManager) { + IStorageManager storageManager, MockGateKeeperService gatekeeper) { super(context); mLockSettingsStorage = storage; mKeyStore = keyStore; mActivityManager = activityManager; mLockPatternUtils = lockPatternUtils; mStorageManager = storageManager; + mGatekeeper = gatekeeper; } @Override @@ -89,13 +93,25 @@ public class LockSettingsServiceTestable extends LockSettingsService { public IStorageManager getStorageManager() { return mStorageManager; } + + @Override + public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) { + return new MockSyntheticPasswordManager(storage, mGatekeeper); + } + + @Override + public int binderGetCallingUid() { + return Process.SYSTEM_UID; + } + + } protected LockSettingsServiceTestable(Context context, LockPatternUtils lockPatternUtils, - LockSettingsStorage storage, IGateKeeperService gatekeeper, KeyStore keystore, + LockSettingsStorage storage, MockGateKeeperService gatekeeper, KeyStore keystore, IStorageManager storageManager, IActivityManager mActivityManager) { super(new MockInjector(context, storage, keystore, mActivityManager, lockPatternUtils, - storageManager)); + storageManager, gatekeeper)); mGateKeeperService = gatekeeper; } @@ -105,12 +121,18 @@ public class LockSettingsServiceTestable extends LockSettingsService { } @Override - protected String getDecryptedPasswordForTiedProfile(int userId) throws FileNotFoundException { + protected String getDecryptedPasswordForTiedProfile(int userId) throws FileNotFoundException, KeyPermanentlyInvalidatedException { byte[] storedData = mStorage.readChildProfileLock(userId); if (storedData == null) { throw new FileNotFoundException("Child profile lock file not found"); } + try { + if (mGateKeeperService.getSecureUserId(userId) == 0) { + throw new KeyPermanentlyInvalidatedException(); + } + } catch (RemoteException e) { + // shouldn't happen. + } return new String(storedData); } - } diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java index 4c2e17172e766..ae9762a814825 100644 --- a/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/LockSettingsServiceTests.java @@ -123,6 +123,12 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { UnifiedPassword, PRIMARY_USER_ID); mStorageManager.setIgnoreBadUnlock(false); assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); + + //Clear unified challenge + mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, UnifiedPassword, + PRIMARY_USER_ID); + assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + assertEquals(0, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); } public void testManagedProfileSeparateChallenge() throws RemoteException { diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java index e81b02f071a81..18da1a560c74a 100644 --- a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java +++ b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTestable.java @@ -31,19 +31,36 @@ public class LockSettingsStorageTestable extends LockSettingsStorage { @Override String getLockPatternFilename(int userId) { - return new File(mStorageDir, - super.getLockPatternFilename(userId).replace('/', '-')).getAbsolutePath(); + return makeDirs(mStorageDir, + super.getLockPatternFilename(userId)).getAbsolutePath(); } @Override String getLockPasswordFilename(int userId) { - return new File(mStorageDir, - super.getLockPasswordFilename(userId).replace('/', '-')).getAbsolutePath(); + return makeDirs(mStorageDir, + super.getLockPasswordFilename(userId)).getAbsolutePath(); } @Override String getChildProfileLockFile(int userId) { - return new File(mStorageDir, - super.getChildProfileLockFile(userId).replace('/', '-')).getAbsolutePath(); + return makeDirs(mStorageDir, + super.getChildProfileLockFile(userId)).getAbsolutePath(); + } + + @Override + protected File getSyntheticPasswordDirectoryForUser(int userId) { + return makeDirs(mStorageDir, super.getSyntheticPasswordDirectoryForUser( + userId).getAbsolutePath()); + } + + private File makeDirs(File baseDir, String filePath) { + File path = new File(filePath); + if (path.getParent() == null) { + return new File(baseDir, filePath); + } else { + File mappedDir = new File(baseDir, path.getParent()); + mappedDir.mkdirs(); + return new File(mappedDir, path.getName()); + } } } diff --git a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java index d110feaab3c95..c68fbdc0a2ac8 100644 --- a/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java +++ b/services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java @@ -329,6 +329,16 @@ public class LockSettingsStorageTests extends AndroidTestCase { assertEquals("/data/system/users/3/gatekeeper.password.key", storage.getLockPasswordFilename(3)); } + public void testSyntheticPasswordState() { + final byte[] data = {1,2,3,4}; + mStorage.writeSyntheticPasswordState(10, 1234L, "state", data); + assertArrayEquals(data, mStorage.readSyntheticPasswordState(10, 1234L, "state")); + assertEquals(null, mStorage.readSyntheticPasswordState(0, 1234L, "state")); + + mStorage.deleteSyntheticPasswordState(10, 1234L, "state", true); + assertEquals(null, mStorage.readSyntheticPasswordState(10, 1234L, "state")); + } + private static void assertArrayEquals(byte[] expected, byte[] actual) { if (!Arrays.equals(expected, actual)) { fail("expected:<" + Arrays.toString(expected) + diff --git a/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java b/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java index 15983cada9c2e..bc933418d9fb5 100644 --- a/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java +++ b/services/tests/servicestests/src/com/android/server/MockGateKeeperService.java @@ -149,6 +149,15 @@ public class MockGateKeeperService implements IGateKeeperService { return authTokenMap.get(uid); } + public AuthToken getAuthTokenForSid(long sid) { + for(AuthToken token : authTokenMap.values()) { + if (token.sid == sid) { + return token; + } + } + return null; + } + public void clearAuthToken(int uid) { authTokenMap.remove(uid); } diff --git a/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java b/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java new file mode 100644 index 0000000000000..93e3fc60a458a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/MockSyntheticPasswordManager.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017 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; + +import android.util.ArrayMap; + +import junit.framework.AssertionFailedError; + +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +public class MockSyntheticPasswordManager extends SyntheticPasswordManager { + + private MockGateKeeperService mGateKeeper; + + public MockSyntheticPasswordManager(LockSettingsStorage storage, + MockGateKeeperService gatekeeper) { + super(storage); + mGateKeeper = gatekeeper; + } + + private ArrayMap mBlobs = new ArrayMap<>(); + + @Override + protected byte[] decryptSPBlob(String blobKeyName, byte[] blob, byte[] applicationId) { + if (mBlobs.containsKey(blobKeyName) && !Arrays.equals(mBlobs.get(blobKeyName), blob)) { + throw new AssertionFailedError("blobKeyName content is overwritten: " + blobKeyName); + } + ByteBuffer buffer = ByteBuffer.allocate(blob.length); + buffer.put(blob, 0, blob.length); + buffer.flip(); + int len; + len = buffer.getInt(); + byte[] data = new byte[len]; + buffer.get(data); + len = buffer.getInt(); + byte[] appId = new byte[len]; + buffer.get(appId); + long sid = buffer.getLong(); + if (!Arrays.equals(appId, applicationId)) { + throw new AssertionFailedError("Invalid application id"); + } + if (sid != 0 && mGateKeeper.getAuthTokenForSid(sid) == null) { + throw new AssertionFailedError("No valid auth token"); + } + return data; + } + + @Override + protected byte[] createSPBlob(String blobKeyName, byte[] data, byte[] applicationId, long sid) { + ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + data.length + Integer.BYTES + + applicationId.length + Long.BYTES); + buffer.putInt(data.length); + buffer.put(data); + buffer.putInt(applicationId.length); + buffer.put(applicationId); + buffer.putLong(sid); + byte[] result = buffer.array(); + mBlobs.put(blobKeyName, result); + return result; + } + + @Override + protected void destroySPBlobKey(String keyAlias) { + } + + @Override + protected long sidFromPasswordHandle(byte[] handle) { + return new MockGateKeeperService.VerifyHandle(handle).sid; + } + + @Override + protected byte[] scrypt(String password, byte[] salt, int N, int r, int p, int outLen) { + try { + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 10, outLen * 8); + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + return f.generateSecret(spec).getEncoded(); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + +} diff --git a/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java new file mode 100644 index 0000000000000..9d9595edc74a4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2017 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; + +import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY; +import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY; + +import android.os.RemoteException; +import android.os.UserHandle; + +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.VerifyCredentialResponse; +import com.android.server.SyntheticPasswordManager.AuthenticationResult; +import com.android.server.SyntheticPasswordManager.AuthenticationToken; + + +/** + * runtest frameworks-services -c com.android.server.SyntheticPasswordTests + */ +public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testPasswordBasedSyntheticPassword() throws RemoteException { + final int USER_ID = 10; + final String PASSWORD = "user-password"; + final String BADPASSWORD = "bad-password"; + MockSyntheticPasswordManager manager = new MockSyntheticPasswordManager(mStorage, mGateKeeperService); + AuthenticationToken authToken = manager.newSyntheticPasswordAndSid(mGateKeeperService, null, + null, USER_ID); + long handle = manager.createPasswordBasedSyntheticPassword(mGateKeeperService, PASSWORD, + LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, authToken, USER_ID); + + AuthenticationResult result = manager.unwrapPasswordBasedSyntheticPassword(mGateKeeperService, handle, PASSWORD, USER_ID); + assertEquals(result.authToken.deriveKeyStorePassword(), authToken.deriveKeyStorePassword()); + + result = manager.unwrapPasswordBasedSyntheticPassword(mGateKeeperService, handle, BADPASSWORD, USER_ID); + assertNull(result.authToken); + } + + private void disableSyntheticPassword(int userId) throws RemoteException { + mService.setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM); + } + + private void enableSyntheticPassword(int userId) throws RemoteException { + mService.setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1, UserHandle.USER_SYSTEM); + } + + private boolean hasSyntheticPassword(int userId) throws RemoteException { + return mService.getLong(SYNTHETIC_PASSWORD_HANDLE_KEY, 0, userId) != 0; + } + + public void testPasswordMigration() throws RemoteException { + final String PASSWORD = "testPasswordMigration-password"; + + disableSyntheticPassword(PRIMARY_USER_ID); + mService.setLockCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID); + long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); + final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID); + enableSyntheticPassword(PRIMARY_USER_ID); + // Performs migration + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + assertTrue(hasSyntheticPassword(PRIMARY_USER_ID)); + + // SP-based verification + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID)); + } + + private void initializeCredentialUnderSP(String password, int userId) throws RemoteException { + enableSyntheticPassword(userId); + mService.setLockCredential(password, password != null ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD : LockPatternUtils.CREDENTIAL_TYPE_NONE, null, userId); + } + + public void testSyntheticPasswordChangeCredential() throws RemoteException { + final String PASSWORD = "testSyntheticPasswordChangeCredential-password"; + final String NEWPASSWORD = "testSyntheticPasswordChangeCredential-newpassword"; + + initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID); + long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); + mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD, PRIMARY_USER_ID); + mGateKeeperService.clearSecureUserId(PRIMARY_USER_ID); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + } + + public void testSyntheticPasswordVerifyCredential() throws RemoteException { + final String PASSWORD = "testSyntheticPasswordVerifyCredential-password"; + final String BADPASSWORD = "testSyntheticPasswordVerifyCredential-badpassword"; + + initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + + assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, + mService.verifyCredential(BADPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + } + + public void testSyntheticPasswordClearCredential() throws RemoteException { + final String PASSWORD = "testSyntheticPasswordClearCredential-password"; + final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword"; + + initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID); + long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); + // clear password + mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD, PRIMARY_USER_ID); + assertEquals(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + + // set a new password + mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertNotSame(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + } + + public void testSyntheticPasswordClearCredentialUntrusted() throws RemoteException { + final String PASSWORD = "testSyntheticPasswordClearCredential-password"; + final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword"; + + initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID); + long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); + // clear password + mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID); + assertEquals(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + + // set a new password + mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertNotSame(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + } + + public void testSyntheticPasswordChangeCredentialUntrusted() throws RemoteException { + final String PASSWORD = "testSyntheticPasswordClearCredential-password"; + final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword"; + + initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID); + long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); + // Untrusted change password + mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID); + assertNotSame(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + assertNotSame(sid ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + + // Verify the password + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + } + + + public void testManagedProfileUnifiedChallengeMigration() throws RemoteException { + final String UnifiedPassword = "testManagedProfileUnifiedChallengeMigration-pwd"; + disableSyntheticPassword(PRIMARY_USER_ID); + disableSyntheticPassword(MANAGED_PROFILE_USER_ID); + mService.setLockCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID); + mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); + final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); + final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID); + final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID); + final byte[] profileStorageKey = mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID); + assertTrue(primarySid != 0); + assertTrue(profileSid != 0); + assertTrue(profileSid != primarySid); + + // do migration + enableSyntheticPassword(PRIMARY_USER_ID); + enableSyntheticPassword(MANAGED_PROFILE_USER_ID); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + + // verify + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertEquals(primarySid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); + assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID)); + assertArrayNotSame(profileStorageKey, mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID)); + assertTrue(hasSyntheticPassword(PRIMARY_USER_ID)); + assertTrue(hasSyntheticPassword(MANAGED_PROFILE_USER_ID)); + } + + public void testManagedProfileSeparateChallengeMigration() throws RemoteException { + final String primaryPassword = "testManagedProfileSeparateChallengeMigration-primary"; + final String profilePassword = "testManagedProfileSeparateChallengeMigration-profile"; + mService.setLockCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID); + mService.setLockCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, MANAGED_PROFILE_USER_ID); + final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); + final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID); + final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID); + final byte[] profileStorageKey = mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID); + assertTrue(primarySid != 0); + assertTrue(profileSid != 0); + assertTrue(profileSid != primarySid); + + // do migration + enableSyntheticPassword(PRIMARY_USER_ID); + enableSyntheticPassword(MANAGED_PROFILE_USER_ID); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, MANAGED_PROFILE_USER_ID).getResponseCode()); + + // verify + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, MANAGED_PROFILE_USER_ID).getResponseCode()); + assertEquals(primarySid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); + assertArrayNotSame(primaryStorageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID)); + assertArrayNotSame(profileStorageKey, mStorageManager.getUserUnlockToken(MANAGED_PROFILE_USER_ID)); + assertTrue(hasSyntheticPassword(PRIMARY_USER_ID)); + assertTrue(hasSyntheticPassword(MANAGED_PROFILE_USER_ID)); + } + // b/34600579 + //TODO: add non-migration work profile case, and unify/un-unify transition. + //TODO: test token after user resets password + //TODO: test token based reset after unified work challenge + //TODO: test clear password after unified work challenge +} +