diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl index b380b13402327..b8c062e176455 100644 --- a/core/java/com/android/internal/widget/ILockSettings.aidl +++ b/core/java/com/android/internal/widget/ILockSettings.aidl @@ -45,4 +45,10 @@ interface ILockSettings { void systemReady(); void userPresent(int userId); int getStrongAuthForUser(int userId); + + long addEscrowToken(in byte[] token, int userId); + boolean removeEscrowToken(long handle, int userId); + boolean isEscrowTokenActive(long handle, int userId); + boolean setLockCredentialWithToken(String credential, int type, long tokenHandle, in byte[] token, int userId); + void unlockUserWithToken(long tokenHandle, in byte[] token, int userId); } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 4bf58b9d95c1f..0aba9c28d75f0 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -773,7 +773,7 @@ public class LockPatternUtils { getLockSettings().setLockCredential(password, CREDENTIAL_TYPE_PASSWORD, savedPassword, userHandle); - addEncryptionPassword(password, computedQuality, userHandle); + updateEncryptionPasswordIfNeeded(password, computedQuality, userHandle); updatePasswordHistory(password, userHandle); } catch (RemoteException re) { // Cant do much @@ -781,7 +781,11 @@ public class LockPatternUtils { } } - private void addEncryptionPassword(String password, int quality, int userHandle) { + /** + * Update device encryption password if calling user is USER_SYSTEM and device supports + * encryption. + */ + private void updateEncryptionPasswordIfNeeded(String password, int quality, int userHandle) { // Update the device encryption password. if (userHandle == UserHandle.USER_SYSTEM && LockPatternUtils.isDeviceEncryptionEnabled()) { @@ -1401,6 +1405,104 @@ public class LockPatternUtils { } } + /** + * Create an escrow token for the current user, which can later be used to unlock FBE + * or change user password. + * + * After adding, if the user currently has lockscreen password, he will need to perform a + * confirm credential operation in order to activate the token for future use. If the user + * has no secure lockscreen, then the token is activated immediately. + * + * @return a unique 64-bit token handle which is needed to refer to this token later. + */ + public long addEscrowToken(byte[] token, int userId) { + try { + return getLockSettings().addEscrowToken(token, userId); + } catch (RemoteException re) { + return 0L; + } + } + + /** + * Remove an escrow token. + * @return true if the given handle refers to a valid token previously returned from + * {@link #addEscrowToken}, whether it's active or not. return false otherwise. + */ + public boolean removeEscrowToken(long handle, int userId) { + try { + return getLockSettings().removeEscrowToken(handle, userId); + } catch (RemoteException re) { + return false; + } + } + + /** + * Check if the given escrow token is active or not. Only active token can be used to call + * {@link #setLockCredentialWithToken} and {@link #unlockUserWithToken} + */ + public boolean isEscrowTokenActive(long handle, int userId) { + try { + return getLockSettings().isEscrowTokenActive(handle, userId); + } catch (RemoteException re) { + return false; + } + } + + public boolean setLockCredentialWithToken(String credential, int type, long tokenHandle, + byte[] token, int userId) { + try { + if (type != CREDENTIAL_TYPE_NONE) { + if (TextUtils.isEmpty(credential) || credential.length() < MIN_LOCK_PASSWORD_SIZE) { + throw new IllegalArgumentException("password must not be null and at least " + + "of length " + MIN_LOCK_PASSWORD_SIZE); + } + + final int computedQuality = PasswordMetrics.computeForPassword(credential).quality; + if (!getLockSettings().setLockCredentialWithToken(credential, type, tokenHandle, + token, userId)) { + return false; + } + setLong(PASSWORD_TYPE_KEY, Math.max(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC, + computedQuality), userId); + + updateEncryptionPasswordIfNeeded(credential, computedQuality, userId); + updatePasswordHistory(credential, userId); + } else { + if (!TextUtils.isEmpty(credential)) { + throw new IllegalArgumentException("password must be emtpy for NONE type"); + } + if (!getLockSettings().setLockCredentialWithToken(null, CREDENTIAL_TYPE_NONE, + tokenHandle, token, userId)) { + return false; + } + setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, + userId); + + if (userId == UserHandle.USER_SYSTEM) { + // Set the encryption password to default. + updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null); + setCredentialRequiredToDecrypt(false); + } + } + onAfterChangingPassword(userId); + return true; + } catch (RemoteException re) { + Log.e(TAG, "Unable to save lock password ", re); + re.rethrowFromSystemServer(); + } + return false; + } + + public void unlockUserWithToken(long tokenHandle, byte[] token, int userId) { + try { + getLockSettings().unlockUserWithToken(tokenHandle, token, userId); + } catch (RemoteException re) { + Log.e(TAG, "Unable to unlock user with token", re); + re.rethrowFromSystemServer(); + } + } + + /** * Callback to be notified about progress when checking credentials. */ diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java index c10bcf0de7170..4a4453046295b 100644 --- a/services/core/java/com/android/server/LockSettingsService.java +++ b/services/core/java/com/android/server/LockSettingsService.java @@ -1926,6 +1926,7 @@ public class LockSettingsService extends ILockSettings.Stub { (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE); trustManager.setDeviceLockedForUser(userId, false); } + activateEscrowTokens(authResult.authToken, userId); } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) { if (response.getTimeout() > 0) { requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId); @@ -2024,6 +2025,132 @@ public class LockSettingsService extends ILockSettings.Stub { mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId); } notifyActivePasswordMetricsAvailable(credential, userId); + + } + + @Override + public long addEscrowToken(byte[] token, int userId) throws RemoteException { + ensureCallerSystemUid(); + if (DEBUG) Slog.d(TAG, "addEscrowToken: user=" + userId); + synchronized (mSpManager) { + enableSyntheticPasswordLocked(); + // Migrate to synthetic password based credentials if ther user has no password, + // the token can then be activated immediately. + AuthenticationToken auth = null; + if (!isUserSecure(userId)) { + if (shouldMigrateToSyntheticPasswordLocked(userId)) { + auth = initializeSyntheticPasswordLocked(null, null, + LockPatternUtils.CREDENTIAL_TYPE_NONE, userId); + } else /* isSyntheticPasswordBasedCredentialLocked(userId) */ { + long pwdHandle = getSyntheticPasswordHandleLocked(userId); + auth = mSpManager.unwrapPasswordBasedSyntheticPassword(getGateKeeperService(), + pwdHandle, null, userId).authToken; + } + } + disableEscrowTokenOnNonManagedDevicesIfNeeded(userId); + if (!mSpManager.hasEscrowData(userId)) { + throw new SecurityException("Escrow token is disabled on the current user"); + } + long handle = mSpManager.createTokenBasedSyntheticPassword(token, userId); + if (auth != null) { + mSpManager.activateTokenBasedSyntheticPassword(handle, auth, userId); + } + return handle; + } + } + + private void activateEscrowTokens(AuthenticationToken auth, int userId) throws RemoteException { + if (DEBUG) Slog.d(TAG, "activateEscrowTokens: user=" + userId); + synchronized (mSpManager) { + for (long handle : mSpManager.getPendingTokensForUser(userId)) { + Slog.i(TAG, String.format("activateEscrowTokens: %x %d ", handle, userId)); + mSpManager.activateTokenBasedSyntheticPassword(handle, auth, userId); + } + } + } + + @Override + public boolean isEscrowTokenActive(long handle, int userId) throws RemoteException { + ensureCallerSystemUid(); + synchronized (mSpManager) { + return mSpManager.existsHandle(handle, userId); + } + } + + @Override + public boolean removeEscrowToken(long handle, int userId) throws RemoteException { + ensureCallerSystemUid(); + synchronized (mSpManager) { + if (handle == getSyntheticPasswordHandleLocked(userId)) { + Slog.w(TAG, "Cannot remove password handle"); + return false; + } + if (mSpManager.removePendingToken(handle, userId)) { + return true; + } + if (mSpManager.existsHandle(handle, userId)) { + mSpManager.destroyTokenBasedSyntheticPassword(handle, userId); + return true; + } else { + return false; + } + } + } + + @Override + public boolean setLockCredentialWithToken(String credential, int type, long tokenHandle, + byte[] token, int userId) throws RemoteException { + ensureCallerSystemUid(); + boolean result; + synchronized (mSpManager) { + if (!mSpManager.hasEscrowData(userId)) { + throw new SecurityException("Escrow token is disabled on the current user"); + } + result = setLockCredentialWithTokenInternal(credential, type, tokenHandle, token, + userId); + } + if (result) { + synchronized (mSeparateChallengeLock) { + setSeparateProfileChallengeEnabled(userId, true, null); + } + notifyPasswordChanged(userId); + } + return result; + } + + private boolean setLockCredentialWithTokenInternal(String credential, int type, + long tokenHandle, byte[] token, int userId) throws RemoteException { + synchronized (mSpManager) { + AuthenticationResult result = mSpManager.unwrapTokenBasedSyntheticPassword( + getGateKeeperService(), tokenHandle, token, userId); + if (result.authToken == null) { + Slog.w(TAG, "Invalid escrow token supplied"); + return false; + } + long oldHandle = getSyntheticPasswordHandleLocked(userId); + setLockCredentialWithAuthTokenLocked(credential, type, result.authToken, userId); + mSpManager.destroyPasswordBasedSyntheticPassword(oldHandle, userId); + return true; + } + } + + @Override + public void unlockUserWithToken(long tokenHandle, byte[] token, int userId) + throws RemoteException { + ensureCallerSystemUid(); + AuthenticationResult authResult; + synchronized (mSpManager) { + if (!mSpManager.hasEscrowData(userId)) { + throw new SecurityException("Escrow token is disabled on the current user"); + } + authResult = mSpManager.unwrapTokenBasedSyntheticPassword(getGateKeeperService(), + tokenHandle, token, userId); + if (authResult.authToken == null) { + Slog.w(TAG, "Invalid escrow token supplied"); + return; + } + } + unlockUser(userId, null, authResult.authToken.deriveDiskEncryptionKey()); } @Override @@ -2058,4 +2185,41 @@ public class LockSettingsService extends ILockSettings.Stub { } } + private void disableEscrowTokenOnNonManagedDevicesIfNeeded(int userId) { + long ident = Binder.clearCallingIdentity(); + try { + // Managed profile should have escrow enabled + if (mUserManager.getUserInfo(userId).isManagedProfile()) { + return; + } + DevicePolicyManager dpm = (DevicePolicyManager) + mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + // Devices with Device Owner should have escrow enabled on all users. + if (dpm.getDeviceOwnerComponentOnAnyUser() != null) { + return; + } + // If the device is yet to be provisioned (still in SUW), there is still + // a chance that Device Owner will be set on the device later, so postpone + // disabling escrow token for now. + if (!dpm.isDeviceProvisioned()) { + return; + } + // Disable escrow token permanently on all other device/user types. + Slog.i(TAG, "Disabling escrow token on user " + userId); + if (isSyntheticPasswordBasedCredentialLocked(userId)) { + mSpManager.destroyEscrowData(userId); + } + } catch (RemoteException e) { + Slog.e(TAG, "disableEscrowTokenOnNonManagedDevices", e); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private void ensureCallerSystemUid() throws SecurityException { + final int callingUid = mInjector.binderGetCallingUid(); + if (callingUid != Process.SYSTEM_UID) { + throw new SecurityException("Only system can call this API."); + } + } } diff --git a/services/core/java/com/android/server/SyntheticPasswordManager.java b/services/core/java/com/android/server/SyntheticPasswordManager.java index 0449d2007b0d4..62678801a9a2b 100644 --- a/services/core/java/com/android/server/SyntheticPasswordManager.java +++ b/services/core/java/com/android/server/SyntheticPasswordManager.java @@ -63,6 +63,7 @@ public class SyntheticPasswordManager { private static final byte SYNTHETIC_PASSWORD_VERSION = 1; private static final byte SYNTHETIC_PASSWORD_PASSWORD_BASED = 0; + private static final byte SYNTHETIC_PASSWORD_TOKEN_BASED = 1; // 256-bit synthetic password private static final byte SYNTHETIC_PASSWORD_LENGTH = 256 / 8; @@ -113,7 +114,7 @@ public class SyntheticPasswordManager { syntheticPassword.getBytes()); } - public void initialize(byte[] P0, byte[] P1) { + private void initialize(byte[] P0, byte[] P1) { this.P1 = P1; this.syntheticPassword = String.valueOf(HexEncoding.encode( SyntheticPasswordCrypto.personalisedHash( @@ -122,6 +123,10 @@ public class SyntheticPasswordManager { PERSONALIZATION_E0, P0); } + public void recreate(byte[] secret) { + initialize(secret, this.P1); + } + protected static AuthenticationToken create() { AuthenticationToken result = new AuthenticationToken(); result.initialize(secureRandom(SYNTHETIC_PASSWORD_LENGTH), @@ -293,6 +298,11 @@ public class SyntheticPasswordManager { saveState(SP_P1_NAME, authToken.P1, DEFAULT_HANDLE, userId); } + public boolean hasEscrowData(int userId) { + return hasState(SP_E0_NAME, DEFAULT_HANDLE, userId) + && hasState(SP_P1_NAME, DEFAULT_HANDLE, userId); + } + public void destroyEscrowData(int userId) { destroyState(SP_E0_NAME, true, DEFAULT_HANDLE, userId); destroyState(SP_P1_NAME, true, DEFAULT_HANDLE, userId); @@ -339,9 +349,60 @@ public class SyntheticPasswordManager { return handle; } + private ArrayMap> tokenMap = new ArrayMap<>(); + + public long createTokenBasedSyntheticPassword(byte[] token, int userId) { + long handle = generateHandle(); + byte[] applicationId = transformUnderSecdiscardable(token, + createSecdiscardable(handle, userId)); + if (!tokenMap.containsKey(userId)) { + tokenMap.put(userId, new ArrayMap<>()); + } + tokenMap.get(userId).put(handle, applicationId); + return handle; + } + + public Set getPendingTokensForUser(int userId) { + if (!tokenMap.containsKey(userId)) { + return Collections.emptySet(); + } + return tokenMap.get(userId).keySet(); + } + + public boolean removePendingToken(long handle, int userId) { + if (!tokenMap.containsKey(userId)) { + return false; + } + return tokenMap.get(userId).remove(handle) != null; + } + + public boolean activateTokenBasedSyntheticPassword(long handle, AuthenticationToken authToken, + int userId) { + if (!tokenMap.containsKey(userId)) { + return false; + } + byte[] applicationId = tokenMap.get(userId).get(handle); + if (applicationId == null) { + return false; + } + if (!loadEscrowData(authToken, userId)) { + Log.w(TAG, "User is not escrowable"); + return false; + } + createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED, authToken, + applicationId, 0L, userId); + tokenMap.get(userId).remove(handle); + return true; + } + private void createSyntheticPasswordBlob(long handle, byte type, AuthenticationToken authToken, byte[] applicationId, long sid, int userId) { - final byte[] secret = authToken.syntheticPassword.getBytes(); + final byte[] secret; + if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) { + secret = authToken.computeP0(); + } else { + 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; @@ -400,6 +461,32 @@ public class SyntheticPasswordManager { return result; } + /** + * Decrypt a synthetic password by supplying an escrow token and corresponding token + * blob handle generated previously. If the decryption is successful, initiate a GateKeeper + * verification to referesh the SID & Auth token maintained by the system. + */ + public @NonNull AuthenticationResult unwrapTokenBasedSyntheticPassword( + IGateKeeperService gatekeeper, long handle, byte[] token, int userId) + throws RemoteException { + AuthenticationResult result = new AuthenticationResult(); + byte[] applicationId = transformUnderSecdiscardable(token, + loadSecdiscardable(handle, userId)); + result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED, + applicationId, userId); + if (result.authToken != null) { + result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId); + if (result.gkResponse == null) { + // The user currently has no password. return OK with null payload so null + // is propagated to unlockUser() + result.gkResponse = VerifyCredentialResponse.OK; + } + } else { + result.gkResponse = VerifyCredentialResponse.ERROR; + } + return result; + } + private AuthenticationToken unwrapSyntheticPasswordBlob(long handle, byte type, byte[] applicationId, int userId) { byte[] blob = loadState(SP_BLOB_NAME, handle, userId); @@ -419,7 +506,15 @@ public class SyntheticPasswordManager { return null; } AuthenticationToken result = new AuthenticationToken(); - result.syntheticPassword = new String(secret); + if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) { + if (!loadEscrowData(result, userId)) { + Log.e(TAG, "User is not escrowable: " + userId); + return null; + } + result.recreate(secret); + } else { + result.syntheticPassword = new String(secret); + } return result; } @@ -470,6 +565,11 @@ public class SyntheticPasswordManager { return hasState(SP_BLOB_NAME, handle, userId); } + public void destroyTokenBasedSyntheticPassword(long handle, int userId) { + destroySyntheticPassword(handle, userId); + destroyState(SECDISCARDABLE_NAME, true, handle, userId); + } + public void destroyPasswordBasedSyntheticPassword(long handle, int userId) { destroySyntheticPassword(handle, userId); destroyState(SECDISCARDABLE_NAME, true, handle, userId); diff --git a/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java index 9d9595edc74a4..6e5ade12ce2c7 100644 --- a/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java @@ -237,6 +237,89 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { assertTrue(hasSyntheticPassword(PRIMARY_USER_ID)); assertTrue(hasSyntheticPassword(MANAGED_PROFILE_USER_ID)); } + + public void testTokenBasedResetPassword() throws RemoteException { + final String PASSWORD = "password"; + final String PATTERN = "123654"; + final String TOKEN = "some-high-entropy-secure-token"; + initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID); + final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID); + + long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID); + assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); + + mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode(); + assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); + + mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, handle, TOKEN.getBytes(), PRIMARY_USER_ID); + + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 0, PRIMARY_USER_ID).getResponseCode()); + assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID)); + } + + public void testTokenBasedClearPassword() throws RemoteException { + final String PASSWORD = "password"; + final String PATTERN = "123654"; + final String TOKEN = "some-high-entropy-secure-token"; + initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID); + final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID); + + long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID); + assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); + + mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode(); + assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); + + mService.setLockCredentialWithToken(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID); + mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID); + + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 0, PRIMARY_USER_ID).getResponseCode()); + assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID)); + } + + public void testTokenBasedResetPasswordAfterCredentialChanges() throws RemoteException { + final String PASSWORD = "password"; + final String PATTERN = "123654"; + final String NEWPASSWORD = "password"; + final String TOKEN = "some-high-entropy-secure-token"; + initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID); + final byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID); + + long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID); + assertFalse(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); + + mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode(); + assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); + + mService.setLockCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, PASSWORD, PRIMARY_USER_ID); + + mService.setLockCredentialWithToken(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID); + + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode()); + assertArrayEquals(storageKey, mStorageManager.getUserUnlockToken(PRIMARY_USER_ID)); + } + + public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNeedsMigration() throws RemoteException { + final String TOKEN = "some-high-entropy-secure-token"; + enableSyntheticPassword(PRIMARY_USER_ID); + long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID); + assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); + assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + assertTrue(hasSyntheticPassword(PRIMARY_USER_ID)); + } + + public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNoMigration() throws RemoteException { + final String TOKEN = "some-high-entropy-secure-token"; + initializeCredentialUnderSP(null, PRIMARY_USER_ID); + long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID); + assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); + assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + assertTrue(hasSyntheticPassword(PRIMARY_USER_ID)); + } + // b/34600579 //TODO: add non-migration work profile case, and unify/un-unify transition. //TODO: test token after user resets password