diff --git a/core/java/android/security/keystore/recovery/RecoveryController.java b/core/java/android/security/keystore/recovery/RecoveryController.java index 4e4a0374087e4..7cd08f76a47ee 100644 --- a/core/java/android/security/keystore/recovery/RecoveryController.java +++ b/core/java/android/security/keystore/recovery/RecoveryController.java @@ -26,9 +26,13 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; +import android.security.KeyStore; +import android.security.keystore.AndroidKeyStoreProvider; import com.android.internal.widget.ILockSettings; +import java.security.Key; +import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.List; @@ -113,9 +117,11 @@ public class RecoveryController { private final ILockSettings mBinder; + private final KeyStore mKeyStore; - private RecoveryController(ILockSettings binder) { + private RecoveryController(ILockSettings binder, KeyStore keystore) { mBinder = binder; + mKeyStore = keystore; } /** @@ -133,7 +139,7 @@ public class RecoveryController { public static RecoveryController getInstance(Context context) { ILockSettings lockSettings = ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings")); - return new RecoveryController(lockSettings); + return new RecoveryController(lockSettings, KeyStore.getInstance()); } /** @@ -430,6 +436,7 @@ public class RecoveryController { } /** + * Deprecated. * Generates a AES256/GCM/NoPADDING key called {@code alias} and loads it into the recoverable * key store. Returns the raw material of the key. * @@ -444,7 +451,6 @@ public class RecoveryController { public byte[] generateAndStoreKey(@NonNull String alias, byte[] account) throws InternalRecoveryServiceException, LockScreenRequiredException { try { - // TODO: add account return mBinder.generateAndStoreKey(alias); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -456,6 +462,72 @@ public class RecoveryController { } } + /** + * Generates a AES256/GCM/NoPADDING key called {@code alias} and loads it into the recoverable + * key store. Returns {@link javax.crypto.SecretKey}. + * + * @param alias The key alias. + * @param account The account associated with the key. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + * @throws LockScreenRequiredException if the user has not set a lock screen. This is required + * to generate recoverable keys, as the snapshots are encrypted using a key derived from the + * lock screen. + * @hide + */ + public Key generateKey(@NonNull String alias, byte[] account) + throws InternalRecoveryServiceException, LockScreenRequiredException { + // TODO: update RecoverySession.recoverKeys + try { + String grantAlias = mBinder.generateKey(alias, account); + if (grantAlias == null) { + return null; + } + Key result = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore( + mKeyStore, + grantAlias, + KeyStore.UID_SELF); + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (UnrecoverableKeyException e) { + throw new InternalRecoveryServiceException("Access to newly generated key failed for"); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_INSECURE_USER) { + throw new LockScreenRequiredException(e.getMessage()); + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Gets a key called {@code alias} from the recoverable key store. + * + * @param alias The key alias. + * @return The key. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + * @throws UnrecoverableKeyException if key is permanently invalidated or not found. + * @hide + */ + public @Nullable Key getKey(@NonNull String alias) + throws InternalRecoveryServiceException, UnrecoverableKeyException { + try { + String grantAlias = mBinder.getKey(alias); + if (grantAlias == null) { + return null; + } + return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore( + mKeyStore, + grantAlias, + KeyStore.UID_SELF); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + /** * Removes a key called {@code alias} from the recoverable key store. * diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl index 5673814ca3620..732534ccbcbed 100644 --- a/core/java/com/android/internal/widget/ILockSettings.aidl +++ b/core/java/com/android/internal/widget/ILockSettings.aidl @@ -66,6 +66,8 @@ interface ILockSettings { void initRecoveryService(in String rootCertificateAlias, in byte[] signedPublicKeyList); KeyChainSnapshot getKeyChainSnapshot(); byte[] generateAndStoreKey(String alias); + String generateKey(String alias, in byte[] account); + String getKey(String alias); void removeKey(String alias); void setSnapshotCreatedPendingIntent(in PendingIntent intent); Map getRecoverySnapshotVersions(); diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 28fa86b3a8932..2cb8c48a38506 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -383,8 +383,8 @@ public class LockSettingsService extends ILockSettings.Stub { return KeyStore.getInstance(); } - public RecoverableKeyStoreManager getRecoverableKeyStoreManager() { - return RecoverableKeyStoreManager.getInstance(mContext); + public RecoverableKeyStoreManager getRecoverableKeyStoreManager(KeyStore keyStore) { + return RecoverableKeyStoreManager.getInstance(mContext, keyStore); } public IStorageManager getStorageManager() { @@ -413,7 +413,7 @@ public class LockSettingsService extends ILockSettings.Stub { mInjector = injector; mContext = injector.getContext(); mKeyStore = injector.getKeyStore(); - mRecoverableKeyStoreManager = injector.getRecoverableKeyStoreManager(); + mRecoverableKeyStoreManager = injector.getRecoverableKeyStoreManager(mKeyStore); mHandler = injector.getHandler(); mStrongAuth = injector.getStrongAuth(); mActivityManager = injector.getActivityManager(); @@ -2064,6 +2064,16 @@ public class LockSettingsService extends ILockSettings.Stub { return mRecoverableKeyStoreManager.generateAndStoreKey(alias); } + @Override + public String generateKey(@NonNull String alias, byte[] account) throws RemoteException { + return mRecoverableKeyStoreManager.generateKey(alias, account); + } + + @Override + public String getKey(@NonNull String alias) throws RemoteException { + return mRecoverableKeyStoreManager.getKey(alias); + } + private static final String[] VALID_SETTINGS = new String[] { LockPatternUtils.LOCKOUT_PERMANENT_KEY, LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE, diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java index 59132da7d4c7a..285e722886c28 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java @@ -16,10 +16,13 @@ package com.android.server.locksettings.recoverablekeystore; +import java.io.IOException; +import java.security.cert.CertificateException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; /** @@ -27,6 +30,7 @@ import java.security.UnrecoverableKeyException; */ public class KeyStoreProxyImpl implements KeyStoreProxy { + private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore"; private final KeyStore mKeyStore; /** @@ -57,4 +61,21 @@ public class KeyStoreProxyImpl implements KeyStoreProxy { public void deleteEntry(String alias) throws KeyStoreException { mKeyStore.deleteEntry(alias); } + + /** + * Returns AndroidKeyStore-provided {@link KeyStore}, having already invoked + * {@link KeyStore#load(KeyStore.LoadStoreParameter)}. + * + * @throws KeyStoreException if there was a problem getting or initializing the key store. + */ + public static KeyStore getAndLoadAndroidKeyStore() throws KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER); + try { + keyStore.load(/*param=*/ null); + } catch (CertificateException | IOException | NoSuchAlgorithmException e) { + // Should never happen. + throw new KeyStoreException("Unable to load keystore.", e); + } + return keyStore; + } } 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 ec72b221931f3..fda6cdf33e0c2 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java @@ -37,21 +37,23 @@ import android.security.keystore.recovery.KeyChainProtectionParams; import android.security.keystore.recovery.KeyChainSnapshot; import android.security.keystore.recovery.RecoveryController; import android.security.keystore.recovery.WrappedApplicationKey; +import android.security.KeyStore; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.HexDump; +import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; import java.security.InvalidKeyException; -import java.security.KeyStoreException; import java.security.KeyFactory; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; -import java.security.UnrecoverableKeyException; import java.security.spec.InvalidKeySpecException; +import java.security.UnrecoverableKeyException; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.HashMap; @@ -82,18 +84,23 @@ public class RecoverableKeyStoreManager { private final RecoverableKeyGenerator mRecoverableKeyGenerator; private final RecoverySnapshotStorage mSnapshotStorage; private final PlatformKeyManager mPlatformKeyManager; + private final KeyStore mKeyStore; + private final ApplicationKeyStorage mApplicationKeyStorage; /** * Returns a new or existing instance. * * @hide */ - public static synchronized RecoverableKeyStoreManager getInstance(Context context) { + public static synchronized RecoverableKeyStoreManager + getInstance(Context context, KeyStore keystore) { if (mInstance == null) { RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(context); PlatformKeyManager platformKeyManager; + ApplicationKeyStorage applicationKeyStorage; try { platformKeyManager = PlatformKeyManager.getInstance(context, db); + applicationKeyStorage = ApplicationKeyStorage.getInstance(keystore); } catch (NoSuchAlgorithmException e) { // Impossible: all algorithms must be supported by AOSP throw new RuntimeException(e); @@ -103,12 +110,14 @@ public class RecoverableKeyStoreManager { mInstance = new RecoverableKeyStoreManager( context.getApplicationContext(), + keystore, db, new RecoverySessionStorage(), Executors.newSingleThreadExecutor(), new RecoverySnapshotStorage(), new RecoverySnapshotListenersStorage(), - platformKeyManager); + platformKeyManager, + applicationKeyStorage); } return mInstance; } @@ -116,19 +125,23 @@ public class RecoverableKeyStoreManager { @VisibleForTesting RecoverableKeyStoreManager( Context context, + KeyStore keystore, RecoverableKeyStoreDb recoverableKeyStoreDb, RecoverySessionStorage recoverySessionStorage, ExecutorService executorService, RecoverySnapshotStorage snapshotStorage, RecoverySnapshotListenersStorage listenersStorage, - PlatformKeyManager platformKeyManager) { + PlatformKeyManager platformKeyManager, + ApplicationKeyStorage applicationKeyStorage) { mContext = context; + mKeyStore = keystore; mDatabase = recoverableKeyStoreDb; mRecoverySessionStorage = recoverySessionStorage; mExecutorService = executorService; mListenersStorage = listenersStorage; mSnapshotStorage = snapshotStorage; mPlatformKeyManager = platformKeyManager; + mApplicationKeyStorage = applicationKeyStorage; try { mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase); @@ -406,6 +419,7 @@ public class RecoverableKeyStoreManager { } /** + * Deprecated * Generates a key named {@code alias} in the recoverable store for the calling uid. Then * returns the raw key material. * @@ -450,9 +464,55 @@ public class RecoverableKeyStoreManager { boolean wasRemoved = mDatabase.removeKey(uid, alias); if (wasRemoved) { mDatabase.setShouldCreateSnapshot(userId, uid, true); + mApplicationKeyStorage.deleteEntry(userId, uid, alias); } } + /** + * Generates a key named {@code alias} in caller's namespace. + * The key is stored in system service keystore namespace. + * + * @return grant alias, which caller can use to access the key. + */ + public String generateKey(@NonNull String alias, byte[] account) throws RemoteException { + int uid = Binder.getCallingUid(); + int userId = UserHandle.getCallingUserId(); + + PlatformEncryptionKey encryptionKey; + try { + encryptionKey = mPlatformKeyManager.getEncryptKey(userId); + } catch (NoSuchAlgorithmException e) { + // Impossible: all algorithms must be supported by AOSP + throw new RuntimeException(e); + } catch (KeyStoreException | UnrecoverableKeyException e) { + throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); + } catch (InsecureUserException e) { + throw new ServiceSpecificException(ERROR_INSECURE_USER, e.getMessage()); + } + + try { + byte[] secretKey = + mRecoverableKeyGenerator.generateAndStoreKey(encryptionKey, userId, uid, alias); + mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, secretKey); + String grantAlias = mApplicationKeyStorage.getGrantAlias(userId, uid, alias); + return grantAlias; + } catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) { + throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); + } + } + + /** + * Gets a key named {@code alias} in caller's namespace. + * + * @return grant alias, which caller can use to access the key. + */ + 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; + } + private byte[] decryptRecoveryKey( RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse) throws RemoteException, ServiceSpecificException { diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java new file mode 100644 index 0000000000000..600a534facf98 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java @@ -0,0 +1,113 @@ +/* + * 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 android.security.keystore.RecoveryController.ERROR_SERVICE_INTERNAL_ERROR; + +import android.annotation.Nullable; +import android.os.ServiceSpecificException; +import android.security.Credentials; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyProtection; +import android.security.keystore.recovery.KeyChainSnapshot; +import android.security.KeyStore; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.locksettings.recoverablekeystore.KeyStoreProxy; +import com.android.server.locksettings.recoverablekeystore.KeyStoreProxyImpl; + +import java.security.KeyStore.SecretKeyEntry; +import java.security.KeyStoreException; +import javax.crypto.spec.SecretKeySpec; + +/** + * Storage for Application keys in LockSettings service KeyStore namespace. + * + *

Uses KeyStore's grant mechanism to make keys usable by application process without + * revealing key material + */ +public class ApplicationKeyStorage { + private static final String APPLICATION_KEY_ALIAS_PREFIX = + "com.android.server.locksettings.recoverablekeystore/application/"; + + KeyStoreProxy mKeyStore; + KeyStore mKeystoreService; + + public static ApplicationKeyStorage getInstance(KeyStore keystoreService) + throws KeyStoreException { + return new ApplicationKeyStorage( + new KeyStoreProxyImpl(KeyStoreProxyImpl.getAndLoadAndroidKeyStore()), + keystoreService); + } + + @VisibleForTesting + ApplicationKeyStorage(KeyStoreProxy keyStore, KeyStore keystoreService) { + mKeyStore = keyStore; + mKeystoreService = keystoreService; + } + + /** + * Returns grant alias, valid in Applications namespace. + */ + 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. + 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 { + try { + mKeyStore.setEntry( + getInternalAlias(userId, uid, alias), + new SecretKeyEntry( + new SecretKeySpec(secretKey, KeyProperties.KEY_ALGORITHM_AES)), + new KeyProtection.Builder( + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()); + } catch (KeyStoreException e) { + throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); + } + } + + public void deleteEntry(int userId, int uid, String alias) { + try { + mKeyStore.deleteEntry(getInternalAlias(userId, uid, alias)); + } catch (KeyStoreException e) { + throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); + } + } + + /** + * Returns the alias in locksettins service's KeyStore namespace used for given application key. + * + *

These IDs look as follows: + * {@code com.security.recoverablekeystore/application///} + * + * @param userId The ID of the user + * @param uid The uid + * @param alias - alias in application's namespace + * @return The alias. + */ + private String getInternalAlias(int userId, int uid, String alias) { + return APPLICATION_KEY_ALIAS_PREFIX + userId + "/" + uid + "/" + alias; + } +} 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 index c863aabb905f0..473a813c3838e 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java @@ -39,6 +39,7 @@ import android.Manifest; import android.os.Binder; import android.os.ServiceSpecificException; import android.os.UserHandle; +import android.security.KeyStore; import android.security.keystore.AndroidKeyStoreSecretKey; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; @@ -49,6 +50,7 @@ import android.support.test.filters.SmallTest; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; @@ -135,6 +137,8 @@ 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; private File mDatabaseFile; @@ -164,12 +168,14 @@ public class RecoverableKeyStoreManagerTest { mRecoverableKeyStoreManager = new RecoverableKeyStoreManager( mMockContext, + mKeyStore, mRecoverableKeyStoreDb, mRecoverySessionStorage, Executors.newSingleThreadExecutor(), mRecoverySnapshotStorage, mMockListenersStorage, - mPlatformKeyManager); + mPlatformKeyManager, + mApplicationKeyStorage); } @After