Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 03e132f6 authored by Dmitry Dementyev's avatar Dmitry Dementyev Committed by Android (Google) Code Review
Browse files

Merge "Implement validateRemoteLockscreen."

parents 93a81c6c 68a84962
Loading
Loading
Loading
Loading
+97 −12
Original line number Diff line number Diff line
@@ -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() {
+125 −16
Original line number Diff line number Diff line
@@ -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 {