Eliminate unnecessary work for empty LSKFs

Improve the performance of creating and unlocking the LSKF-based SP
protector when the LSKF is empty by using the minimum scrypt parameters,
and by skipping the Gatekeeper step when Weaver is unavailable.

This also has the "benefit" that it avoids depending on auth-bound keys
working correctly for the device to boot when the LSKF is empty.  That
is relevant when an OEM has not implemented auth-bound keys correctly.

Bug: 232452368
Bug: 251131631
Bug: 251147505
Bug: 251398909
Change-Id: Ic58580455a789d177c077d12efe5ab6c0ec2085b
This commit is contained in:
Eric Biggers
2022-10-05 23:19:15 +00:00
parent 026b01f03e
commit 264b27f819
2 changed files with 137 additions and 79 deletions

View File

@@ -333,16 +333,25 @@ public class SyntheticPasswordManager {
byte scryptLogP;
public int credentialType;
byte[] salt;
// If Weaver is available, then this field is empty. Otherwise, it is the Gatekeeper
// password handle that resulted from enrolling the hashed LSKF.
// 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.
public byte[] passwordHandle;
public static PasswordData create(int passwordType) {
public static PasswordData create(int credentialType) {
PasswordData result = new PasswordData();
result.scryptLogN = PASSWORD_SCRYPT_LOG_N;
result.scryptLogR = PASSWORD_SCRYPT_LOG_R;
result.scryptLogP = PASSWORD_SCRYPT_LOG_P;
result.credentialType = passwordType;
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;
}
@@ -776,11 +785,12 @@ public class SyntheticPasswordManager {
long protectorId = generateProtectorId();
PasswordData pwd = PasswordData.create(credential.getType());
byte[] stretchedLskf = stretchLskf(credential, pwd);
final long sid;
long sid = GateKeeper.INVALID_SECURE_USER_ID;
final byte[] protectorSecret;
if (isWeaverAvailable()) {
// Protector uses Weaver to verify the LSKF
// Weaver is available, so make the protector use it to verify the LSKF. Do this even
// if the LSKF is empty, as that gives us support for securely deleting the protector.
int weaverSlot = getNextAvailableWeaverSlot();
Slog.i(TAG, "Weaver enroll password to slot " + weaverSlot + " for user " + userId);
byte[] weaverSecret = weaverEnroll(weaverSlot, stretchedLskfToWeaverKey(stretchedLskf),
@@ -794,33 +804,34 @@ public class SyntheticPasswordManager {
// No need to pass in quality since the credential type already encodes sufficient info
synchronizeWeaverFrpPassword(pwd, 0, userId, weaverSlot);
pwd.passwordHandle = null;
sid = GateKeeper.INVALID_SECURE_USER_ID;
protectorSecret = transformUnderWeaverSecret(stretchedLskf, weaverSecret);
} else {
// Protector uses Gatekeeper to verify the LSKF
// In case GK enrollment leaves persistent state around (in RPMB), this will nuke them
// to prevent them from accumulating and causing problems.
try {
gatekeeper.clearSecureUserId(fakeUserId(userId));
} catch (RemoteException ignore) {
Slog.w(TAG, "Failed to clear SID from gatekeeper");
// Weaver is unavailable, so make the protector use Gatekeeper to verify the LSKF
// instead. However, skip Gatekeeper when the LSKF is empty, since it wouldn't give any
// benefit in that case as Gatekeeper isn't expected to provide secure deletion.
if (!credential.isNone()) {
// In case GK enrollment leaves persistent state around (in RPMB), this will nuke
// them to prevent them from accumulating and causing problems.
try {
gatekeeper.clearSecureUserId(fakeUserId(userId));
} catch (RemoteException ignore) {
Slog.w(TAG, "Failed to clear SID from gatekeeper");
}
GateKeeperResponse response;
try {
response = gatekeeper.enroll(fakeUserId(userId), null, null,
stretchedLskfToGkPassword(stretchedLskf));
} catch (RemoteException e) {
throw new IllegalStateException("Failed to enroll LSKF for new SP protector"
+ " for user " + userId, e);
}
if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
throw new IllegalStateException("Failed to enroll LSKF for new SP protector"
+ " for user " + userId);
}
pwd.passwordHandle = response.getPayload();
sid = sidFromPasswordHandle(pwd.passwordHandle);
}
GateKeeperResponse response;
try {
response = gatekeeper.enroll(fakeUserId(userId), null, null,
stretchedLskfToGkPassword(stretchedLskf));
} catch (RemoteException e) {
throw new IllegalStateException("Failed to enroll LSKF for new SP protector for "
+ "user " + userId, e);
}
if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
throw new IllegalStateException("Failed to enroll LSKF for new SP protector for "
+ "user " + userId);
}
pwd.passwordHandle = response.getPayload();
sid = sidFromPasswordHandle(pwd.passwordHandle);
protectorSecret = transformUnderSecdiscardable(stretchedLskf,
createSecdiscardable(protectorId, userId));
// No need to pass in quality since the credential type already encodes sufficient info
@@ -1049,7 +1060,7 @@ public class SyntheticPasswordManager {
byte[] stretchedLskf = stretchLskf(credential, pwd);
final byte[] protectorSecret;
final long sid;
long sid = GateKeeper.INVALID_SECURE_USER_ID;
int weaverSlot = loadWeaverSlot(protectorId, userId);
if (weaverSlot != INVALID_WEAVER_SLOT) {
// Protector uses Weaver to verify the LSKF
@@ -1062,54 +1073,62 @@ public class SyntheticPasswordManager {
if (result.gkResponse.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK) {
return result;
}
sid = GateKeeper.INVALID_SECURE_USER_ID;
protectorSecret = transformUnderWeaverSecret(stretchedLskf,
result.gkResponse.getGatekeeperHAT());
} else {
// Protector uses Gatekeeper to verify the LSKF
byte[] gkPassword = stretchedLskfToGkPassword(stretchedLskf);
GateKeeperResponse response;
try {
response = gatekeeper.verifyChallenge(fakeUserId(userId), 0L,
pwd.passwordHandle, gkPassword);
} catch (RemoteException e) {
Slog.e(TAG, "gatekeeper verify failed", e);
result.gkResponse = VerifyCredentialResponse.ERROR;
return result;
}
int responseCode = response.getResponseCode();
if (responseCode == GateKeeperResponse.RESPONSE_OK) {
result.gkResponse = VerifyCredentialResponse.OK;
if (response.getShouldReEnroll()) {
GateKeeperResponse reenrollResponse;
try {
reenrollResponse = gatekeeper.enroll(fakeUserId(userId),
pwd.passwordHandle, gkPassword, gkPassword);
} catch (RemoteException e) {
Slog.w(TAG, "Fail to invoke gatekeeper.enroll", e);
reenrollResponse = GateKeeperResponse.ERROR;
// continue the flow anyway
}
if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
pwd.passwordHandle = reenrollResponse.getPayload();
// Use the reenrollment opportunity to update credential type
// (getting rid of CREDENTIAL_TYPE_PASSWORD_OR_PIN)
pwd.credentialType = credential.getType();
saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
synchronizeFrpPassword(pwd, 0, userId);
} else {
Slog.w(TAG, "Fail to re-enroll user password for user " + userId);
// continue the flow anyway
}
// 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 (!credential.isNone()) {
Slog.e(TAG, "Missing Gatekeeper password handle for nonempty LSKF");
result.gkResponse = VerifyCredentialResponse.ERROR;
return result;
}
} else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
result.gkResponse = VerifyCredentialResponse.fromTimeout(response.getTimeout());
return result;
} else {
result.gkResponse = VerifyCredentialResponse.ERROR;
return result;
} else {
byte[] gkPassword = stretchedLskfToGkPassword(stretchedLskf);
GateKeeperResponse response;
try {
response = gatekeeper.verifyChallenge(fakeUserId(userId), 0L,
pwd.passwordHandle, gkPassword);
} catch (RemoteException e) {
Slog.e(TAG, "gatekeeper verify failed", e);
result.gkResponse = VerifyCredentialResponse.ERROR;
return result;
}
int responseCode = response.getResponseCode();
if (responseCode == GateKeeperResponse.RESPONSE_OK) {
result.gkResponse = VerifyCredentialResponse.OK;
if (response.getShouldReEnroll()) {
GateKeeperResponse reenrollResponse;
try {
reenrollResponse = gatekeeper.enroll(fakeUserId(userId),
pwd.passwordHandle, gkPassword, gkPassword);
} catch (RemoteException e) {
Slog.w(TAG, "Fail to invoke gatekeeper.enroll", e);
reenrollResponse = GateKeeperResponse.ERROR;
// continue the flow anyway
}
if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
pwd.passwordHandle = reenrollResponse.getPayload();
// Use the reenrollment opportunity to update credential type
// (getting rid of CREDENTIAL_TYPE_PASSWORD_OR_PIN)
pwd.credentialType = credential.getType();
saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
synchronizeFrpPassword(pwd, 0, userId);
} else {
Slog.w(TAG, "Fail to re-enroll user password for user " + userId);
// continue the flow anyway
}
}
} else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
result.gkResponse = VerifyCredentialResponse.fromTimeout(response.getTimeout());
return result;
} else {
result.gkResponse = VerifyCredentialResponse.ERROR;
return result;
}
sid = sidFromPasswordHandle(pwd.passwordHandle);
}
sid = sidFromPasswordHandle(pwd.passwordHandle);
protectorSecret = transformUnderSecdiscardable(stretchedLskf,
loadSecdiscardable(protectorId, userId));
}
@@ -1463,7 +1482,8 @@ public class SyntheticPasswordManager {
return result;
}
private int fakeUserId(int userId) {
@VisibleForTesting
static int fakeUserId(int userId) {
return 100000 + userId;
}

View File

@@ -75,15 +75,34 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
}
@Test
public void testLskfBasedProtector() throws RemoteException {
public void testNoneLskfBasedProtector() throws RemoteException {
final int USER_ID = 10;
MockSyntheticPasswordManager manager = new MockSyntheticPasswordManager(mContext, mStorage,
mGateKeeperService, mUserManager, mPasswordSlotManager);
SyntheticPassword sp = manager.newSyntheticPassword(USER_ID);
assertFalse(lskfGatekeeperHandleExists(USER_ID));
long protectorId = manager.createLskfBasedProtector(mGateKeeperService,
LockscreenCredential.createNone(), sp, USER_ID);
assertFalse(lskfGatekeeperHandleExists(USER_ID));
AuthenticationResult result = manager.unlockLskfBasedProtector(mGateKeeperService,
protectorId, LockscreenCredential.createNone(), USER_ID, null);
assertArrayEquals(result.syntheticPassword.deriveKeyStorePassword(),
sp.deriveKeyStorePassword());
}
@Test
public void testNonNoneLskfBasedProtector() throws RemoteException {
final int USER_ID = 10;
final LockscreenCredential password = newPassword("user-password");
final LockscreenCredential badPassword = newPassword("bad-password");
MockSyntheticPasswordManager manager = new MockSyntheticPasswordManager(mContext, mStorage,
mGateKeeperService, mUserManager, mPasswordSlotManager);
SyntheticPassword sp = manager.newSyntheticPassword(USER_ID);
assertFalse(lskfGatekeeperHandleExists(USER_ID));
long protectorId = manager.createLskfBasedProtector(mGateKeeperService, password, sp,
USER_ID);
assertTrue(lskfGatekeeperHandleExists(USER_ID));
AuthenticationResult result = manager.unlockLskfBasedProtector(mGateKeeperService,
protectorId, password, USER_ID, null);
@@ -95,6 +114,10 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
assertNull(result.syntheticPassword);
}
private boolean lskfGatekeeperHandleExists(int userId) throws RemoteException {
return mGateKeeperService.getSecureUserId(SyntheticPasswordManager.fakeUserId(userId)) != 0;
}
private boolean hasSyntheticPassword(int userId) throws RemoteException {
return mService.getLong(CURRENT_LSKF_BASED_PROTECTOR_ID_KEY, 0, userId) != 0;
}
@@ -429,6 +452,21 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
mService.getHashFactor(profilePassword, MANAGED_PROFILE_USER_ID));
}
@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);
// 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);
}
@Test
public void testPasswordData_serializeDeserialize() {
PasswordData data = new PasswordData();