Merge "Implement validateRemoteLockscreen."
This commit is contained in:
committed by
Android (Google) Code Review
commit
03e132f634
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user