diff options
author | 2023-02-07 07:48:22 +0000 | |
---|---|---|
committer | 2023-02-07 07:48:22 +0000 | |
commit | 03e132f6348281ddedac359c863a47d8fd9459a5 (patch) | |
tree | 4abdcfc7199dc794646050ad69d0b8b09434757f | |
parent | 93a81c6c5fe711086c89c44731899c0a1bf4eb9b (diff) | |
parent | 68a84962bc12a6320453f99a7410563e113041c7 (diff) |
Merge "Implement validateRemoteLockscreen."
2 files changed, 222 insertions, 28 deletions
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java index d9815699e726..33dc7efe0a9b 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java @@ -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() { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java index b9adc3042d04..32cb8c4e0a91 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java @@ -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(); + 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(request.getRemainingAttempts()).isEqualTo(0); assertThat(credetialsType).isEqualTo(KeyguardManager.PASSWORD); - mRemoteLockscreenValidationSessionStorage.finishSession(mUserId); } - @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 { |