if (grantAlias == null) {
throw new InternalRecoveryServiceException("null grant alias");
}
- return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(
- mKeyStore,
- grantAlias,
- KeyStore.UID_SELF);
+ return getKeyFromGrant(grantAlias);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (UnrecoverableKeyException e) {
if (grantAlias == null) {
throw new InternalRecoveryServiceException("Null grant alias");
}
- return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(
- mKeyStore,
- grantAlias,
- KeyStore.UID_SELF);
+ return getKeyFromGrant(alias);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (UnrecoverableKeyException e) {
if (grantAlias == null) {
return null;
}
- return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(
- mKeyStore,
- grantAlias,
- KeyStore.UID_SELF);
+ return getKeyFromGrant(grantAlias);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (ServiceSpecificException e) {
}
/**
+ * Returns the key with the given {@code grantAlias}.
+ */
+ Key getKeyFromGrant(String grantAlias) throws UnrecoverableKeyException {
+ return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(
+ mKeyStore,
+ grantAlias,
+ KeyStore.UID_SELF);
+ }
+
+ /**
* Removes a key called {@code alias} from the recoverable key store.
*
* @param alias The key alias.
package android.security.keystore.recovery;
+import android.Manifest;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.util.ArrayMap;
import android.util.Log;
+import java.security.Key;
import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
/**
}
/**
+ * Imports key chain snapshot recovered from a remote vault.
+ *
+ * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
+ * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
+ * and session.
+ * @throws SessionExpiredException if {@code session} has since been closed.
+ * @throws DecryptionFailedException if unable to decrypt the snapshot.
+ * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.RECOVER_KEYSTORE)
+ public Map<String, Key> recoverKeyChainSnapshot(
+ @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<WrappedApplicationKey> applicationKeys
+ ) throws SessionExpiredException, DecryptionFailedException, InternalRecoveryServiceException {
+ try {
+ Map<String, String> grantAliases = mRecoveryController
+ .getBinder()
+ .recoverKeyChainSnapshot(mSessionId, recoveryKeyBlob, applicationKeys);
+ return getKeysFromGrants(grantAliases);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
+ throw new DecryptionFailedException(e.getMessage());
+ }
+ if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
+ throw new SessionExpiredException(e.getMessage());
+ }
+ throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /** Given a map from alias to grant alias, returns a map from alias to a {@link Key} handle. */
+ private Map<String, Key> getKeysFromGrants(Map<String, String> grantAliases)
+ throws InternalRecoveryServiceException {
+ ArrayMap<String, Key> keysByAlias = new ArrayMap<>(grantAliases.size());
+ for (String alias : grantAliases.keySet()) {
+ String grantAlias = grantAliases.get(alias);
+ Key key;
+ try {
+ key = mRecoveryController.getKeyFromGrant(grantAlias);
+ } catch (UnrecoverableKeyException e) {
+ throw new InternalRecoveryServiceException(
+ String.format(
+ Locale.US,
+ "Failed to get key '%s' from grant '%s'",
+ alias,
+ grantAlias), e);
+ }
+ keysByAlias.put(alias, key);
+ }
+ return keysByAlias;
+ }
+
+ /**
* An internal session ID, used by the framework to match recovery claims to snapshot responses.
*
* @hide
in List<KeyChainProtectionParams> secrets);
Map/*<String, byte[]>*/ recoverKeys(in String sessionId, in byte[] recoveryKeyBlob,
in List<WrappedApplicationKey> applicationKeys);
+ Map/*<String, String>*/ recoverKeyChainSnapshot(
+ in String sessionId,
+ in byte[] recoveryKeyBlob,
+ in List<WrappedApplicationKey> applicationKeys);
void closeSession(in String sessionId);
}
}
@Override
+ public Map<String, String> recoverKeyChainSnapshot(
+ @NonNull String sessionId,
+ @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException {
+ return mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
+ sessionId, recoveryKeyBlob, applicationKeys);
+ }
+
+ @Override
public Map<String, byte[]> recoverKeys(@NonNull String sessionId,
@NonNull byte[] recoveryKeyBlob, @NonNull List<WrappedApplicationKey> applicationKeys)
throws RemoteException {
- return mRecoverableKeyStoreManager.recoverKeys(
- sessionId, recoveryKeyBlob, applicationKeys);
+ return mRecoverableKeyStoreManager.recoverKeys(sessionId, recoveryKeyBlob, applicationKeys);
}
@Override
import android.app.PendingIntent;
import android.content.Context;
import android.os.Binder;
+import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.UserHandle;
import android.security.keystore.recovery.RecoveryController;
import android.security.keystore.recovery.WrappedApplicationKey;
import android.security.KeyStore;
+import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
}
/**
+ * Invoked by a recovery agent after a successful recovery claim is sent to the remote vault
+ * service.
+ *
+ * @param sessionId The session ID used to generate the claim. See
+ * {@link #startRecoverySession(String, byte[], byte[], byte[], List)}.
+ * @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.
+ * @throws RemoteException if an error occurred recovering the keys.
+ */
+ public Map<String, String> recoverKeyChainSnapshot(
+ @NonNull String sessionId,
+ @NonNull byte[] encryptedRecoveryKey,
+ @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException {
+ checkRecoverKeyStorePermission();
+ int userId = UserHandle.getCallingUserId();
+ int uid = Binder.getCallingUid();
+ RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId);
+ if (sessionEntry == null) {
+ throw new ServiceSpecificException(ERROR_SESSION_EXPIRED,
+ String.format(Locale.US,
+ "Application uid=%d does not have pending session '%s'",
+ uid,
+ sessionId));
+ }
+
+ try {
+ byte[] recoveryKey = decryptRecoveryKey(sessionEntry, encryptedRecoveryKey);
+ Map<String, byte[]> keysByAlias = recoverApplicationKeys(recoveryKey, applicationKeys);
+ return importKeyMaterials(userId, uid, keysByAlias);
+ } catch (KeyStoreException e) {
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
+ } finally {
+ sessionEntry.destroy();
+ mRecoverySessionStorage.remove(uid);
+ }
+ }
+
+ /**
+ * Imports the key materials, returning a map from alias to grant alias for the calling user.
+ *
+ * @param userId The calling user ID.
+ * @param uid The calling uid.
+ * @param keysByAlias The key materials, keyed by alias.
+ * @throws KeyStoreException if an error occurs importing the key or getting the grant.
+ */
+ private Map<String, String> importKeyMaterials(
+ int userId, int uid, Map<String, byte[]> keysByAlias) throws KeyStoreException {
+ ArrayMap<String, String> grantAliasesByAlias = new ArrayMap<>(keysByAlias.size());
+ for (String alias : keysByAlias.keySet()) {
+ mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keysByAlias.get(alias));
+ String grantAlias = getAlias(userId, uid, alias);
+ Log.i(TAG, String.format(Locale.US, "Import %s -> %s", alias, grantAlias));
+ grantAliasesByAlias.put(alias, grantAlias);
+ }
+ return grantAliasesByAlias;
+ }
+
+ /**
+ * Returns an alias for the key.
+ *
+ * @param userId The user ID of the calling process.
+ * @param uid The uid of the calling process.
+ * @param alias The alias of the key.
+ * @return The alias in the calling process's keystore.
+ */
+ private String getAlias(int userId, int uid, String alias) {
+ return mApplicationKeyStorage.getGrantAlias(userId, uid, alias);
+ }
+
+ /**
* Deprecated
* Generates a key named {@code alias} in the recoverable store for the calling uid. Then
* returns the raw key material.
byte[] secretKey =
mRecoverableKeyGenerator.generateAndStoreKey(encryptionKey, userId, uid, alias);
mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, secretKey);
- return mApplicationKeyStorage.getGrantAlias(userId, uid, alias);
+ return getAlias(userId, uid, alias);
} catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) {
throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
}
// Import the key to Android KeyStore and get grant
mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keyBytes);
- return mApplicationKeyStorage.getGrantAlias(userId, uid, alias);
+ return getAlias(userId, uid, alias);
} catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) {
throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
}
public String getKey(@NonNull String alias) throws RemoteException {
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
- String grantAlias = mApplicationKeyStorage.getGrantAlias(userId, uid, alias);
- return grantAlias;
+ return getAlias(userId, uid, alias);
}
private byte[] decryptRecoveryKey(
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
import android.security.KeyStore;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.locksettings.recoverablekeystore.KeyStoreProxy;
import java.security.KeyStore.SecretKeyEntry;
import java.security.KeyStoreException;
+import java.util.Locale;
+
import javax.crypto.spec.SecretKeySpec;
/**
* revealing key material
*/
public class ApplicationKeyStorage {
+ private static final String TAG = "RecoverableAppKeyStore";
+
private static final String APPLICATION_KEY_ALIAS_PREFIX =
"com.android.server.locksettings.recoverablekeystore/application/";
- KeyStoreProxy mKeyStore;
- KeyStore mKeystoreService;
+ private final KeyStoreProxy mKeyStore;
+ private final KeyStore mKeystoreService;
public static ApplicationKeyStorage getInstance(KeyStore keystoreService)
throws KeyStoreException {
public @Nullable String getGrantAlias(int userId, int uid, String alias) {
// Aliases used by {@link KeyStore} are different than used by public API.
// {@code USER_PRIVATE_KEY} prefix is used secret keys.
+ Log.i(TAG, String.format(Locale.US, "Get %d/%d/%s", userId, uid, alias));
String keystoreAlias = Credentials.USER_PRIVATE_KEY + getInternalAlias(userId, uid, alias);
return mKeystoreService.grant(keystoreAlias, uid);
}
public void setSymmetricKeyEntry(int userId, int uid, String alias, byte[] secretKey)
throws KeyStoreException {
+ Log.i(TAG, String.format(Locale.US, "Set %d/%d/%s: %d bytes of key material",
+ userId, uid, alias, secretKey.length));
try {
mKeyStore.setEntry(
getInternalAlias(userId, uid, alias),
}
public void deleteEntry(int userId, int uid, String alias) {
+ Log.i(TAG, String.format(Locale.US, "Del %d/%d/%s", userId, uid, alias));
try {
mKeyStore.deleteEntry(getInternalAlias(userId, uid, alias));
} catch (KeyStoreException e) {
import android.os.ServiceSpecificException;
import android.os.UserHandle;
import android.security.KeyStore;
+import android.security.keystore.AndroidKeyStoreProvider;
import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import java.io.File;
import java.nio.charset.StandardCharsets;
+import java.security.UnrecoverableKeyException;
import java.security.cert.CertPath;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.Random;
+import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@SmallTest
@Mock private RecoverySnapshotListenersStorage mMockListenersStorage;
@Mock private KeyguardManager mKeyguardManager;
@Mock private PlatformKeyManager mPlatformKeyManager;
- @Mock private KeyStore mKeyStore;
@Mock private ApplicationKeyStorage mApplicationKeyStorage;
private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
mRecoverableKeyStoreManager = new RecoverableKeyStoreManager(
mMockContext,
- mKeyStore,
+ KeyStore.getInstance(),
mRecoverableKeyStoreDb,
mRecoverySessionStorage,
Executors.newSingleThreadExecutor(),
assertThat(mRecoverableKeyStoreDb.getKey(uid, TEST_ALIAS)).isNotNull();
assertThat(mRecoverableKeyStoreDb.getShouldCreateSnapshot(userId, uid)).isTrue();
+ // TODO(76083050) Test the grant mechanism for the keys.
}
@Test
}
@Test
- public void recoverKeys_throwsIfNoSessionIsPresent() throws Exception {
+ public void recoverKeyChainSnapshot_throwsIfNoSessionIsPresent() throws Exception {
try {
- mRecoverableKeyStoreManager.recoverKeys(
+ mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
TEST_SESSION_ID,
/*recoveryKeyBlob=*/ randomBytes(32),
/*applicationKeys=*/ ImmutableList.of(
}
@Test
- public void recoverKeys_throwsIfRecoveryClaimCannotBeDecrypted() throws Exception {
+ public void recoverKeyChainSnapshot_throwsIfRecoveryClaimCannotBeDecrypted() throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
TEST_SECRET)));
try {
- mRecoverableKeyStoreManager.recoverKeys(
+ mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
TEST_SESSION_ID,
/*encryptedRecoveryKey=*/ randomBytes(60),
/*applicationKeys=*/ ImmutableList.of());
}
@Test
- public void recoverKeys_throwsIfFailedToDecryptAllApplicationKeys() throws Exception {
+ public void recoverKeyChainSnapshot_throwsIfFailedToDecryptAllApplicationKeys() throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
encryptedApplicationKey(randomRecoveryKey(), randomBytes(32)));
try {
- mRecoverableKeyStoreManager.recoverKeys(
+ mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
TEST_SESSION_ID,
/*encryptedRecoveryKey=*/ encryptedClaimResponse,
/*applicationKeys=*/ ImmutableList.of(badApplicationKey));
}
@Test
- public void recoverKeys_doesNotThrowIfNoApplicationKeysToBeDecrypted() throws Exception {
+ public void recoverKeyChainSnapshot_doesNotThrowIfNoApplicationKeysToBeDecrypted()
+ throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
byte[] encryptedClaimResponse = encryptClaimResponse(
keyClaimant, TEST_SECRET, TEST_VAULT_PARAMS, recoveryKey);
- mRecoverableKeyStoreManager.recoverKeys(
+ mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
TEST_SESSION_ID,
/*encryptedRecoveryKey=*/ encryptedClaimResponse,
/*applicationKeys=*/ ImmutableList.of());
}
@Test
- public void recoverKeys_returnsDecryptedKeys() throws Exception {
+ public void recoverKeyChainSnapshot_returnsDecryptedKeys() throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
TEST_ALIAS,
encryptedApplicationKey(recoveryKey, applicationKeyBytes));
- Map<String, byte[]> recoveredKeys = mRecoverableKeyStoreManager.recoverKeys(
+ Map<String, String> recoveredKeys = mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
TEST_SESSION_ID,
encryptedClaimResponse,
ImmutableList.of(applicationKey));
assertThat(recoveredKeys).hasSize(1);
- assertThat(recoveredKeys.get(TEST_ALIAS)).isEqualTo(applicationKeyBytes);
+ assertThat(recoveredKeys).containsKey(TEST_ALIAS);
+ // TODO(76083050) Test the grant mechanism for the keys.
}
@Test
- public void recoverKeys_worksOnOtherApplicationKeysIfOneDecryptionFails() throws Exception {
+ public void recoverKeyChainSnapshot_worksOnOtherApplicationKeysIfOneDecryptionFails() throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
TEST_ALIAS2,
encryptedApplicationKey(recoveryKey, applicationKeyBytes2));
- Map<String, byte[]> recoveredKeys = mRecoverableKeyStoreManager.recoverKeys(
+ Map<String, String> recoveredKeys = mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
TEST_SESSION_ID,
encryptedClaimResponse,
ImmutableList.of(applicationKey1, applicationKey2));
assertThat(recoveredKeys).hasSize(1);
- assertThat(recoveredKeys.get(TEST_ALIAS2)).isEqualTo(applicationKeyBytes2);
+ assertThat(recoveredKeys).containsKey(TEST_ALIAS2);
+ // TODO(76083050) Test the grant mechanism for the keys.
}
@Test