Loading core/java/android/security/recoverablekeystore/KeyDerivationParameters.java +1 −1 Original line number Diff line number Diff line Loading @@ -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); } Loading services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java +19 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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}. */ Loading services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +62 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading @@ -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( Loading Loading @@ -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( Loading @@ -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( Loading services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java 0 → 100644 +173 −0 Original line number Diff line number Diff line /* * 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<ArrayList<Entry>> 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<Entry> 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<Entry> 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<Entry> 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<Entry> 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); } } } services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java 0 → 100644 +186 −0 Original line number Diff line number Diff line /* * 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); } } Loading
core/java/android/security/recoverablekeystore/KeyDerivationParameters.java +1 −1 Original line number Diff line number Diff line Loading @@ -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); } Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java +19 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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}. */ Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +62 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading @@ -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( Loading Loading @@ -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( Loading @@ -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( Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java 0 → 100644 +173 −0 Original line number Diff line number Diff line /* * 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<ArrayList<Entry>> 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<Entry> 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<Entry> 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<Entry> 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<Entry> 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); } } }
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java 0 → 100644 +186 −0 Original line number Diff line number Diff line /* * 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); } }