Merge "Add RecoverySession importKeyChainSnapshot method" into pi-dev

This commit is contained in:
Robert Berry
2018-03-21 12:25:37 +00:00
committed by Android (Google) Code Review
7 changed files with 200 additions and 36 deletions

View File

@@ -547,10 +547,7 @@ public class RecoveryController {
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) {
@@ -581,10 +578,7 @@ public class RecoveryController {
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) {
@@ -614,10 +608,7 @@ public class RecoveryController {
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) {
@@ -625,6 +616,16 @@ public class RecoveryController {
}
}
/**
* 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.
*

View File

@@ -16,17 +16,22 @@
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;
/**
@@ -243,6 +248,63 @@ public class RecoverySession implements AutoCloseable {
}
}
/**
* 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.
*

View File

@@ -83,5 +83,9 @@ interface ILockSettings {
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);
}

View File

@@ -2064,12 +2064,20 @@ public class LockSettingsService extends ILockSettings.Stub {
mRecoverableKeyStoreManager.closeSession(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

View File

@@ -31,6 +31,7 @@ import android.annotation.Nullable;
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;
@@ -41,6 +42,7 @@ import android.security.keystore.recovery.RecoveryController;
import android.security.keystore.recovery.TrustedRootCertificates;
import android.security.keystore.recovery.WrappedApplicationKey;
import android.security.KeyStore;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -555,6 +557,78 @@ public class RecoverableKeyStoreManager {
}
}
/**
* 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
@@ -632,7 +706,7 @@ public class RecoverableKeyStoreManager {
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());
}
@@ -683,7 +757,7 @@ public class RecoverableKeyStoreManager {
// 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());
}
@@ -697,8 +771,7 @@ public class RecoverableKeyStoreManager {
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(

View File

@@ -24,6 +24,7 @@ import android.security.Credentials;
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;
@@ -31,6 +32,8 @@ import com.android.server.locksettings.recoverablekeystore.KeyStoreProxyImpl;
import java.security.KeyStore.SecretKeyEntry;
import java.security.KeyStoreException;
import java.util.Locale;
import javax.crypto.spec.SecretKeySpec;
/**
@@ -40,11 +43,13 @@ 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 {
@@ -65,12 +70,15 @@ public class ApplicationKeyStorage {
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),
@@ -87,6 +95,7 @@ public class ApplicationKeyStorage {
}
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) {

View File

@@ -40,6 +40,7 @@ import android.os.Binder;
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;
@@ -68,6 +69,7 @@ import org.mockito.MockitoAnnotations;
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;
@@ -76,8 +78,10 @@ import java.util.concurrent.Executors;
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
@@ -145,7 +149,6 @@ public class RecoverableKeyStoreManagerTest {
@Mock private RecoverySnapshotListenersStorage mMockListenersStorage;
@Mock private KeyguardManager mKeyguardManager;
@Mock private PlatformKeyManager mPlatformKeyManager;
@Mock private KeyStore mKeyStore;
@Mock private ApplicationKeyStorage mApplicationKeyStorage;
private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
@@ -176,7 +179,7 @@ public class RecoverableKeyStoreManagerTest {
mRecoverableKeyStoreManager = new RecoverableKeyStoreManager(
mMockContext,
mKeyStore,
KeyStore.getInstance(),
mRecoverableKeyStoreDb,
mRecoverySessionStorage,
Executors.newSingleThreadExecutor(),
@@ -220,6 +223,7 @@ public class RecoverableKeyStoreManagerTest {
assertThat(mRecoverableKeyStoreDb.getKey(uid, TEST_ALIAS)).isNotNull();
assertThat(mRecoverableKeyStoreDb.getShouldCreateSnapshot(userId, uid)).isTrue();
// TODO(76083050) Test the grant mechanism for the keys.
}
@Test
@@ -680,9 +684,9 @@ public class RecoverableKeyStoreManagerTest {
}
@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(
@@ -695,7 +699,7 @@ public class RecoverableKeyStoreManagerTest {
}
@Test
public void recoverKeys_throwsIfRecoveryClaimCannotBeDecrypted() throws Exception {
public void recoverKeyChainSnapshot_throwsIfRecoveryClaimCannotBeDecrypted() throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
@@ -708,7 +712,7 @@ public class RecoverableKeyStoreManagerTest {
TEST_SECRET)));
try {
mRecoverableKeyStoreManager.recoverKeys(
mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
TEST_SESSION_ID,
/*encryptedRecoveryKey=*/ randomBytes(60),
/*applicationKeys=*/ ImmutableList.of());
@@ -719,7 +723,7 @@ public class RecoverableKeyStoreManagerTest {
}
@Test
public void recoverKeys_throwsIfFailedToDecryptAllApplicationKeys() throws Exception {
public void recoverKeyChainSnapshot_throwsIfFailedToDecryptAllApplicationKeys() throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
@@ -740,7 +744,7 @@ public class RecoverableKeyStoreManagerTest {
encryptedApplicationKey(randomRecoveryKey(), randomBytes(32)));
try {
mRecoverableKeyStoreManager.recoverKeys(
mRecoverableKeyStoreManager.recoverKeyChainSnapshot(
TEST_SESSION_ID,
/*encryptedRecoveryKey=*/ encryptedClaimResponse,
/*applicationKeys=*/ ImmutableList.of(badApplicationKey));
@@ -751,7 +755,8 @@ public class RecoverableKeyStoreManagerTest {
}
@Test
public void recoverKeys_doesNotThrowIfNoApplicationKeysToBeDecrypted() throws Exception {
public void recoverKeyChainSnapshot_doesNotThrowIfNoApplicationKeysToBeDecrypted()
throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
@@ -768,14 +773,14 @@ public class RecoverableKeyStoreManagerTest {
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,
@@ -796,17 +801,18 @@ public class RecoverableKeyStoreManagerTest {
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,
@@ -834,13 +840,14 @@ public class RecoverableKeyStoreManagerTest {
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