diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java new file mode 100644 index 0000000000000..5155a99ee04fa --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java @@ -0,0 +1,31 @@ +/* + * 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; + +/** + * Error thrown initializing {@link PlatformKeyManager} if the user is not secure (i.e., has no + * lock screen set). + */ +public class InsecureUserException extends Exception { + + /** + * A new instance with {@code message} error message. + */ + public InsecureUserException(String message) { + super(message); + } +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java new file mode 100644 index 0000000000000..7c9b39597ab2e --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java @@ -0,0 +1,43 @@ +/* + * 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 java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +/** + * Proxies {@link java.security.KeyStore}. As all of its methods are final, it cannot otherwise be + * mocked for tests. + * + * @hide + */ +public interface KeyStoreProxy { + + /** @see KeyStore#containsAlias(String) */ + boolean containsAlias(String alias) throws KeyStoreException; + + /** @see KeyStore#getKey(String, char[]) */ + Key getKey(String alias, char[] password) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException; + + /** @see KeyStore#setEntry(String, KeyStore.Entry, KeyStore.ProtectionParameter) */ + void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam) + throws KeyStoreException; +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java new file mode 100644 index 0000000000000..ceee3815845e4 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java @@ -0,0 +1,55 @@ +/* + * 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 java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +/** + * Implementation of {@link KeyStoreProxy} that delegates all method calls to the {@link KeyStore}. + */ +public class KeyStoreProxyImpl implements KeyStoreProxy { + + private final KeyStore mKeyStore; + + /** + * A new instance, delegating to {@code keyStore}. + */ + public KeyStoreProxyImpl(KeyStore keyStore) { + mKeyStore = keyStore; + } + + @Override + public boolean containsAlias(String alias) throws KeyStoreException { + return mKeyStore.containsAlias(alias); + } + + @Override + public Key getKey(String alias, char[] password) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return mKeyStore.getKey(alias, password); + } + + @Override + public void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam) + throws KeyStoreException { + mKeyStore.setEntry(alias, entry, protParam); + } +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java new file mode 100644 index 0000000000000..074c596ec7274 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java @@ -0,0 +1,345 @@ +/* + * 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 android.app.KeyguardManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.security.keystore.AndroidKeyStoreSecretKey; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyProtection; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Locale; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; + +/** + * Manages creating and checking the validity of the platform key. + * + *
The platform key is used to wrap the material of recoverable keys before persisting them to + * disk. It is also used to decrypt the same keys on a screen unlock, before re-wrapping them with + * a recovery key and syncing them with remote storage. + * + *
Each platform key has two entries in AndroidKeyStore: + * + *
Both entries are enabled only for AES/GCM/NoPadding Cipher algorithm. + * + * @hide + */ +public class PlatformKeyManager { + private static final String TAG = "PlatformKeyManager"; + + private static final String KEY_ALGORITHM = "AES"; + private static final int KEY_SIZE_BITS = 256; + private static final String SHARED_PREFS_KEY_GENERATION_ID = "generationId"; + private static final String SHARED_PREFS_PATH = "/system/recoverablekeystore/platform_keys.xml"; + private static final String KEY_ALIAS_PREFIX = + "com.android.server.locksettings.recoverablekeystore/platform/"; + private static final String ENCRYPT_KEY_ALIAS_SUFFIX = "encrypt"; + private static final String DECRYPT_KEY_ALIAS_SUFFIX = "decrypt"; + private static final int USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 15; + + private final Context mContext; + private final KeyStoreProxy mKeyStore; + private final SharedPreferences mSharedPreferences; + private final int mUserId; + + private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore"; + + /** + * A new instance operating on behalf of {@code userId}, storing its prefs in the location + * defined by {@code context}. + * + * @param context This should be the context of the RecoverableKeyStoreLoader service. + * @param userId The ID of the user to whose lock screen the platform key must be bound. + * @throws KeyStoreException if failed to initialize AndroidKeyStore. + * @throws NoSuchAlgorithmException if AES is unavailable - should never happen. + * @throws InsecureUserException if the user does not have a lock screen set. + * @throws SecurityException if the caller does not have permission to write to /data/system. + * + * @hide + */ + public static PlatformKeyManager getInstance(Context context, int userId) + throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException { + context = context.getApplicationContext(); + File sharedPreferencesFile = new File( + Environment.getDataDirectory().getAbsoluteFile(), SHARED_PREFS_PATH); + sharedPreferencesFile.mkdirs(); + PlatformKeyManager keyManager = new PlatformKeyManager( + userId, + context, + new KeyStoreProxyImpl(getAndLoadAndroidKeyStore()), + context.getSharedPreferences(sharedPreferencesFile, Context.MODE_PRIVATE)); + keyManager.init(); + return keyManager; + } + + @VisibleForTesting + PlatformKeyManager( + int userId, + Context context, + KeyStoreProxy keyStore, + SharedPreferences sharedPreferences) { + mUserId = userId; + mKeyStore = keyStore; + mContext = context; + mSharedPreferences = sharedPreferences; + } + + /** + * Returns the current generation ID of the platform key. This increments whenever a platform + * key has to be replaced. (e.g., because the user has removed and then re-added their lock + * screen). + * + * @hide + */ + public int getGenerationId() { + return mSharedPreferences.getInt(getGenerationIdKey(), 1); + } + + /** + * Returns {@code true} if the platform key is available. A platform key won't be available if + * the user has not set up a lock screen. + * + * @hide + */ + public boolean isAvailable() { + return mContext.getSystemService(KeyguardManager.class).isDeviceSecure(mUserId); + } + + /** + * Generates a new key and increments the generation ID. Should be invoked if the platform key + * is corrupted and needs to be rotated. + * + * @throws NoSuchAlgorithmException if AES is unavailable - should never happen. + * @throws KeyStoreException if there is an error in AndroidKeyStore. + * + * @hide + */ + public void regenerate() throws NoSuchAlgorithmException, KeyStoreException { + int generationId = getGenerationId(); + generateAndLoadKey(generationId + 1); + setGenerationId(generationId + 1); + } + + /** + * Returns the platform key used for encryption. + * + * @throws KeyStoreException if there was an AndroidKeyStore error. + * @throws UnrecoverableKeyException if the key could not be recovered. + * @throws NoSuchAlgorithmException if AES is unavailable - should never occur. + * + * @hide + */ + public PlatformEncryptionKey getEncryptKey() + throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException { + int generationId = getGenerationId(); + AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey( + getEncryptAlias(generationId), /*password=*/ null); + return new PlatformEncryptionKey(generationId, key); + } + + /** + * Returns the platform key used for decryption. Only works after a recent screen unlock. + * + * @throws KeyStoreException if there was an AndroidKeyStore error. + * @throws UnrecoverableKeyException if the key could not be recovered. + * @throws NoSuchAlgorithmException if AES is unavailable - should never occur. + * + * @hide + */ + public PlatformDecryptionKey getDecryptKey() + throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException { + int generationId = getGenerationId(); + AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey( + getDecryptAlias(generationId), /*password=*/ null); + return new PlatformDecryptionKey(generationId, key); + } + + /** + * Initializes the class. If there is no current platform key, and the user has a lock screen + * set, will create the platform key and set the generation ID. + * + * @throws KeyStoreException if there was an error in AndroidKeyStore. + * @throws NoSuchAlgorithmException if AES is unavailable - should never happen. + * + * @hide + */ + public void init() throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException { + if (!isAvailable()) { + throw new InsecureUserException(String.format( + Locale.US, "%d does not have a lock screen set.", mUserId)); + } + + int generationId = getGenerationId(); + if (isKeyLoaded(generationId)) { + Log.i(TAG, String.format( + Locale.US, "Platform key generation %d exists already.", generationId)); + return; + } + if (generationId == 1) { + Log.i(TAG, "Generating initial platform ID."); + } else { + Log.w(TAG, String.format(Locale.US, "Platform generation ID was %d but no " + + "entry was present in AndroidKeyStore. Generating fresh key.", generationId)); + } + + generateAndLoadKey(generationId); + } + + /** + * Returns the alias of the encryption key with the specific {@code generationId} in the + * AndroidKeyStore. + * + *
These IDs look as follows:
+ * {@code com.security.recoverablekeystore/platform/ These IDs look as follows:
+ * {@code com.security.recoverablekeystore/platform/