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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user