Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b9382c23 authored by Rubin Xu's avatar Rubin Xu Committed by Android (Google) Code Review
Browse files

Merge "Add escrow token support to synthetic password flow"

parents 5ccd8291 f095f836
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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);
}
+104 −2
Original line number Diff line number Diff line
@@ -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.
     */
+164 −0
Original line number Diff line number Diff line
@@ -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.");
        }
    }
}
+103 −3
Original line number Diff line number Diff line
@@ -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<Integer, ArrayMap<Long, byte[]>> 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<Long> 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();
        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);
+83 −0
Original line number Diff line number Diff line
@@ -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