Merge "Implement validateRemoteLockscreen."

This commit is contained in:
Dmitry Dementyev
2023-02-07 07:48:22 +00:00
committed by Android (Google) Code Review
2 changed files with 224 additions and 30 deletions

View File

@@ -49,6 +49,9 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.HexDump;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternView;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.security.SecureBox;
import com.android.server.locksettings.LockSettingsService;
import com.android.server.locksettings.recoverablekeystore.certificate.CertParsingException;
@@ -65,6 +68,7 @@ import com.android.server.locksettings.recoverablekeystore.storage.RemoteLockscr
import com.android.server.locksettings.recoverablekeystore.storage.RemoteLockscreenValidationSessionStorage.LockscreenVerificationSession;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -97,6 +101,9 @@ import javax.crypto.AEADBadTagException;
public class RecoverableKeyStoreManager {
private static final String TAG = "RecoverableKeyStoreMgr";
private static final long SYNC_DELAY_MILLIS = 2000;
private static final int INVALID_REMOTE_GUESS_LIMIT = 5;
public static final byte[] ENCRYPTED_REMOTE_CREDENTIALS_HEADER =
"encrypted_remote_credentials".getBytes(StandardCharsets.UTF_8);
private static RecoverableKeyStoreManager mInstance;
@@ -995,7 +1002,7 @@ public class RecoverableKeyStoreManager {
* Starts a session to verify lock screen credentials provided by a remote device.
*/
public StartLockscreenValidationRequest startRemoteLockscreenValidation(
LockSettingsService lockSettingService) {
LockSettingsService lockSettingsService) {
if (mRemoteLockscreenValidationSessionStorage == null) {
throw new UnsupportedOperationException("Under development");
}
@@ -1004,40 +1011,118 @@ public class RecoverableKeyStoreManager {
int savedCredentialType;
final long token = Binder.clearCallingIdentity();
try {
savedCredentialType = lockSettingService.getCredentialType(userId);
savedCredentialType = lockSettingsService.getCredentialType(userId);
} finally {
Binder.restoreCallingIdentity(token);
}
int keyguardCredentailsType = lockPatternUtilsToKeyguardType(savedCredentialType);
int keyguardCredentialsType = lockPatternUtilsToKeyguardType(savedCredentialType);
LockscreenVerificationSession session =
mRemoteLockscreenValidationSessionStorage.startSession(userId);
PublicKey publicKey = session.getKeyPair().getPublic();
byte[] encodedPublicKey = SecureBox.encodePublicKey(publicKey);
int badGuesses = mDatabase.getBadRemoteGuessCounter(userId);
int remainingAttempts = Math.max(INVALID_REMOTE_GUESS_LIMIT - badGuesses, 0);
// TODO(b/254335492): Schedule task to remove inactive session
return new StartLockscreenValidationRequest.Builder()
.setLockscreenUiType(keyguardCredentailsType)
.setSourcePublicKey(new byte[]{})
.setLockscreenUiType(keyguardCredentialsType)
.setRemainingAttempts(remainingAttempts)
.setSourcePublicKey(encodedPublicKey)
.build();
}
/**
* Verifies encrypted credentials guess from a remote device.
*/
public RemoteLockscreenValidationResult validateRemoteLockscreen(
public synchronized RemoteLockscreenValidationResult validateRemoteLockscreen(
@NonNull byte[] encryptedCredential,
LockSettingsService lockSettingService) {
if (mRemoteLockscreenValidationSessionStorage == null) {
throw new UnsupportedOperationException("Under development");
}
LockSettingsService lockSettingsService) {
checkVerifyRemoteLockscreenPermission();
int userId = UserHandle.getCallingUserId();
LockscreenVerificationSession session =
mRemoteLockscreenValidationSessionStorage.get(userId);
int badGuesses = mDatabase.getBadRemoteGuessCounter(userId);
int remainingAttempts = INVALID_REMOTE_GUESS_LIMIT - badGuesses;
if (remainingAttempts <= 0) {
return new RemoteLockscreenValidationResult.Builder()
.setResultCode(RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS)
.build();
}
if (session == null) {
throw new IllegalStateException("There is no active lock screen check session");
}
// TODO(b/254335492): Call lockSettingService.verifyCredential
return new RemoteLockscreenValidationResult.Builder().build();
byte[] decryptedCredentials;
try {
decryptedCredentials = SecureBox.decrypt(
session.getKeyPair().getPrivate(),
/* sharedSecret= */ null,
ENCRYPTED_REMOTE_CREDENTIALS_HEADER,
encryptedCredential);
} catch (NoSuchAlgorithmException e) {
Log.wtf(TAG, "Missing SecureBox algorithm. AOSP required to support this.", e);
throw new IllegalStateException(e);
} catch (InvalidKeyException e) {
Log.e(TAG, "Got InvalidKeyException during lock screen credentials decryption");
throw new IllegalStateException(e);
} catch (AEADBadTagException e) {
throw new IllegalStateException("Could not decrypt credentials guess", e);
}
int savedCredentialType;
final long token = Binder.clearCallingIdentity();
try {
savedCredentialType = lockSettingsService.getCredentialType(userId);
int keyguardCredentialsType = lockPatternUtilsToKeyguardType(savedCredentialType);
try (LockscreenCredential credential =
createLockscreenCredential(keyguardCredentialsType, decryptedCredentials)) {
// TODO(b/254335492): remove decryptedCredentials
VerifyCredentialResponse verifyResponse =
lockSettingsService.verifyCredential(credential, userId, 0);
return handleVerifyCredentialResponse(verifyResponse, userId);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
private RemoteLockscreenValidationResult handleVerifyCredentialResponse(
VerifyCredentialResponse response, int userId) {
if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
mDatabase.setBadRemoteGuessCounter(userId, 0);
mRemoteLockscreenValidationSessionStorage.finishSession(userId);
return new RemoteLockscreenValidationResult.Builder()
.setResultCode(RemoteLockscreenValidationResult.RESULT_GUESS_VALID)
.build();
}
if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
long timeout = (long) response.getTimeout();
return new RemoteLockscreenValidationResult.Builder()
.setResultCode(RemoteLockscreenValidationResult.RESULT_LOCKOUT)
.setTimeoutMillis(timeout)
.build();
}
// Invalid guess
int badGuesses = mDatabase.getBadRemoteGuessCounter(userId);
mDatabase.setBadRemoteGuessCounter(userId, badGuesses + 1);
return new RemoteLockscreenValidationResult.Builder()
.setResultCode(RemoteLockscreenValidationResult.RESULT_GUESS_INVALID)
.build();
}
private LockscreenCredential createLockscreenCredential(
int lockType, byte[] password) {
switch (lockType) {
case KeyguardManager.PASSWORD:
CharSequence passwordStr = new String(password, StandardCharsets.UTF_8);
return LockscreenCredential.createPassword(passwordStr);
case KeyguardManager.PIN:
CharSequence pinStr = new String(password);
return LockscreenCredential.createPin(pinStr);
case KeyguardManager.PATTERN:
List<LockPatternView.Cell> pattern =
LockPatternUtils.byteArrayToPattern(password);
return LockscreenCredential.createPattern(pattern);
default:
throw new IllegalStateException("Lockscreen is not set");
}
}
private void checkVerifyRemoteLockscreenPermission() {

View File

@@ -40,6 +40,7 @@ import static org.mockito.Mockito.when;
import android.Manifest;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.app.RemoteLockscreenValidationResult;
import android.app.StartLockscreenValidationRequest;
import android.content.Context;
import android.content.Intent;
@@ -61,6 +62,8 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.ArrayUtils;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.security.SecureBox;
import com.android.server.locksettings.LockSettingsService;
import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
@@ -84,10 +87,12 @@ import org.mockito.Spy;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.cert.CertPath;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ScheduledExecutorService;
@@ -155,6 +160,10 @@ public class RecoverableKeyStoreManagerTest {
.setKeyDerivationParams(KeyDerivationParams.createSha256Params(TEST_SALT))
.setSecret(TEST_SECRET)
.build();
private static final byte[] VALID_GUESS = getUtf8Bytes("password123");
private static final byte[] INVALID_GUESS = getUtf8Bytes("not_password");
private static final byte[] GUESS_LOCKOUT = getUtf8Bytes("need_to_wait");
private static final int TIMEOUT_MILLIS = 30 * 1000;
@Mock private Context mMockContext;
@Mock private RecoverySnapshotListenersStorage mMockListenersStorage;
@@ -208,10 +217,20 @@ public class RecoverableKeyStoreManagerTest {
mTestOnlyInsecureCertificateHelper,
mCleanupManager,
mRemoteLockscreenValidationSessionStorage);
when(mLockSettingsService.verifyCredential(
any(LockscreenCredential.class), anyInt(), anyInt())).thenAnswer(args -> {
LockscreenCredential argument = (LockscreenCredential) args.getArguments()[0];
if (Arrays.equals(argument.getCredential(), VALID_GUESS)) {
return VerifyCredentialResponse.OK;
} else if (Arrays.equals(argument.getCredential(), INVALID_GUESS)) {
return VerifyCredentialResponse.ERROR;
} else return VerifyCredentialResponse.fromTimeout(TIMEOUT_MILLIS);
});
}
@After
public void tearDown() {
mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
mRecoverableKeyStoreDb.close();
mDatabaseFile.delete();
}
@@ -1289,73 +1308,163 @@ public class RecoverableKeyStoreManagerTest {
assertThat(e.getMessage()).contains("not set");
}
verify(mLockSettingsService).getCredentialType(mUserId);
mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
}
@Test
public void startRemoteLockscreenValidation_checksPermission() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PIN);
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
// TODO(b/254335492): Check new system permission
verify(mMockContext, times(1))
.enforceCallingOrSelfPermission(
eq(Manifest.permission.RECOVER_KEYSTORE), any());
mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
}
@Test
public void startRemoteLockscreenValidation_returnsCredentailsType() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PIN);
StartLockscreenValidationRequest request =
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
int credetialsType = request.getLockscreenUiType();
assertThat(credetialsType).isEqualTo(KeyguardManager.PIN);
assertThat(request.getRemainingAttempts()).isEqualTo(5);
verify(mLockSettingsService).getCredentialType(anyInt());
mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
}
@Test
public void startRemoteLockscreenValidation_returnsRemainingAttempts() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PATTERN);
mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 3);
StartLockscreenValidationRequest request =
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
int credetialsType = request.getLockscreenUiType();
assertThat(credetialsType).isEqualTo(KeyguardManager.PATTERN);
// TODO(b/254335492): Verify returned value
mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
assertThat(request.getRemainingAttempts()).isEqualTo(2);
}
@Test
public void startRemoteLockscreenValidation_password() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 3);
mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 7);
StartLockscreenValidationRequest request =
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
int credetialsType = request.getLockscreenUiType();
assertThat(credetialsType).isEqualTo(KeyguardManager.PASSWORD);
mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
}
int credetialsType = request.getLockscreenUiType();
assertThat(request.getRemainingAttempts()).isEqualTo(0);
assertThat(credetialsType).isEqualTo(KeyguardManager.PASSWORD);
}
@Test
public void validateRemoteLockscreen_noActiveSession() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_NONE);
byte[] invalidGuess = new byte[]{1, 2, 3};
try {
mRecoverableKeyStoreManager.validateRemoteLockscreen(invalidGuess,
mRecoverableKeyStoreManager.validateRemoteLockscreen(INVALID_GUESS,
mLockSettingsService);
fail("should have thrown");
} catch (IllegalStateException e) {
assertThat(e.getMessage()).contains("session");
}
}
@Test
public void validateRemoteLockscreen_decryptionError() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 4);
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
try {
mRecoverableKeyStoreManager.validateRemoteLockscreen(
new byte[] {1, 2, 3},
mLockSettingsService);
fail("should have thrown");
} catch (IllegalStateException e) {
// Decryption error
}
}
@Test
public void validateRemoteLockscreen_zeroRemainingAttempts() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 5);
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
RemoteLockscreenValidationResult result =
mRecoverableKeyStoreManager.validateRemoteLockscreen(
encryptCredentialsForNewSession(VALID_GUESS),
mLockSettingsService);
assertThat(result.getResultCode()).isEqualTo(
RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS);
}
@Test
public void validateRemoteLockscreen_guessValid() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 4);
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
RemoteLockscreenValidationResult result =
mRecoverableKeyStoreManager.validateRemoteLockscreen(
encryptCredentialsForNewSession(VALID_GUESS),
mLockSettingsService);
assertThat(result.getResultCode()).isEqualTo(
RemoteLockscreenValidationResult.RESULT_GUESS_VALID);
// Valid guess resets counter
assertThat(mRecoverableKeyStoreDb.getBadRemoteGuessCounter(mUserId)).isEqualTo(0);
}
@Test
public void validateRemoteLockscreen_timeout() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 4);
RemoteLockscreenValidationResult result =
mRecoverableKeyStoreManager.validateRemoteLockscreen(
encryptCredentialsForNewSession(GUESS_LOCKOUT),
mLockSettingsService);
assertThat(result.getResultCode()).isEqualTo(
RemoteLockscreenValidationResult.RESULT_LOCKOUT);
assertThat(result.getTimeoutMillis()).isEqualTo((long) TIMEOUT_MILLIS);
// Counter was not changed
assertThat(mRecoverableKeyStoreDb.getBadRemoteGuessCounter(mUserId)).isEqualTo(4);
}
@Test
public void validateRemoteLockscreen_guessInvalid() throws Exception {
when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 4);
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
RemoteLockscreenValidationResult result =
mRecoverableKeyStoreManager.validateRemoteLockscreen(
encryptCredentialsForNewSession(INVALID_GUESS),
mLockSettingsService);
assertThat(result.getResultCode()).isEqualTo(
RemoteLockscreenValidationResult.RESULT_GUESS_INVALID);
assertThat(mRecoverableKeyStoreDb.getBadRemoteGuessCounter(mUserId)).isEqualTo(5);
}
private byte[] encryptCredentialsForNewSession(byte[] credentials) throws Exception {
StartLockscreenValidationRequest request =
mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
PublicKey publicKey = SecureBox.decodePublicKey(request.getSourcePublicKey());
return SecureBox.encrypt(
publicKey,
/* sharedSecret= */ null,
RecoverableKeyStoreManager.ENCRYPTED_REMOTE_CREDENTIALS_HEADER,
credentials);
}
private static byte[] encryptedApplicationKey(
SecretKey recoveryKey, byte[] applicationKey) throws Exception {