OSDN Git Service

Implement recoverKeys
authorRobert Berry <robertberry@google.com>
Thu, 21 Dec 2017 12:41:01 +0000 (12:41 +0000)
committerRobert Berry <robertberry@google.com>
Fri, 22 Dec 2017 00:05:38 +0000 (00:05 +0000)
This implements all of recoverKeys, except for loading keys into the
AndroidKeyStore. Also omitting re-enrolling keys into the recoverable
store for now, as it is not clear whether the user will have a lock
screen set at this point. If they do not have a lock screen set, we
cannot re-enroll keys, as the platform-decrypt key is bound to the
lock screen. Also modifies SecureBox to throw AEADBadTagException for
any issues with the encrypted payload. IllegalArgumentException is
a runtime exception, so would be unexpected, but might occur if the
encrypted payload is for some reason garbage. Also, throw NPE if the
payload is null, as that is a programmer error - not something that
should ever occur at runtime.

Test: adb shell am instrument -w -e package com.android.server.locksettings.recoverablekeystore com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
Change-Id: I4f0be412c3044f3472a6aed514f1caf54b7ee41f

services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java
services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/SecureBoxTest.java
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorageTest.java

index b390fe8..4597fad 100644 (file)
@@ -54,6 +54,8 @@ public class KeySyncUtils {
             "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);
 
@@ -204,6 +206,28 @@ public class KeySyncUtils {
     }
 
     /**
+     * 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.
index c089b40..cfeaaf8 100644 (file)
@@ -35,6 +35,7 @@ import com.android.internal.annotations.VisibleForTesting;
 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;
@@ -42,10 +43,13 @@ import java.security.PublicKey;
 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}.
@@ -225,7 +229,7 @@ public class RecoverableKeyStoreManager {
      * @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
@@ -248,7 +252,8 @@ public class RecoverableKeyStoreManager {
         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);
@@ -275,14 +280,101 @@ public class RecoverableKeyStoreManager {
         }
     }
 
+    /**
+     * 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);
+            }
+        }
     }
 
     /**
index d8a2d31..801d4de 100644 (file)
@@ -230,7 +230,7 @@ public class SecureBox {
      * @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(
@@ -244,12 +244,14 @@ public class SecureBox {
             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;
@@ -271,12 +273,13 @@ public class SecureBox {
         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;
     }
index bc56ae1..f7633e4 100644 (file)
@@ -129,15 +129,17 @@ public class RecoverySessionStorage implements Destroyable {
     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;
         }
 
         /**
@@ -160,6 +162,15 @@ public class RecoverySessionStorage implements Destroyable {
         }
 
         /**
+         * 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
index c328dda..6254d52 100644 (file)
@@ -55,6 +55,8 @@ public class KeySyncUtilsTest {
             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 {
@@ -173,6 +175,42 @@ public class KeySyncUtilsTest {
     }
 
     @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();
index 56a23de..fb2d341 100644 (file)
@@ -21,6 +21,7 @@ import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE
 
 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;
@@ -29,6 +30,7 @@ import static org.mockito.Mockito.verify;
 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;
@@ -39,6 +41,7 @@ import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKe
 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;
@@ -50,6 +53,10 @@ import org.mockito.MockitoAnnotations;
 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)
@@ -77,6 +84,9 @@ public class RecoverableKeyStoreManagerTest {
     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;
 
@@ -156,6 +166,7 @@ public class RecoverableKeyStoreManagerTest {
                     TEST_VAULT_CHALLENGE,
                     ImmutableList.of(),
                     TEST_USER_ID);
+            fail("should have thrown");
         } catch (RemoteException e) {
             assertEquals("Only a single KeyStoreRecoveryMetadata is supported",
                     e.getMessage());
@@ -176,13 +187,151 @@ public class RecoverableKeyStoreManagerTest {
                             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;
+    }
 }
index 72b69f0..35ec23b 100644 (file)
@@ -274,9 +274,9 @@ public class SecureBoxTest {
 
     @Test
     public void decrypt_nullEncryptedPayload() throws Exception {
-        IllegalArgumentException expected =
+        NullPointerException expected =
                 expectThrows(
-                        IllegalArgumentException.class,
+                        NullPointerException.class,
                         () ->
                                 SecureBox.decrypt(
                                         THM_PRIVATE_KEY,
index 6aeff28..6f93fe4 100644 (file)
@@ -37,6 +37,7 @@ public class RecoverySessionStorageTest {
     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() {
@@ -47,7 +48,7 @@ public class RecoverySessionStorageTest {
     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());
     }
@@ -56,7 +57,7 @@ public class RecoverySessionStorageTest {
     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());
@@ -66,7 +67,7 @@ public class RecoverySessionStorageTest {
     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);
@@ -78,7 +79,7 @@ public class RecoverySessionStorageTest {
     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);
@@ -90,7 +91,7 @@ public class RecoverySessionStorageTest {
     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();
@@ -102,7 +103,7 @@ public class RecoverySessionStorageTest {
     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();
@@ -126,6 +127,10 @@ public class RecoverySessionStorageTest {
         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);
     }