Remove unused and insecure fallback to legacy password history hash

Since users with an LSKF now always have a synthetic password, the
hashFactor needed by passwordToHistoryHash() is always available.
Therefore, new hashes in the password history always use
passwordToHistoryHash(), and the fallback to legacyPasswordToHash() is
unused.  Also, since the legacy algorithm can be easily bruteforced,
falling back to it would be a security vulnerability.  Therefore, remove
this dangerous and unnecessary code.

To make it clear that hashFactor is always available, also move the call
to updatePasswordHistory() into setLockCredentialWithSpLocked(), where
the SP is available.  This makes it so that the SP doesn't need to be
unwrapped by updatePasswordHistory().  This shouldn't have failed
anyway, but this avoids needing to consider this case at all.

For now, legacyPasswordToHash() itself is still needed for checking the
password history on devices that have legacy hashes in their database.
However, remove one of its two overloads that is no longer needed.

Finally, add a couple unit tests, as the password history functionality
didn't have any unit tests.

Test: atest com.android.server.locksettings
Test: atest LockscreenCredentialTest
Change-Id: Ib48f05fba2e63397a89da2c323b60a4641852827
This commit is contained in:
Eric Biggers
2022-07-22 19:01:03 +00:00
parent e8b1a8986a
commit bd3558749e
4 changed files with 69 additions and 38 deletions

View File

@@ -290,25 +290,15 @@ public class LockscreenCredential implements Parcelable, AutoCloseable {
} }
/** /**
* Generate a hash for the given password. To avoid brute force attacks, we use a salted hash. * Hash the given password for the password history, using the legacy algorithm.
* Not the most secure, but it is at least a second level of protection. First level is that
* the file is in a location only readable by the system process.
* *
* @return the hash of the pattern in a byte array. * @deprecated This algorithm is insecure because the password can be easily bruteforced, given
*/ * the hash and salt. Use {@link #passwordToHistoryHash(byte[], byte[], byte[])}
public String legacyPasswordToHash(byte[] salt) { * instead, which incorporates an SP-derived secret into the hash.
return legacyPasswordToHash(mCredential, salt); *
} * @return the legacy password hash
/**
* Generate a hash for the given password. To avoid brute force attacks, we use a salted hash.
* Not the most secure, but it is at least a second level of protection. First level is that
* the file is in a location only readable by the system process.
*
* @param password the gesture pattern.
*
* @return the hash of the pattern in a byte array.
*/ */
@Deprecated
public static String legacyPasswordToHash(byte[] password, byte[] salt) { public static String legacyPasswordToHash(byte[] password, byte[] salt) {
if (password == null || password.length == 0 || salt == null) { if (password == null || password.length == 0 || salt == null) {
return null; return null;

View File

@@ -223,14 +223,10 @@ public class LockscreenCredentialTest extends AndroidTestCase {
public void testLegacyPasswordToHash() { public void testLegacyPasswordToHash() {
String password = "1234"; String password = "1234";
LockscreenCredential credential = LockscreenCredential.createPassword(password);
String salt = "6d5331dd120077a0"; String salt = "6d5331dd120077a0";
String expectedHash = String expectedHash =
"2DD04348ADBF8F4CABD7F722DC2E2887FAD4B6020A0C3E02C831E09946F0554FDC13B155"; "2DD04348ADBF8F4CABD7F722DC2E2887FAD4B6020A0C3E02C831E09946F0554FDC13B155";
assertThat(
credential.legacyPasswordToHash(salt.getBytes()))
.isEqualTo(expectedHash);
assertThat( assertThat(
LockscreenCredential.legacyPasswordToHash( LockscreenCredential.legacyPasswordToHash(
password.getBytes(), salt.getBytes())) password.getBytes(), salt.getBytes()))
@@ -239,10 +235,8 @@ public class LockscreenCredentialTest extends AndroidTestCase {
public void testLegacyPasswordToHashInvalidInput() { public void testLegacyPasswordToHashInvalidInput() {
String password = "1234"; String password = "1234";
LockscreenCredential credential = LockscreenCredential.createPassword(password);
String salt = "6d5331dd120077a0"; String salt = "6d5331dd120077a0";
assertThat(credential.legacyPasswordToHash(/* salt= */ null)).isNull();
assertThat(LockscreenCredential.legacyPasswordToHash( assertThat(LockscreenCredential.legacyPasswordToHash(
password.getBytes(), /* salt= */ null)).isNull(); password.getBytes(), /* salt= */ null)).isNull();

View File

@@ -1626,7 +1626,7 @@ public class LockSettingsService extends ILockSettings.Stub {
} }
onSyntheticPasswordKnown(userId, sp); onSyntheticPasswordKnown(userId, sp);
setLockCredentialWithSpLocked(credential, sp, userId); setLockCredentialWithSpLocked(credential, sp, userId, isLockTiedToParent);
sendCredentialsOnChangeIfRequired(credential, userId, isLockTiedToParent); sendCredentialsOnChangeIfRequired(credential, userId, isLockTiedToParent);
return true; return true;
} }
@@ -1640,15 +1640,15 @@ public class LockSettingsService extends ILockSettings.Stub {
if (newCredential.isPattern()) { if (newCredential.isPattern()) {
setBoolean(LockPatternUtils.PATTERN_EVER_CHOSEN_KEY, true, userHandle); setBoolean(LockPatternUtils.PATTERN_EVER_CHOSEN_KEY, true, userHandle);
} }
updatePasswordHistory(newCredential, userHandle);
mContext.getSystemService(TrustManager.class).reportEnabledTrustAgentsChanged(userHandle); mContext.getSystemService(TrustManager.class).reportEnabledTrustAgentsChanged(userHandle);
} }
/** /**
* Store the hash of the *current* password in the password history list, if device policy * Store the hash of the new password in the password history list, if device policy enforces
* enforces password history requirement. * a password history requirement.
*/ */
private void updatePasswordHistory(LockscreenCredential password, int userHandle) { private void updatePasswordHistory(SyntheticPassword sp, LockscreenCredential password,
int userHandle, boolean isLockTiedToParent) {
if (password.isNone()) { if (password.isNone()) {
return; return;
} }
@@ -1656,8 +1656,11 @@ public class LockSettingsService extends ILockSettings.Stub {
// Do not keep track of historical patterns // Do not keep track of historical patterns
return; return;
} }
// Add the password to the password history. We assume all if (isLockTiedToParent) {
// password hashes have the same length for simplicity of implementation. // Do not keep track of historical auto-generated profile passwords
return;
}
// Add the password to the password history.
String passwordHistory = getString( String passwordHistory = getString(
LockPatternUtils.PASSWORD_HISTORY_KEY, /* defaultValue= */ null, userHandle); LockPatternUtils.PASSWORD_HISTORY_KEY, /* defaultValue= */ null, userHandle);
if (passwordHistory == null) { if (passwordHistory == null) {
@@ -1667,13 +1670,9 @@ public class LockSettingsService extends ILockSettings.Stub {
if (passwordHistoryLength == 0) { if (passwordHistoryLength == 0) {
passwordHistory = ""; passwordHistory = "";
} else { } else {
final byte[] hashFactor = getHashFactor(password, userHandle); final byte[] hashFactor = sp.derivePasswordHashFactor();
final byte[] salt = getSalt(userHandle).getBytes(); final byte[] salt = getSalt(userHandle).getBytes();
String hash = password.passwordToHistoryHash(salt, hashFactor); String hash = password.passwordToHistoryHash(salt, hashFactor);
if (hash == null) {
Slog.e(TAG, "Compute new style password hash failed, fallback to legacy style");
hash = password.legacyPasswordToHash(salt);
}
if (TextUtils.isEmpty(passwordHistory)) { if (TextUtils.isEmpty(passwordHistory)) {
passwordHistory = hash; passwordHistory = hash;
} else { } else {
@@ -2651,7 +2650,7 @@ public class LockSettingsService extends ILockSettings.Stub {
*/ */
@GuardedBy("mSpManager") @GuardedBy("mSpManager")
private long setLockCredentialWithSpLocked(LockscreenCredential credential, private long setLockCredentialWithSpLocked(LockscreenCredential credential,
SyntheticPassword sp, int userId) { SyntheticPassword sp, int userId, boolean isLockTiedToParent) {
if (DEBUG) Slog.d(TAG, "setLockCredentialWithSpLocked: user=" + userId); if (DEBUG) Slog.d(TAG, "setLockCredentialWithSpLocked: user=" + userId);
final int savedCredentialType = getCredentialTypeInternal(userId); final int savedCredentialType = getCredentialTypeInternal(userId);
final long oldProtectorId = getCurrentLskfBasedProtectorId(userId); final long oldProtectorId = getCurrentLskfBasedProtectorId(userId);
@@ -2689,6 +2688,7 @@ public class LockSettingsService extends ILockSettings.Stub {
LockPatternUtils.invalidateCredentialTypeCache(); LockPatternUtils.invalidateCredentialTypeCache();
synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords); synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords);
updatePasswordHistory(sp, credential, userId, isLockTiedToParent);
setUserPasswordMetrics(credential, userId); setUserPasswordMetrics(credential, userId);
mManagedProfilePasswordCache.removePassword(userId); mManagedProfilePasswordCache.removePassword(userId);
if (savedCredentialType != CREDENTIAL_TYPE_NONE) { if (savedCredentialType != CREDENTIAL_TYPE_NONE) {
@@ -2934,7 +2934,8 @@ public class LockSettingsService extends ILockSettings.Stub {
return false; return false;
} }
onSyntheticPasswordKnown(userId, result.syntheticPassword); onSyntheticPasswordKnown(userId, result.syntheticPassword);
setLockCredentialWithSpLocked(credential, result.syntheticPassword, userId); setLockCredentialWithSpLocked(credential, result.syntheticPassword, userId,
/* isLockTiedToParent= */ false);
return true; return true;
} }

View File

@@ -33,15 +33,18 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.PropertyInvalidatedCache; import android.app.PropertyInvalidatedCache;
import android.os.RemoteException; import android.os.RemoteException;
import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.Presubmit;
import android.service.gatekeeper.GateKeeperResponse; import android.service.gatekeeper.GateKeeperResponse;
import android.text.TextUtils;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4; import androidx.test.runner.AndroidJUnit4;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse; import com.android.internal.widget.VerifyCredentialResponse;
@@ -450,6 +453,45 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
PRIMARY_USER_ID)); PRIMARY_USER_ID));
} }
@Test
public void testPasswordHistoryDisabledByDefault() throws Exception {
final int userId = PRIMARY_USER_ID;
checkPasswordHistoryLength(userId, 0);
initializeStorageWithCredential(userId, nonePassword());
checkPasswordHistoryLength(userId, 0);
assertTrue(mService.setLockCredential(newPassword("1234"), nonePassword(), userId));
checkPasswordHistoryLength(userId, 0);
}
@Test
public void testPasswordHistoryLengthHonored() throws Exception {
final int userId = PRIMARY_USER_ID;
when(mDevicePolicyManager.getPasswordHistoryLength(any(), eq(userId))).thenReturn(3);
checkPasswordHistoryLength(userId, 0);
initializeStorageWithCredential(userId, nonePassword());
checkPasswordHistoryLength(userId, 0);
assertTrue(mService.setLockCredential(newPassword("pass1"), nonePassword(), userId));
checkPasswordHistoryLength(userId, 1);
assertTrue(mService.setLockCredential(newPassword("pass2"), newPassword("pass1"), userId));
checkPasswordHistoryLength(userId, 2);
assertTrue(mService.setLockCredential(newPassword("pass3"), newPassword("pass2"), userId));
checkPasswordHistoryLength(userId, 3);
// maximum length should have been reached
assertTrue(mService.setLockCredential(newPassword("pass4"), newPassword("pass3"), userId));
checkPasswordHistoryLength(userId, 3);
}
private void checkPasswordHistoryLength(int userId, int expectedLen) {
String history = mService.getString(LockPatternUtils.PASSWORD_HISTORY_KEY, "", userId);
String[] hashes = TextUtils.split(history, LockPatternUtils.PASSWORD_HISTORY_DELIMITER);
assertEquals(expectedLen, hashes.length);
}
private void testCreateCredential(int userId, LockscreenCredential credential) private void testCreateCredential(int userId, LockscreenCredential credential)
throws RemoteException { throws RemoteException {
assertTrue(mService.setLockCredential(credential, nonePassword(), userId)); assertTrue(mService.setLockCredential(credential, nonePassword(), userId));
@@ -510,6 +552,10 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
synchronized (mService.mSpManager) { synchronized (mService.mSpManager) {
mService.initializeSyntheticPasswordLocked(credential, userId); mService.initializeSyntheticPasswordLocked(credential, userId);
} }
if (credential.isNone()) {
assertEquals(0, mGateKeeperService.getSecureUserId(userId));
} else {
assertNotEquals(0, mGateKeeperService.getSecureUserId(userId)); assertNotEquals(0, mGateKeeperService.getSecureUserId(userId));
} }
}
} }