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

Commit 3bfee24f authored by Eric Biggers's avatar Eric Biggers
Browse files

Skip creating password data file for unsecured users

As another optimization, skip creating the "password data" file in the
LSKF-based protector when the LSKF is empty.  When the LSKF is empty,
the password data file is unnecessary except for providing the scrypt
salt and parameters.  However, since scrypt provides no security benefit
when the LSKF is empty, we can instead just skip the scrypt step.

This saves creating one file, which can be somewhat expensive due to the
use of synchronous writes and an fsync of the parent directory.  It also
saves the scrypt operation (albeit with the minimum parameters) and the
random salt generation.

Bug: 232452368
Bug: 251131631
Bug: 251147505
Test: atest com.android.server.locksettings
Change-Id: Ifdc0d507b81cc4b877d1177fb9c31dbff84cdbb0
parent cdf39de7
Loading
Loading
Loading
Loading
+60 −39
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import com.android.internal.widget.ICheckCredentialProgressCallback;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockPatternUtils;
@@ -97,7 +98,8 @@ import java.util.Set;
 *     For each protector, stored under the corresponding protector ID:
 *       SP_BLOB_NAME: The encrypted SP secret (the SP itself or the P0 value).  Always exists.
 *       PASSWORD_DATA_NAME: Data used for LSKF verification, such as the scrypt salt and
 *                           parameters.  Only exists for LSKF-based protectors.
 *                           parameters.  Only exists for LSKF-based protectors.  Doesn't exist when
 *                           the LSKF is empty, except in old protectors.
 *       PASSWORD_METRICS_NAME: Metrics about the LSKF, encrypted by a key derived from the SP.
 *                              Only exists for LSKF-based protectors.  Doesn't exist when the LSKF
 *                              is empty, except in old protectors.
@@ -334,24 +336,15 @@ public class SyntheticPasswordManager {
        byte scryptLogP;
        public int credentialType;
        byte[] salt;
        // This is the Gatekeeper password handle that resulted from enrolling the stretched LSKF,
        // when applicable.  This field isn't used if Weaver is available, or in new protectors when
        // the LSKF is empty.
        // When Weaver is unavailable, this is the Gatekeeper password handle that resulted from
        // enrolling the stretched LSKF.
        public byte[] passwordHandle;

        public static PasswordData create(int credentialType) {
            PasswordData result = new PasswordData();
            if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
                // When the LSKF is empty, scrypt provides no security benefit, so just use the
                // minimum parameters (N=2, r=1, p=1).
                result.scryptLogN = 1;
                result.scryptLogR = 0;
                result.scryptLogP = 0;
            } else {
            result.scryptLogN = PASSWORD_SCRYPT_LOG_N;
            result.scryptLogR = PASSWORD_SCRYPT_LOG_R;
            result.scryptLogP = PASSWORD_SCRYPT_LOG_P;
            }
            result.credentialType = credentialType;
            result.salt = secureRandom(PASSWORD_SALT_LENGTH);
            return result;
@@ -612,7 +605,6 @@ public class SyntheticPasswordManager {
    int getCredentialType(long protectorId, int userId) {
        byte[] passwordData = loadState(PASSWORD_DATA_NAME, protectorId, userId);
        if (passwordData == null) {
            Slog.w(TAG, "getCredentialType: encountered empty password data for user " + userId);
            return LockPatternUtils.CREDENTIAL_TYPE_NONE;
        }
        return PasswordData.fromBytes(passwordData).credentialType;
@@ -784,7 +776,8 @@ public class SyntheticPasswordManager {
    public long createLskfBasedProtector(IGateKeeperService gatekeeper,
            LockscreenCredential credential, SyntheticPassword sp, int userId) {
        long protectorId = generateProtectorId();
        PasswordData pwd = PasswordData.create(credential.getType());
        // There's no need to store password data about an empty LSKF.
        PasswordData pwd = credential.isNone() ? null : PasswordData.create(credential.getType());
        byte[] stretchedLskf = stretchLskf(credential, pwd);
        long sid = GateKeeper.INVALID_SECURE_USER_ID;
        final byte[] protectorSecret;
@@ -838,8 +831,8 @@ public class SyntheticPasswordManager {
            // No need to pass in quality since the credential type already encodes sufficient info
            synchronizeFrpPassword(pwd, 0, userId);
        }
        saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
        if (!credential.isNone()) {
            saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
            savePasswordMetrics(credential, sp, protectorId, userId);
        }
        createSyntheticPasswordBlob(protectorId, PROTECTOR_TYPE_LSKF_BASED, sp, protectorSecret,
@@ -886,10 +879,11 @@ public class SyntheticPasswordManager {
    public void migrateFrpPasswordLocked(long protectorId, UserInfo userInfo,
            int requestedQuality) {
        if (mStorage.getPersistentDataBlockManager() != null
                && LockPatternUtils.userOwnsFrpCredential(mContext, userInfo)) {
                && LockPatternUtils.userOwnsFrpCredential(mContext, userInfo)
                && getCredentialType(protectorId, userInfo.id) !=
                        LockPatternUtils.CREDENTIAL_TYPE_NONE) {
            PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId,
                    userInfo.id));
            if (pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
            int weaverSlot = loadWeaverSlot(protectorId, userInfo.id);
            if (weaverSlot != INVALID_WEAVER_SLOT) {
                synchronizeWeaverFrpPassword(pwd, requestedQuality, userInfo.id, weaverSlot);
@@ -898,14 +892,13 @@ public class SyntheticPasswordManager {
            }
        }
    }
    }

    private void synchronizeFrpPassword(PasswordData pwd,
            int requestedQuality, int userId) {
    private void synchronizeFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
            int userId) {
        if (mStorage.getPersistentDataBlockManager() != null
                && LockPatternUtils.userOwnsFrpCredential(mContext,
                mUserManager.getUserInfo(userId))) {
            if (pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
            if (pwd != null && pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
                mStorage.writePersistentDataBlock(PersistentData.TYPE_SP, userId, requestedQuality,
                        pwd.toBytes());
            } else {
@@ -914,12 +907,12 @@ public class SyntheticPasswordManager {
        }
    }

    private void synchronizeWeaverFrpPassword(PasswordData pwd, int requestedQuality, int userId,
            int weaverSlot) {
    private void synchronizeWeaverFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
            int userId, int weaverSlot) {
        if (mStorage.getPersistentDataBlockManager() != null
                && LockPatternUtils.userOwnsFrpCredential(mContext,
                mUserManager.getUserInfo(userId))) {
            if (pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
            if (pwd != null && pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
                mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, weaverSlot,
                        requestedQuality, pwd.toBytes());
            } else {
@@ -1050,12 +1043,20 @@ public class SyntheticPasswordManager {
            return result;
        }

        PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId,
                    userId));

        if (!credential.checkAgainstStoredType(pwd.credentialType)) {
        // Load the PasswordData file.  If it doesn't exist, then the LSKF is empty (i.e.,
        // CREDENTIAL_TYPE_NONE), and we'll skip the scrypt and Gatekeeper steps.  If it exists,
        // then either the LSKF is nonempty, or it's an old protector that uses scrypt and
        // Gatekeeper even though the LSKF is empty.
        byte[] pwdDataBytes = loadState(PASSWORD_DATA_NAME, protectorId, userId);
        PasswordData pwd = null;
        int storedType = LockPatternUtils.CREDENTIAL_TYPE_NONE;
        if (pwdDataBytes != null) {
            pwd = PasswordData.fromBytes(pwdDataBytes);
            storedType = pwd.credentialType;
        }
        if (!credential.checkAgainstStoredType(storedType)) {
            Slog.e(TAG, TextUtils.formatSimple("Credential type mismatch: expected %d actual %d",
                    pwd.credentialType, credential.getType()));
                    storedType, credential.getType()));
            result.gkResponse = VerifyCredentialResponse.ERROR;
            return result;
        }
@@ -1081,7 +1082,7 @@ public class SyntheticPasswordManager {
        } else {
            // Weaver is unavailable, so the protector uses Gatekeeper to verify the LSKF, unless
            // the LSKF is empty in which case Gatekeeper might not have been used at all.
            if (pwd.passwordHandle == null) {
            if (pwd == null || pwd.passwordHandle == null) {
                if (!credential.isNone()) {
                    Slog.e(TAG, "Missing Gatekeeper password handle for nonempty LSKF");
                    result.gkResponse = VerifyCredentialResponse.ERROR;
@@ -1419,6 +1420,11 @@ public class SyntheticPasswordManager {
        }
    }

    @VisibleForTesting
    boolean hasPasswordData(long protectorId, int userId) {
        return hasState(PASSWORD_DATA_NAME, protectorId, userId);
    }

    /**
     * Retrieves a user's saved password metrics from their LSKF-based SP protector.  The
     * SyntheticPassword itself is needed to decrypt the file containing the password metrics.
@@ -1511,8 +1517,23 @@ public class SyntheticPasswordManager {
        return TextUtils.formatSimple("%s%x", PROTECTOR_KEY_ALIAS_PREFIX, protectorId);
    }

    private byte[] stretchLskf(LockscreenCredential credential, PasswordData data) {
    /**
     * Stretches <code>credential</code>, if needed, using the parameters from <code>data</code>.
     * <p>
     * When the credential is empty, stetching provides no security benefit.  Thus, new protectors
     * for an empty credential use <code>null</code> {@link PasswordData} and skip the stretching.
     * <p>
     * However, old protectors always stored {@link PasswordData} and did the stretching, regardless
     * of whether the credential was empty or not.  For this reason, this method also continues to
     * support stretching of empty credentials so that old protectors can still be unlocked.
     */
    @VisibleForTesting
    byte[] stretchLskf(LockscreenCredential credential, @Nullable PasswordData data) {
        final byte[] password = credential.isNone() ? DEFAULT_PASSWORD : credential.getCredential();
        if (data == null) {
            Preconditions.checkArgument(credential.isNone());
            return Arrays.copyOf(password, STRETCHED_LSKF_LENGTH);
        }
        return scrypt(password, data.salt, 1 << data.scryptLogN, 1 << data.scryptLogR,
                1 << data.scryptLogP, STRETCHED_LSKF_LENGTH);
    }
+43 −11
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
@@ -49,6 +50,8 @@ import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationRe
import com.android.server.locksettings.SyntheticPasswordManager.PasswordData;
import com.android.server.locksettings.SyntheticPasswordManager.SyntheticPassword;

import libcore.util.HexEncoding;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -84,6 +87,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
        long protectorId = manager.createLskfBasedProtector(mGateKeeperService,
                LockscreenCredential.createNone(), sp, USER_ID);
        assertFalse(lskfGatekeeperHandleExists(USER_ID));
        assertFalse(manager.hasPasswordData(protectorId, USER_ID));
        assertFalse(manager.hasPasswordMetrics(protectorId, USER_ID));

        AuthenticationResult result = manager.unlockLskfBasedProtector(mGateKeeperService,
@@ -104,6 +108,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
        long protectorId = manager.createLskfBasedProtector(mGateKeeperService, password, sp,
                USER_ID);
        assertTrue(lskfGatekeeperHandleExists(USER_ID));
        assertTrue(manager.hasPasswordData(protectorId, USER_ID));
        assertTrue(manager.hasPasswordMetrics(protectorId, USER_ID));

        AuthenticationResult result = manager.unlockLskfBasedProtector(mGateKeeperService,
@@ -454,19 +459,46 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
                mService.getHashFactor(profilePassword, MANAGED_PROFILE_USER_ID));
    }

    // Tests stretching of a nonempty LSKF.
    @Test
    public void testPasswordData_scryptParams() {
        // CREDENTIAL_TYPE_NONE should result in the minimum scrypt params being used.
        PasswordData data = PasswordData.create(CREDENTIAL_TYPE_NONE);
        assertEquals(1, data.scryptLogN);
        assertEquals(0, data.scryptLogR);
        assertEquals(0, data.scryptLogP);
    public void testStretchLskf_enabled() {
        byte[] actual = mSpManager.stretchLskf(newPin("12345"), createTestPasswordData());
        String expected = "467986710DE8F0D4F4A3668DFF58C9B7E5DB96A79B7CCF415BBD4D7767F8CFFA";
        assertEquals(expected, HexEncoding.encodeToString(actual));
    }

        // Any other credential type should result in the real scrypt params being used.
        data = PasswordData.create(CREDENTIAL_TYPE_PASSWORD);
        assertTrue(data.scryptLogN > 1);
        assertTrue(data.scryptLogR > 0);
        assertTrue(data.scryptLogP > 0);
    // Tests the case where stretching is disabled for an empty LSKF.
    @Test
    public void testStretchLskf_disabled() {
        byte[] actual = mSpManager.stretchLskf(nonePassword(), null);
        // "default-password", zero padded
        String expected = "64656661756C742D70617373776F726400000000000000000000000000000000";
        assertEquals(expected, HexEncoding.encodeToString(actual));
    }

    // Tests the legacy case where stretching is enabled for an empty LSKF.
    @Test
    public void testStretchLskf_emptyButEnabled() {
        byte[] actual = mSpManager.stretchLskf(nonePassword(), createTestPasswordData());
        String expected = "9E6DDCC1EC388BB1E1CD54097AF924CA80BCB90993196FA8F6122FF58EB333DE";
        assertEquals(expected, HexEncoding.encodeToString(actual));
    }

    // Tests the forbidden case where stretching is disabled for a nonempty LSKF.
    @Test
    public void testStretchLskf_nonEmptyButDisabled() {
        assertThrows(IllegalArgumentException.class,
                () -> mSpManager.stretchLskf(newPin("12345"), null));
    }

    private PasswordData createTestPasswordData() {
        PasswordData data = new PasswordData();
        // For the unit test, the scrypt parameters have to be constant; the salt can't be random.
        data.scryptLogN = 11;
        data.scryptLogR = 3;
        data.scryptLogP = 1;
        data.salt = "abcdefghijklmnop".getBytes();
        return data;
    }

    @Test