diff --git a/core/java/android/security/recoverablekeystore/KeyDerivationParameters.java b/core/java/android/security/recoverablekeystore/KeyDerivationParameters.java index 2205c416921de..978e60eec3a5a 100644 --- a/core/java/android/security/recoverablekeystore/KeyDerivationParameters.java +++ b/core/java/android/security/recoverablekeystore/KeyDerivationParameters.java @@ -60,7 +60,7 @@ public final class KeyDerivationParameters implements Parcelable { /** * Creates instance of the class to to derive key using salted SHA256 hash. */ - public KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) { + public static KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) { return new KeyDerivationParameters(ALGORITHM_SHA256, salt); } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java index 37aeb3af051e4..25428e7a6028f 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java @@ -20,10 +20,13 @@ import com.android.internal.annotations.VisibleForTesting; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; +import java.security.KeyFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; import java.util.HashMap; import java.util.Map; @@ -39,6 +42,7 @@ import javax.crypto.SecretKey; */ public class KeySyncUtils { + private static final String PUBLIC_KEY_FACTORY_ALGORITHM = "EC"; private static final String RECOVERY_KEY_ALGORITHM = "AES"; private static final int RECOVERY_KEY_SIZE_BITS = 256; @@ -236,6 +240,21 @@ public class KeySyncUtils { /*encryptedPayload=*/ encryptedApplicationKey); } + /** + * Deserializes a X509 public key. + * + * @param key The bytes of the key. + * @return The key. + * @throws NoSuchAlgorithmException if the public key algorithm is unavailable. + * @throws InvalidKeySpecException if the bytes of the key are not a valid key. + */ + public static PublicKey deserializePublicKey(byte[] key) + throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory keyFactory = KeyFactory.getInstance(PUBLIC_KEY_FACTORY_ALGORITHM); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(key); + return keyFactory.generatePublic(publicKeySpec); + } + /** * Returns the concatenation of all the given {@code arrays}. */ 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 e459f288b4c0e..74d132fd15544 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java @@ -30,7 +30,13 @@ import android.security.recoverablekeystore.KeyStoreRecoveryMetadata; import android.security.recoverablekeystore.RecoverableKeyStoreLoader; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; +import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.List; @@ -44,7 +50,10 @@ public class RecoverableKeyStoreManager { private static final String TAG = "RecoverableKeyStoreManager"; private static RecoverableKeyStoreManager mInstance; - private Context mContext; + + private final Context mContext; + private final RecoverableKeyStoreDb mDatabase; + private final RecoverySessionStorage mRecoverySessionStorage; /** * Returns a new or existing instance. @@ -53,14 +62,23 @@ public class RecoverableKeyStoreManager { */ public static synchronized RecoverableKeyStoreManager getInstance(Context mContext) { if (mInstance == null) { - mInstance = new RecoverableKeyStoreManager(mContext); + RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(mContext); + mInstance = new RecoverableKeyStoreManager( + mContext.getApplicationContext(), + db, + new RecoverySessionStorage()); } return mInstance; } @VisibleForTesting - RecoverableKeyStoreManager(Context context) { + RecoverableKeyStoreManager( + Context context, + RecoverableKeyStoreDb recoverableKeyStoreDb, + RecoverySessionStorage recoverySessionStorage) { mContext = context; + mDatabase = recoverableKeyStoreDb; + mRecoverySessionStorage = recoverySessionStorage; } public int initRecoveryService( @@ -161,7 +179,13 @@ public class RecoverableKeyStoreManager { /** * Initializes recovery session. * - * @return recovery claim + * @param sessionId A unique ID to identify the recovery session. + * @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? + * @return Encrypted bytes of recovery claim. This can then be issued to the vault service. + * * @hide */ public byte[] startRecoverySession( @@ -173,7 +197,40 @@ public class RecoverableKeyStoreManager { int userId) throws RemoteException { checkRecoverKeyStorePermission(); - throw new UnsupportedOperationException(); + + if (secrets.size() != 1) { + // TODO: support multiple secrets + throw new RemoteException("Only a single KeyStoreRecoveryMetadata is supported"); + } + + byte[] keyClaimant = KeySyncUtils.generateKeyClaimant(); + byte[] kfHash = secrets.get(0).getSecret(); + mRecoverySessionStorage.add( + userId, new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant)); + + try { + byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash); + PublicKey publicKey = KeySyncUtils.deserializePublicKey(verifierPublicKey); + return KeySyncUtils.encryptRecoveryClaim( + publicKey, + vaultParams, + vaultChallenge, + thmKfHash, + keyClaimant); + } 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 (InvalidKeySpecException | InvalidKeyException e) { + throw new RemoteException( + "Not a valid X509 key", + e, + /*enableSuppression=*/ true, + /*writeableStackTrace=*/ true); + } } public void recoverKeys( diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java new file mode 100644 index 0000000000000..bc56ae1dc1815 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings.recoverablekeystore.storage; + +import android.annotation.Nullable; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.Arrays; + +import javax.security.auth.Destroyable; + +/** + * Stores pending recovery sessions in memory. We do not write these to disk, as it contains hashes + * of the user's lock screen. + * + * @hide + */ +public class RecoverySessionStorage implements Destroyable { + + private final SparseArray> mSessionsByUid = new SparseArray<>(); + + /** + * Returns the session for the given user with the given id. + * + * @param uid The uid of the recovery agent who created the session. + * @param sessionId The unique identifier for the session. + * @return The session info. + * + * @hide + */ + @Nullable + public Entry get(int uid, String sessionId) { + ArrayList userEntries = mSessionsByUid.get(uid); + if (userEntries == null) { + return null; + } + for (Entry entry : userEntries) { + if (sessionId.equals(entry.mSessionId)) { + return entry; + } + } + return null; + } + + /** + * Adds a pending session for the given user. + * + * @param uid The uid of the recovery agent who created the session. + * @param entry The session info. + * + * @hide + */ + public void add(int uid, Entry entry) { + if (mSessionsByUid.get(uid) == null) { + mSessionsByUid.put(uid, new ArrayList<>()); + } + mSessionsByUid.get(uid).add(entry); + } + + /** + * Removes all sessions associated with the given recovery agent uid. + * + * @param uid The uid of the recovery agent whose sessions to remove. + * + * @hide + */ + public void remove(int uid) { + ArrayList entries = mSessionsByUid.get(uid); + if (entries == null) { + return; + } + for (Entry entry : entries) { + entry.destroy(); + } + mSessionsByUid.remove(uid); + } + + /** + * Returns the total count of pending sessions. + * + * @hide + */ + public int size() { + int size = 0; + int numberOfUsers = mSessionsByUid.size(); + for (int i = 0; i < numberOfUsers; i++) { + ArrayList entries = mSessionsByUid.valueAt(i); + size += entries.size(); + } + return size; + } + + /** + * Wipes the memory of any sensitive information (i.e., lock screen hashes and key claimants). + * + * @hide + */ + @Override + public void destroy() { + int numberOfUids = mSessionsByUid.size(); + for (int i = 0; i < numberOfUids; i++) { + ArrayList entries = mSessionsByUid.valueAt(i); + for (Entry entry : entries) { + entry.destroy(); + } + } + } + + /** + * Information about a recovery session. + * + * @hide + */ + public static class Entry implements Destroyable { + private final byte[] mLskfHash; + private final byte[] mKeyClaimant; + private final String mSessionId; + + /** + * @hide + */ + public Entry(String sessionId, byte[] lskfHash, byte[] keyClaimant) { + this.mLskfHash = lskfHash; + this.mSessionId = sessionId; + this.mKeyClaimant = keyClaimant; + } + + /** + * Returns the hash of the lock screen associated with the recovery attempt. + * + * @hide + */ + public byte[] getLskfHash() { + return mLskfHash; + } + + /** + * Returns the key generated for this recovery attempt (used to decrypt data returned by + * the server). + * + * @hide + */ + public byte[] getKeyClaimant() { + return mKeyClaimant; + } + + /** + * Overwrites the memory for the lskf hash and key claimant. + * + * @hide + */ + @Override + public void destroy() { + Arrays.fill(mLskfHash, (byte) 0); + Arrays.fill(mKeyClaimant, (byte) 0); + } + } +} 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 new file mode 100644 index 0000000000000..35b18b14ad3cc --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings.recoverablekeystore; + +import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN; +import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_PASSWORD; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.os.RemoteException; +import android.security.recoverablekeystore.KeyDerivationParameters; +import android.security.recoverablekeystore.KeyStoreRecoveryMetadata; +import android.security.recoverablekeystore.RecoverableKeyStoreLoader; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; +import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; + +import com.google.common.collect.ImmutableList; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.nio.charset.StandardCharsets; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RecoverableKeyStoreManagerTest { + private static final String DATABASE_FILE_NAME = "recoverablekeystore.db"; + + private static final String TEST_SESSION_ID = "karlin"; + private static final byte[] TEST_PUBLIC_KEY = new byte[] { + (byte) 0x30, (byte) 0x59, (byte) 0x30, (byte) 0x13, (byte) 0x06, (byte) 0x07, (byte) 0x2a, + (byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x02, (byte) 0x01, (byte) 0x06, + (byte) 0x08, (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x03, + (byte) 0x01, (byte) 0x07, (byte) 0x03, (byte) 0x42, (byte) 0x00, (byte) 0x04, (byte) 0xb8, + (byte) 0x00, (byte) 0x11, (byte) 0x18, (byte) 0x98, (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, + (byte) 0xb4, (byte) 0x94, (byte) 0xfe, (byte) 0x86, (byte) 0xda, (byte) 0x1c, (byte) 0x07, + (byte) 0x8d, (byte) 0x01, (byte) 0xb4, (byte) 0x3a, (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, + (byte) 0x61, (byte) 0xd0, (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10, + (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, (byte) 0x3f, (byte) 0xd2, + (byte) 0xdf, (byte) 0xf3, (byte) 0x79, (byte) 0x20, (byte) 0x1d, (byte) 0x91, (byte) 0x55, + (byte) 0xb0, (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, (byte) 0x8b, (byte) 0x32, (byte) 0x7d, + (byte) 0x25, (byte) 0x53, (byte) 0xa2, (byte) 0xfc, (byte) 0xa5, (byte) 0x65, (byte) 0xe1, + (byte) 0xbd, (byte) 0x21, (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa}; + private static final byte[] TEST_SALT = getUtf8Bytes("salt"); + private static final byte[] TEST_SECRET = getUtf8Bytes("password1234"); + private static final byte[] TEST_VAULT_CHALLENGE = getUtf8Bytes("vault_challenge"); + 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; + + @Mock private Context mMockContext; + + private RecoverableKeyStoreDb mRecoverableKeyStoreDb; + private File mDatabaseFile; + private RecoverableKeyStoreManager mRecoverableKeyStoreManager; + private RecoverySessionStorage mRecoverySessionStorage; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Context context = InstrumentationRegistry.getTargetContext(); + mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME); + mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context); + mRecoverySessionStorage = new RecoverySessionStorage(); + mRecoverableKeyStoreManager = new RecoverableKeyStoreManager( + mMockContext, + mRecoverableKeyStoreDb, + mRecoverySessionStorage); + } + + @After + public void tearDown() { + mRecoverableKeyStoreDb.close(); + mDatabaseFile.delete(); + } + + @Test + public void startRecoverySession_checksPermissionFirst() 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); + + verify(mMockContext, times(1)).enforceCallingOrSelfPermission( + eq(RecoverableKeyStoreLoader.PERMISSION_RECOVER_KEYSTORE), + any()); + } + + @Test + public void startRecoverySession_storesTheSessionInfo() 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); + + assertEquals(1, mRecoverySessionStorage.size()); + RecoverySessionStorage.Entry entry = mRecoverySessionStorage.get( + TEST_USER_ID, TEST_SESSION_ID); + assertArrayEquals(TEST_SECRET, entry.getLskfHash()); + assertEquals(KEY_CLAIMANT_LENGTH_BYTES, entry.getKeyClaimant().length); + } + + @Test + public void startRecoverySession_throwsIfBadNumberOfSecrets() throws Exception { + try { + mRecoverableKeyStoreManager.startRecoverySession( + TEST_SESSION_ID, + TEST_PUBLIC_KEY, + TEST_VAULT_PARAMS, + TEST_VAULT_CHALLENGE, + ImmutableList.of(), + TEST_USER_ID); + } catch (RemoteException e) { + assertEquals("Only a single KeyStoreRecoveryMetadata is supported", + e.getMessage()); + } + } + + @Test + public void startRecoverySession_throwsIfBadKey() throws Exception { + try { + mRecoverableKeyStoreManager.startRecoverySession( + TEST_SESSION_ID, + getUtf8Bytes("0"), + TEST_VAULT_PARAMS, + TEST_VAULT_CHALLENGE, + ImmutableList.of(new KeyStoreRecoveryMetadata( + TYPE_LOCKSCREEN, + TYPE_PASSWORD, + KeyDerivationParameters.createSHA256Parameters(TEST_SALT), + TEST_SECRET)), + TEST_USER_ID); + } catch (RemoteException e) { + assertEquals("Not a valid X509 key", + e.getMessage()); + } + } + + private static byte[] getUtf8Bytes(String s) { + return s.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorageTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorageTest.java new file mode 100644 index 0000000000000..6aeff2892b9c1 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorageTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings.recoverablekeystore.storage; + +import static junit.framework.Assert.fail; + +import static org.junit.Assert.assertEquals; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RecoverySessionStorageTest { + + private static final String TEST_SESSION_ID = "peter"; + 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"); + + @Test + public void size_isZeroForEmpty() { + assertEquals(0, new RecoverySessionStorage().size()); + } + + @Test + public void size_incrementsAfterAdd() { + RecoverySessionStorage storage = new RecoverySessionStorage(); + storage.add(TEST_USER_ID, new RecoverySessionStorage.Entry( + TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture())); + + assertEquals(1, storage.size()); + } + + @Test + public void size_decrementsAfterRemove() { + RecoverySessionStorage storage = new RecoverySessionStorage(); + storage.add(TEST_USER_ID, new RecoverySessionStorage.Entry( + TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture())); + storage.remove(TEST_USER_ID); + + assertEquals(0, storage.size()); + } + + @Test + public void remove_overwritesLskfHashMemory() { + RecoverySessionStorage storage = new RecoverySessionStorage(); + RecoverySessionStorage.Entry entry = new RecoverySessionStorage.Entry( + TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture()); + storage.add(TEST_USER_ID, entry); + + storage.remove(TEST_USER_ID); + + assertZeroedOut(entry.getLskfHash()); + } + + @Test + public void remove_overwritesKeyClaimantMemory() { + RecoverySessionStorage storage = new RecoverySessionStorage(); + RecoverySessionStorage.Entry entry = new RecoverySessionStorage.Entry( + TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture()); + storage.add(TEST_USER_ID, entry); + + storage.remove(TEST_USER_ID); + + assertZeroedOut(entry.getKeyClaimant()); + } + + @Test + public void destroy_overwritesLskfHashMemory() { + RecoverySessionStorage storage = new RecoverySessionStorage(); + RecoverySessionStorage.Entry entry = new RecoverySessionStorage.Entry( + TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture()); + storage.add(TEST_USER_ID, entry); + + storage.destroy(); + + assertZeroedOut(entry.getLskfHash()); + } + + @Test + public void destroy_overwritesKeyClaimantMemory() { + RecoverySessionStorage storage = new RecoverySessionStorage(); + RecoverySessionStorage.Entry entry = new RecoverySessionStorage.Entry( + TEST_SESSION_ID, lskfHashFixture(), keyClaimantFixture()); + storage.add(TEST_USER_ID, entry); + + storage.destroy(); + + assertZeroedOut(entry.getKeyClaimant()); + } + + private static void assertZeroedOut(byte[] bytes) { + for (byte b : bytes) { + if (b != (byte) 0) { + fail("Bytes were not all zeroed out."); + } + } + } + + private static byte[] lskfHashFixture() { + return Arrays.copyOf(TEST_LSKF_HASH, TEST_LSKF_HASH.length); + } + + private static byte[] keyClaimantFixture() { + return Arrays.copyOf(TEST_KEY_CLAIMANT, TEST_KEY_CLAIMANT.length); + } + + private static byte[] getUtf8Bytes(String s) { + return s.getBytes(StandardCharsets.UTF_8); + } +}