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

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

Merge "Restore password metrics after a credential-less unlock" into rvc-dev

parents 7410a9c3 3589e891
Loading
Loading
Loading
Loading
+25 −10
Original line number Diff line number Diff line
@@ -2161,6 +2161,13 @@ public class LockSettingsService extends ILockSettings.Stub {
        }
    }

    private PasswordMetrics loadPasswordMetrics(AuthenticationToken auth, int userHandle) {
        synchronized (mSpManager) {
            return mSpManager.getPasswordMetrics(auth, getSyntheticPasswordHandleLocked(userHandle),
                    userHandle);
        }
    }

    /**
     * Call after {@link #setUserPasswordMetrics} so metrics are updated before
     * reporting the password changed.
@@ -2611,7 +2618,8 @@ public class LockSettingsService extends ILockSettings.Stub {
        return auth;
    }

    private long getSyntheticPasswordHandleLocked(int userId) {
    @VisibleForTesting
    long getSyntheticPasswordHandleLocked(int userId) {
        return getLong(SYNTHETIC_PASSWORD_HANDLE_KEY,
                SyntheticPasswordManager.DEFAULT_HANDLE, userId);
    }
@@ -2706,13 +2714,8 @@ public class LockSettingsService extends ILockSettings.Stub {
                resetLockouts.add(new PendingResetLockout(userId, response.getPayload()));
            }

            // TODO: Move setUserPasswordMetrics() inside onCredentialVerified(): this will require
            // LSS to store an encrypted version of the latest password metric for every user,
            // because user credential is not known when onCredentialVerified() is called during
            // a token-based unlock.
            setUserPasswordMetrics(userCredential, userId);
            onCredentialVerified(authResult.authToken, challengeType, challenge, resetLockouts,
                    userId);
                    PasswordMetrics.computeForCredential(userCredential), userId);
        } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
            if (response.getTimeout() > 0) {
                requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
@@ -2724,7 +2727,16 @@ public class LockSettingsService extends ILockSettings.Stub {

    private void onCredentialVerified(AuthenticationToken authToken,
            @ChallengeType int challengeType, long challenge,
            @Nullable ArrayList<PendingResetLockout> resetLockouts, int userId) {
            @Nullable ArrayList<PendingResetLockout> resetLockouts, PasswordMetrics metrics,
            int userId) {

        if (metrics != null) {
            synchronized (this) {
                mUserPasswordMetrics.put(userId,  metrics);
            }
        } else {
            Slog.wtf(TAG, "Null metrics after credential verification");
        }

        unlockKeystore(authToken.deriveKeyStorePassword(), userId);

@@ -3118,7 +3130,9 @@ public class LockSettingsService extends ILockSettings.Stub {
        // TODO: Reset biometrics lockout here. Ideally that should be self-contained inside
        // onCredentialVerified(), which will require some refactoring on the current lockout
        // reset logic.
        onCredentialVerified(authResult.authToken, CHALLENGE_NONE, 0, null, userId);

        onCredentialVerified(authResult.authToken, CHALLENGE_NONE, 0, null,
                loadPasswordMetrics(authResult.authToken, userId), userId);
        return true;
    }

@@ -3414,7 +3428,8 @@ public class LockSettingsService extends ILockSettings.Stub {
            SyntheticPasswordManager.AuthenticationToken
                    authToken = new SyntheticPasswordManager.AuthenticationToken(spVersion);
            authToken.recreateDirectly(syntheticPassword);
            onCredentialVerified(authToken, CHALLENGE_NONE, 0, null, userId);
            onCredentialVerified(authToken, CHALLENGE_NONE, 0, null,
                    loadPasswordMetrics(authToken, userId), userId);
        }
    }
}
+44 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static com.android.internal.widget.LockPatternUtils.EscrowTokenStateChang

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.PasswordMetrics;
import android.content.Context;
import android.content.pm.UserInfo;
import android.hardware.weaver.V1_0.IWeaver;
@@ -97,6 +98,7 @@ public class SyntheticPasswordManager {
    private static final int SECDISCARDABLE_LENGTH = 16 * 1024;
    private static final String PASSWORD_DATA_NAME = "pwd";
    private static final String WEAVER_SLOT_NAME = "weaver";
    private static final String PASSWORD_METRICS_NAME = "metrics";

    public static final long DEFAULT_HANDLE = 0L;
    private static final byte[] DEFAULT_PASSWORD = "default-password".getBytes();
@@ -132,6 +134,7 @@ public class SyntheticPasswordManager {
    private static final byte[] PERSONALISATION_WEAVER_PASSWORD = "weaver-pwd".getBytes();
    private static final byte[] PERSONALISATION_WEAVER_KEY = "weaver-key".getBytes();
    private static final byte[] PERSONALISATION_WEAVER_TOKEN = "weaver-token".getBytes();
    private static final byte[] PERSONALIZATION_PASSWORD_METRICS = "password-metrics".getBytes();
    private static final byte[] PERSONALISATION_CONTEXT =
        "android-synthetic-password-personalization-context".getBytes();

@@ -212,6 +215,11 @@ public class SyntheticPasswordManager {
            return derivePassword(PERSONALIZATION_PASSWORD_HASH);
        }

        /** Derives key used to encrypt password metrics */
        public byte[] deriveMetricsKey() {
            return derivePassword(PERSONALIZATION_PASSWORD_METRICS);
        }

        /**
         * Assign escrow data to this auth token. This is a prerequisite to call
         * {@link AuthenticationToken#recreateFromEscrow}.
@@ -778,7 +786,7 @@ public class SyntheticPasswordManager {
            synchronizeFrpPassword(pwd, 0, userId);
        }
        saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);

        savePasswordMetrics(credential, authToken, handle, userId);
        createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED, authToken,
                applicationId, sid, userId);
        return handle;
@@ -1061,6 +1069,12 @@ public class SyntheticPasswordManager {

        // Perform verifyChallenge to refresh auth tokens for GK if user password exists.
        result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId);

        // Upgrade case: store the metrics if the device did not have stored metrics before, should
        // only happen once on old synthetic password blobs.
        if (result.authToken != null && !hasPasswordMetrics(handle, userId)) {
            savePasswordMetrics(credential, result.authToken, handle, userId);
        }
        return result;
    }

@@ -1216,6 +1230,7 @@ public class SyntheticPasswordManager {
        destroySyntheticPassword(handle, userId);
        destroyState(SECDISCARDABLE_NAME, handle, userId);
        destroyState(PASSWORD_DATA_NAME, handle, userId);
        destroyState(PASSWORD_METRICS_NAME, handle, userId);
    }

    private void destroySyntheticPassword(long handle, int userId) {
@@ -1258,6 +1273,34 @@ public class SyntheticPasswordManager {
        return loadState(SECDISCARDABLE_NAME, handle, userId);
    }

    /**
     * Retrieves the saved password metrics associated with a SP handle. Only meaningful to be
     * called on the handle of a password-based synthetic password. A valid AuthenticationToken for
     * the target user is required in order to be able to decrypt the encrypted password metrics on
     * disk.
     */
    public @Nullable PasswordMetrics getPasswordMetrics(AuthenticationToken authToken, long handle,
            int userId) {
        final byte[] encrypted = loadState(PASSWORD_METRICS_NAME, handle, userId);
        if (encrypted == null) return null;
        final byte[] decrypted = SyntheticPasswordCrypto.decrypt(authToken.deriveMetricsKey(),
                /* personalization= */ new byte[0], encrypted);
        if (decrypted == null) return null;
        return VersionedPasswordMetrics.deserialize(decrypted).getMetrics();
    }

    private void savePasswordMetrics(LockscreenCredential credential, AuthenticationToken authToken,
            long handle, int userId) {
        final byte[] encrypted = SyntheticPasswordCrypto.encrypt(authToken.deriveMetricsKey(),
                /* personalization= */ new byte[0],
                new VersionedPasswordMetrics(credential).serialize());
        saveState(PASSWORD_METRICS_NAME, encrypted, handle, userId);
    }

    private boolean hasPasswordMetrics(long handle, int userId) {
        return hasState(PASSWORD_METRICS_NAME, handle, userId);
    }

    private boolean hasState(String stateName, long handle, int userId) {
        return !ArrayUtils.isEmpty(loadState(stateName, handle, userId));
    }
+79 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server.locksettings;

import android.app.admin.PasswordMetrics;

import com.android.internal.widget.LockscreenCredential;

import java.nio.ByteBuffer;

/**
 * A versioned and serializable wrapper around {@link PasswordMetrics},
 * for long-term persistence on disk.
 */
public class VersionedPasswordMetrics {
    private static final int VERSION_1 = 1;

    private final PasswordMetrics mMetrics;
    private final int mVersion;

    private VersionedPasswordMetrics(int version, PasswordMetrics metrics) {
        mMetrics = metrics;
        mVersion = version;
    }

    public VersionedPasswordMetrics(LockscreenCredential credential) {
        this(VERSION_1, PasswordMetrics.computeForCredential(credential));
    }

    public int getVersion() {
        return mVersion;
    }

    public PasswordMetrics getMetrics() {
        return mMetrics;
    }

    /** Serialize object to a byte array. */
    public byte[] serialize() {
        final ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES * 11);
        buffer.putInt(mVersion);
        buffer.putInt(mMetrics.credType);
        buffer.putInt(mMetrics.length);
        buffer.putInt(mMetrics.letters);
        buffer.putInt(mMetrics.upperCase);
        buffer.putInt(mMetrics.lowerCase);
        buffer.putInt(mMetrics.numeric);
        buffer.putInt(mMetrics.symbols);
        buffer.putInt(mMetrics.nonLetter);
        buffer.putInt(mMetrics.nonNumeric);
        buffer.putInt(mMetrics.seqLength);
        return buffer.array();
    }

    /** Deserialize byte array to an object */
    public static VersionedPasswordMetrics deserialize(byte[] data) {
        final ByteBuffer buffer = ByteBuffer.allocate(data.length);
        buffer.put(data, 0, data.length);
        buffer.flip();
        final int version = buffer.getInt();
        PasswordMetrics metrics = new PasswordMetrics(buffer.getInt(), buffer.getInt(),
                buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getInt(),
                buffer.getInt(), buffer.getInt(), buffer.getInt());
        return new VersionedPasswordMetrics(version, metrics);
    }
}
+40 −1
Original line number Diff line number Diff line
@@ -53,6 +53,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;

import java.io.File;
import java.util.ArrayList;


@@ -104,7 +105,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
    protected void initializeCredentialUnderSP(LockscreenCredential password, int userId)
            throws RemoteException {
        enableSyntheticPassword();
        mService.setLockCredential(password, nonePassword(), userId);
        assertTrue(mService.setLockCredential(password, nonePassword(), userId));
        assertEquals(CREDENTIAL_TYPE_PASSWORD, mService.getCredentialType(userId));
        assertTrue(mService.isSyntheticPasswordBasedCredential(userId));
    }
@@ -492,6 +493,44 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
        verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
    }

    @Test
    public void testUnlockUserWithToken() throws Exception {
        LockscreenCredential password = newPassword("password");
        byte[] token = "some-high-entropy-secure-token".getBytes();
        initializeCredentialUnderSP(password, PRIMARY_USER_ID);
        // Disregard any reportPasswordChanged() invocations as part of credential setup.
        flushHandlerTasks();
        reset(mDevicePolicyManager);

        long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
        mService.verifyCredential(password, 0, PRIMARY_USER_ID).getResponseCode();
        assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));

        mService.onCleanupUser(PRIMARY_USER_ID);
        assertNull(mLocalService.getUserPasswordMetrics(PRIMARY_USER_ID));

        assertTrue(mLocalService.unlockUserWithToken(handle, token, PRIMARY_USER_ID));
        assertEquals(PasswordMetrics.computeForCredential(password),
                mLocalService.getUserPasswordMetrics(PRIMARY_USER_ID));
    }

    @Test
    public void testPasswordChange_NoOrphanedFilesLeft() throws Exception {
        LockscreenCredential password = newPassword("password");
        initializeCredentialUnderSP(password, PRIMARY_USER_ID);
        assertTrue(mService.setLockCredential(password, password, PRIMARY_USER_ID));

        String handleString = String.format("%016x",
                mService.getSyntheticPasswordHandleLocked(PRIMARY_USER_ID));
        File directory = mStorage.getSyntheticPasswordDirectoryForUser(PRIMARY_USER_ID);
        for (File file : directory.listFiles()) {
            String[] parts = file.getName().split("\\.");
            if (!parts[0].equals(handleString) && !parts[0].equals("0000000000000000")) {
                fail("Orphaned state left: " + file.getName());
            }
        }
    }

    // b/62213311
    //TODO: add non-migration work profile case, and unify/un-unify transition.
    //TODO: test token after user resets password