"V1 encrypted_application_key".getBytes(StandardCharsets.UTF_8);
private static final byte[] RECOVERY_CLAIM_HEADER =
"V1 KF_claim".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] RECOVERY_RESPONSE_HEADER =
+ "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
private static final byte[] THM_KF_HASH_PREFIX = "THM_KF_hash".getBytes(StandardCharsets.UTF_8);
}
/**
+ * Decrypts response from recovery claim, returning the locally encrypted key.
+ *
+ * @param keyClaimant The key claimant, used by the remote service to encrypt the response.
+ * @param vaultParams Vault params associated with the claim.
+ * @param encryptedResponse The encrypted response.
+ * @return The locally encrypted recovery key.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is not present.
+ * @throws InvalidKeyException if the {@code keyClaimant} could not be used to decrypt.
+ * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
+ * different key.
+ */
+ public static byte[] decryptRecoveryClaimResponse(
+ byte[] keyClaimant, byte[] vaultParams, byte[] encryptedResponse)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ return SecureBox.decrypt(
+ /*ourPrivateKey=*/ null,
+ /*sharedSecret=*/ keyClaimant,
+ /*header=*/ concat(RECOVERY_RESPONSE_HEADER, vaultParams),
+ /*encryptedPayload=*/ encryptedResponse);
+ }
+
+ /**
* Decrypts a recovery key, after having retrieved it from a remote server.
*
* @param lskfHash The lock screen hash associated with the key.
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
+import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import javax.crypto.AEADBadTagException;
+
/**
* Class with {@link RecoverableKeyStoreLoader} API implementation and internal methods to interact
* with {@code LockSettingsService}.
* @param verifierPublicKey X509-encoded public key.
* @param vaultParams Additional params associated with vault.
* @param vaultChallenge Challenge issued by vault service.
- * @param secrets Lock-screen hashes. Should have a single element. TODO: why is this a list?
+ * @param secrets Lock-screen hashes. For now only a single secret is supported.
* @return Encrypted bytes of recovery claim. This can then be issued to the vault service.
*
* @hide
byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
byte[] kfHash = secrets.get(0).getSecret();
mRecoverySessionStorage.add(
- userId, new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant));
+ userId,
+ new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant, vaultParams));
try {
byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash);
}
}
+ /**
+ * Invoked by a recovery agent after a successful recovery claim is sent to the remote vault
+ * service.
+ *
+ * <p>TODO: should also load into AndroidKeyStore.
+ *
+ * @param sessionId The session ID used to generate the claim. See
+ * {@link #startRecoverySession(String, byte[], byte[], byte[], List, int)}.
+ * @param encryptedRecoveryKey The encrypted recovery key blob returned by the remote vault
+ * service.
+ * @param applicationKeys The encrypted key blobs returned by the remote vault service. These
+ * were wrapped with the recovery key.
+ * @param uid The uid of the recovery agent.
+ * @throws RemoteException if an error occurred recovering the keys.
+ */
public void recoverKeys(
@NonNull String sessionId,
- @NonNull byte[] recoveryKeyBlob,
+ @NonNull byte[] encryptedRecoveryKey,
@NonNull List<KeyEntryRecoveryData> applicationKeys,
- int userId)
+ int uid)
throws RemoteException {
checkRecoverKeyStorePermission();
- throw new UnsupportedOperationException();
+
+ RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId);
+ if (sessionEntry == null) {
+ throw new RemoteException(String.format(Locale.US,
+ "User %d does not have pending session '%s'", uid, sessionId));
+ }
+
+ try {
+ byte[] recoveryKey = decryptRecoveryKey(sessionEntry, encryptedRecoveryKey);
+ recoverApplicationKeys(recoveryKey, applicationKeys);
+ } finally {
+ sessionEntry.destroy();
+ mRecoverySessionStorage.remove(uid);
+ }
+ }
+
+ private byte[] decryptRecoveryKey(
+ RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse)
+ throws RemoteException {
+ try {
+ byte[] locallyEncryptedKey = KeySyncUtils.decryptRecoveryClaimResponse(
+ sessionEntry.getKeyClaimant(),
+ sessionEntry.getVaultParams(),
+ encryptedClaimResponse);
+ return KeySyncUtils.decryptRecoveryKey(sessionEntry.getLskfHash(), locallyEncryptedKey);
+ } catch (InvalidKeyException | AEADBadTagException e) {
+ throw new RemoteException(
+ "Failed to decrypt recovery key",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen: all the algorithms used are required by AOSP implementations
+ throw new RemoteException(
+ "Missing required algorithm",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ }
+ }
+
+ /**
+ * Uses {@code recoveryKey} to decrypt {@code applicationKeys}.
+ *
+ * <p>TODO: and load them into store?
+ *
+ * @throws RemoteException if an error occurred decrypting the keys.
+ */
+ private void recoverApplicationKeys(
+ @NonNull byte[] recoveryKey,
+ @NonNull List<KeyEntryRecoveryData> applicationKeys) throws RemoteException {
+ for (KeyEntryRecoveryData applicationKey : applicationKeys) {
+ String alias = new String(applicationKey.getAlias(), StandardCharsets.UTF_8);
+ byte[] encryptedKeyMaterial = applicationKey.getEncryptedKeyMaterial();
+
+ try {
+ // TODO: put decrypted key material in appropriate AndroidKeyStore
+ KeySyncUtils.decryptApplicationKey(recoveryKey, encryptedKeyMaterial);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen: all the algorithms used are required by AOSP implementations
+ throw new RemoteException(
+ "Missing required algorithm",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ } catch (InvalidKeyException | AEADBadTagException e) {
+ throw new RemoteException(
+ "Failed to recover key with alias '" + alias + "'",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ }
+ }
}
/**
* @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported
* @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms
* @throws AEADBadTagException if the authentication tag contained in {@code encryptedPayload}
- * cannot be validated
+ * cannot be validated, or if the payload is not a valid SecureBox V2 payload.
* @hide
*/
public static byte[] decrypt(
throw new IllegalArgumentException("Both the private key and shared secret are empty");
}
header = emptyByteArrayIfNull(header);
- encryptedPayload = emptyByteArrayIfNull(encryptedPayload);
+ if (encryptedPayload == null) {
+ throw new NullPointerException("Encrypted payload must not be null.");
+ }
ByteBuffer ciphertextBuffer = ByteBuffer.wrap(encryptedPayload);
byte[] version = readEncryptedPayload(ciphertextBuffer, VERSION.length);
if (!Arrays.equals(version, VERSION)) {
- throw new IllegalArgumentException("The payload was not encrypted by SecureBox v2");
+ throw new AEADBadTagException("The payload was not encrypted by SecureBox v2");
}
byte[] senderPublicKeyBytes;
return aesGcmDecrypt(decryptionKey, randNonce, ciphertext, header);
}
- private static byte[] readEncryptedPayload(ByteBuffer buffer, int length) {
+ private static byte[] readEncryptedPayload(ByteBuffer buffer, int length)
+ throws AEADBadTagException {
byte[] output = new byte[length];
try {
buffer.get(output);
} catch (BufferUnderflowException ex) {
- throw new IllegalArgumentException("The encrypted payload is too short");
+ throw new AEADBadTagException("The encrypted payload is too short");
}
return output;
}
public static class Entry implements Destroyable {
private final byte[] mLskfHash;
private final byte[] mKeyClaimant;
+ private final byte[] mVaultParams;
private final String mSessionId;
/**
* @hide
*/
- public Entry(String sessionId, byte[] lskfHash, byte[] keyClaimant) {
- this.mLskfHash = lskfHash;
- this.mSessionId = sessionId;
- this.mKeyClaimant = keyClaimant;
+ public Entry(String sessionId, byte[] lskfHash, byte[] keyClaimant, byte[] vaultParams) {
+ mLskfHash = lskfHash;
+ mSessionId = sessionId;
+ mKeyClaimant = keyClaimant;
+ mVaultParams = vaultParams;
}
/**
}
/**
+ * Returns the vault params associated with the session.
+ *
+ * @hide
+ */
+ public byte[] getVaultParams() {
+ return mVaultParams;
+ }
+
+ /**
* Overwrites the memory for the lskf hash and key claimant.
*
* @hide
utf8Bytes("snQzsbvclkSsG6PwasAp1oFLzbq3KtFe");
private static final byte[] RECOVERY_CLAIM_HEADER =
"V1 KF_claim".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] RECOVERY_RESPONSE_HEADER =
+ "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
@Test
public void calculateThmKfHash_isShaOfLockScreenHashWithPrefix() throws Exception {
}
@Test
+ public void decryptRecoveryClaimResponse_decryptsAValidResponse() throws Exception {
+ byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
+ byte[] vaultParams = randomBytes(100);
+ byte[] recoveryKey = randomBytes(32);
+ byte[] encryptedPayload = SecureBox.encrypt(
+ /*theirPublicKey=*/ null,
+ /*sharedSecret=*/ keyClaimant,
+ /*header=*/ KeySyncUtils.concat(RECOVERY_RESPONSE_HEADER, vaultParams),
+ /*payload=*/ recoveryKey);
+
+ byte[] decrypted = KeySyncUtils.decryptRecoveryClaimResponse(
+ keyClaimant, vaultParams, encryptedPayload);
+
+ assertArrayEquals(recoveryKey, decrypted);
+ }
+
+ @Test
+ public void decryptRecoveryClaimResponse_throwsIfCannotDecrypt() throws Exception {
+ byte[] vaultParams = randomBytes(100);
+ byte[] recoveryKey = randomBytes(32);
+ byte[] encryptedPayload = SecureBox.encrypt(
+ /*theirPublicKey=*/ null,
+ /*sharedSecret=*/ KeySyncUtils.generateKeyClaimant(),
+ /*header=*/ KeySyncUtils.concat(RECOVERY_RESPONSE_HEADER, vaultParams),
+ /*payload=*/ recoveryKey);
+
+ try {
+ KeySyncUtils.decryptRecoveryClaimResponse(
+ KeySyncUtils.generateKeyClaimant(), vaultParams, encryptedPayload);
+ fail("Did not throw decrypting with bad keyClaimant");
+ } catch (AEADBadTagException error) {
+ // expected
+ }
+ }
+
+ @Test
public void encryptRecoveryClaim_encryptsLockScreenAndKeyClaimant() throws Exception {
KeyPair keyPair = SecureBox.genKeyPair();
byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import android.content.Context;
import android.os.RemoteException;
import android.security.recoverablekeystore.KeyDerivationParameters;
+import android.security.recoverablekeystore.KeyEntryRecoveryData;
import android.security.recoverablekeystore.KeyStoreRecoveryMetadata;
import android.security.recoverablekeystore.RecoverableKeyStoreLoader;
import android.support.test.InstrumentationRegistry;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import org.junit.After;
import org.junit.Before;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
+import java.util.Random;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
@SmallTest
@RunWith(AndroidJUnit4.class)
private static final byte[] TEST_VAULT_PARAMS = getUtf8Bytes("vault_params");
private static final int TEST_USER_ID = 10009;
private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;
+ private static final byte[] RECOVERY_RESPONSE_HEADER =
+ "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
+ private static final String TEST_ALIAS = "nick";
@Mock private Context mMockContext;
TEST_VAULT_CHALLENGE,
ImmutableList.of(),
TEST_USER_ID);
+ fail("should have thrown");
} catch (RemoteException e) {
assertEquals("Only a single KeyStoreRecoveryMetadata is supported",
e.getMessage());
KeyDerivationParameters.createSHA256Parameters(TEST_SALT),
TEST_SECRET)),
TEST_USER_ID);
+ fail("should have thrown");
} catch (RemoteException e) {
assertEquals("Not a valid X509 key",
e.getMessage());
}
}
+ @Test
+ public void recoverKeys_throwsIfNoSessionIsPresent() throws Exception {
+ try {
+ mRecoverableKeyStoreManager.recoverKeys(
+ TEST_SESSION_ID,
+ /*recoveryKeyBlob=*/ randomBytes(32),
+ /*applicationKeys=*/ ImmutableList.of(
+ new KeyEntryRecoveryData(getUtf8Bytes("alias"), randomBytes(32))
+ ),
+ TEST_USER_ID);
+ fail("should have thrown");
+ } catch (RemoteException e) {
+ assertEquals("User 10009 does not have pending session 'karlin'",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void recoverKeys_throwsIfRecoveryClaimCannotBeDecrypted() throws Exception {
+ mRecoverableKeyStoreManager.startRecoverySession(
+ TEST_SESSION_ID,
+ TEST_PUBLIC_KEY,
+ TEST_VAULT_PARAMS,
+ TEST_VAULT_CHALLENGE,
+ ImmutableList.of(new KeyStoreRecoveryMetadata(
+ TYPE_LOCKSCREEN,
+ TYPE_PASSWORD,
+ KeyDerivationParameters.createSHA256Parameters(TEST_SALT),
+ TEST_SECRET)),
+ TEST_USER_ID);
+
+ try {
+ mRecoverableKeyStoreManager.recoverKeys(
+ TEST_SESSION_ID,
+ /*encryptedRecoveryKey=*/ randomBytes(60),
+ /*applicationKeys=*/ ImmutableList.of(),
+ /*uid=*/ TEST_USER_ID);
+ fail("should have thrown");
+ } catch (RemoteException e) {
+ assertEquals("Failed to decrypt recovery key", e.getMessage());
+ }
+ }
+
+ @Test
+ public void recoverKeys_throwsIfFailedToDecryptAnApplicationKey() throws Exception {
+ mRecoverableKeyStoreManager.startRecoverySession(
+ TEST_SESSION_ID,
+ TEST_PUBLIC_KEY,
+ TEST_VAULT_PARAMS,
+ TEST_VAULT_CHALLENGE,
+ ImmutableList.of(new KeyStoreRecoveryMetadata(
+ TYPE_LOCKSCREEN,
+ TYPE_PASSWORD,
+ KeyDerivationParameters.createSHA256Parameters(TEST_SALT),
+ TEST_SECRET)),
+ TEST_USER_ID);
+ byte[] keyClaimant = mRecoverySessionStorage.get(TEST_USER_ID, TEST_SESSION_ID)
+ .getKeyClaimant();
+ SecretKey recoveryKey = randomRecoveryKey();
+ byte[] encryptedClaimResponse = encryptClaimResponse(
+ keyClaimant, TEST_SECRET, TEST_VAULT_PARAMS, recoveryKey);
+ KeyEntryRecoveryData badApplicationKey = new KeyEntryRecoveryData(
+ TEST_ALIAS.getBytes(StandardCharsets.UTF_8),
+ randomBytes(32));
+
+ try {
+ mRecoverableKeyStoreManager.recoverKeys(
+ TEST_SESSION_ID,
+ /*encryptedRecoveryKey=*/ encryptedClaimResponse,
+ /*applicationKeys=*/ ImmutableList.of(badApplicationKey),
+ /*uid=*/ TEST_USER_ID);
+ fail("should have thrown");
+ } catch (RemoteException e) {
+ assertEquals("Failed to recover key with alias 'nick'", e.getMessage());
+ }
+ }
+
+ @Test
+ public void recoverKeys_doesNotThrowIfAllIsOk() throws Exception {
+ mRecoverableKeyStoreManager.startRecoverySession(
+ TEST_SESSION_ID,
+ TEST_PUBLIC_KEY,
+ TEST_VAULT_PARAMS,
+ TEST_VAULT_CHALLENGE,
+ ImmutableList.of(new KeyStoreRecoveryMetadata(
+ TYPE_LOCKSCREEN,
+ TYPE_PASSWORD,
+ KeyDerivationParameters.createSHA256Parameters(TEST_SALT),
+ TEST_SECRET)),
+ TEST_USER_ID);
+ byte[] keyClaimant = mRecoverySessionStorage.get(TEST_USER_ID, TEST_SESSION_ID)
+ .getKeyClaimant();
+ SecretKey recoveryKey = randomRecoveryKey();
+ byte[] encryptedClaimResponse = encryptClaimResponse(
+ keyClaimant, TEST_SECRET, TEST_VAULT_PARAMS, recoveryKey);
+ KeyEntryRecoveryData applicationKey = new KeyEntryRecoveryData(
+ TEST_ALIAS.getBytes(StandardCharsets.UTF_8),
+ randomEncryptedApplicationKey(recoveryKey)
+ );
+
+ mRecoverableKeyStoreManager.recoverKeys(
+ TEST_SESSION_ID,
+ encryptedClaimResponse,
+ ImmutableList.of(applicationKey),
+ TEST_USER_ID);
+ }
+
+ private static byte[] randomEncryptedApplicationKey(SecretKey recoveryKey) throws Exception {
+ return KeySyncUtils.encryptKeysWithRecoveryKey(recoveryKey, ImmutableMap.of(
+ "alias", new SecretKeySpec(randomBytes(32), "AES")
+ )).get("alias");
+ }
+
+ private static byte[] encryptClaimResponse(
+ byte[] keyClaimant,
+ byte[] lskfHash,
+ byte[] vaultParams,
+ SecretKey recoveryKey) throws Exception {
+ byte[] locallyEncryptedRecoveryKey = KeySyncUtils.locallyEncryptRecoveryKey(
+ lskfHash, recoveryKey);
+ return SecureBox.encrypt(
+ /*theirPublicKey=*/ null,
+ /*sharedSecret=*/ keyClaimant,
+ /*header=*/ KeySyncUtils.concat(RECOVERY_RESPONSE_HEADER, vaultParams),
+ /*payload=*/ locallyEncryptedRecoveryKey);
+ }
+
+ private static SecretKey randomRecoveryKey() {
+ return new SecretKeySpec(randomBytes(32), "AES");
+ }
+
private static byte[] getUtf8Bytes(String s) {
return s.getBytes(StandardCharsets.UTF_8);
}
+
+ private static byte[] randomBytes(int n) {
+ byte[] bytes = new byte[n];
+ new Random().nextBytes(bytes);
+ return bytes;
+ }
}
@Test
public void decrypt_nullEncryptedPayload() throws Exception {
- IllegalArgumentException expected =
+ NullPointerException expected =
expectThrows(
- IllegalArgumentException.class,
+ NullPointerException.class,
() ->
SecureBox.decrypt(
THM_PRIVATE_KEY,
private static final int TEST_USER_ID = 696;
private static final byte[] TEST_LSKF_HASH = getUtf8Bytes("lskf");
private static final byte[] TEST_KEY_CLAIMANT = getUtf8Bytes("0000111122223333");
+ private static final byte[] TEST_VAULT_PARAMS = getUtf8Bytes("vault params vault params");
@Test
public void size_isZeroForEmpty() {
public void size_incrementsAfterAdd() {
RecoverySessionStorage storage = new RecoverySessionStorage();
storage.add(TEST_USER_ID, new RecoverySessionStorage.Entry(
- TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture()));
+ TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture(), vaultParamsFixture()));
assertEquals(1, storage.size());
}
public void size_decrementsAfterRemove() {
RecoverySessionStorage storage = new RecoverySessionStorage();
storage.add(TEST_USER_ID, new RecoverySessionStorage.Entry(
- TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture()));
+ TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture(), vaultParamsFixture()));
storage.remove(TEST_USER_ID);
assertEquals(0, storage.size());
public void remove_overwritesLskfHashMemory() {
RecoverySessionStorage storage = new RecoverySessionStorage();
RecoverySessionStorage.Entry entry = new RecoverySessionStorage.Entry(
- TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture());
+ TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture(), vaultParamsFixture());
storage.add(TEST_USER_ID, entry);
storage.remove(TEST_USER_ID);
public void remove_overwritesKeyClaimantMemory() {
RecoverySessionStorage storage = new RecoverySessionStorage();
RecoverySessionStorage.Entry entry = new RecoverySessionStorage.Entry(
- TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture());
+ TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture(), vaultParamsFixture());
storage.add(TEST_USER_ID, entry);
storage.remove(TEST_USER_ID);
public void destroy_overwritesLskfHashMemory() {
RecoverySessionStorage storage = new RecoverySessionStorage();
RecoverySessionStorage.Entry entry = new RecoverySessionStorage.Entry(
- TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture());
+ TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture(), vaultParamsFixture());
storage.add(TEST_USER_ID, entry);
storage.destroy();
public void destroy_overwritesKeyClaimantMemory() {
RecoverySessionStorage storage = new RecoverySessionStorage();
RecoverySessionStorage.Entry entry = new RecoverySessionStorage.Entry(
- TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture());
+ TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture(), vaultParamsFixture());
storage.add(TEST_USER_ID, entry);
storage.destroy();
return Arrays.copyOf(TEST_KEY_CLAIMANT, TEST_KEY_CLAIMANT.length);
}
+ private static byte[] vaultParamsFixture() {
+ return Arrays.copyOf(TEST_VAULT_PARAMS, TEST_VAULT_PARAMS.length);
+ }
+
private static byte[] getUtf8Bytes(String s) {
return s.getBytes(StandardCharsets.UTF_8);
}