/*
|
* 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.annotation.NonNull;
|
import android.annotation.Nullable;
|
import android.util.Log;
|
|
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
|
|
import java.security.InvalidKeyException;
|
import java.security.KeyStoreException;
|
import java.security.NoSuchAlgorithmException;
|
import java.util.Locale;
|
|
import javax.crypto.KeyGenerator;
|
import javax.crypto.SecretKey;
|
import javax.crypto.spec.SecretKeySpec;
|
|
/**
|
* Generates/imports keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
|
*
|
* <p>Generates/imports 256-bit AES keys, which can be used for encrypt and decrypt with AES-GCM.
|
* They are synced to disk wrapped by a platform key. This allows them to be exported to a remote
|
* service.
|
*
|
* @hide
|
*/
|
public class RecoverableKeyGenerator {
|
|
private static final String TAG = "PlatformKeyGen";
|
private static final int RESULT_CANNOT_INSERT_ROW = -1;
|
private static final String SECRET_KEY_ALGORITHM = "AES";
|
|
static final int KEY_SIZE_BITS = 256;
|
|
/**
|
* A new {@link RecoverableKeyGenerator} instance.
|
*
|
* @throws NoSuchAlgorithmException if "AES" key generation or "AES/GCM/NoPadding" cipher is
|
* unavailable. Should never happen.
|
*
|
* @hide
|
*/
|
public static RecoverableKeyGenerator newInstance(RecoverableKeyStoreDb database)
|
throws NoSuchAlgorithmException {
|
// NB: This cannot use AndroidKeyStore as the provider, as we need access to the raw key
|
// material, so that it can be synced to disk in encrypted form.
|
KeyGenerator keyGenerator = KeyGenerator.getInstance(SECRET_KEY_ALGORITHM);
|
return new RecoverableKeyGenerator(keyGenerator, database);
|
}
|
|
private final KeyGenerator mKeyGenerator;
|
private final RecoverableKeyStoreDb mDatabase;
|
|
private RecoverableKeyGenerator(
|
KeyGenerator keyGenerator,
|
RecoverableKeyStoreDb recoverableKeyStoreDb) {
|
mKeyGenerator = keyGenerator;
|
mDatabase = recoverableKeyStoreDb;
|
}
|
|
/**
|
* Generates a 256-bit AES key with the given alias.
|
*
|
* <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
|
* persisted to disk so that it can be synced remotely, and then recovered on another device.
|
* The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
|
*
|
* @param platformKey The user's platform key, with which to wrap the generated key.
|
* @param userId The user ID of the profile to which the calling app belongs.
|
* @param uid The uid of the application that will own the key.
|
* @param alias The alias by which the key will be known in the recoverable key store.
|
* @param metadata The optional metadata that will be authenticated (but unencrypted) together
|
* with the key material when the key is uploaded to cloud.
|
* @throws RecoverableKeyStorageException if there is some error persisting the key either to
|
* the database.
|
* @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
|
* @throws InvalidKeyException if the platform key cannot be used to wrap keys.
|
*
|
* @hide
|
*/
|
public byte[] generateAndStoreKey(
|
PlatformEncryptionKey platformKey, int userId, int uid, String alias,
|
@Nullable byte[] metadata)
|
throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
|
mKeyGenerator.init(KEY_SIZE_BITS);
|
SecretKey key = mKeyGenerator.generateKey();
|
|
WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key, metadata);
|
long result = mDatabase.insertKey(userId, uid, alias, wrappedKey);
|
|
if (result == RESULT_CANNOT_INSERT_ROW) {
|
throw new RecoverableKeyStorageException(
|
String.format(
|
Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
|
}
|
|
long updatedRows = mDatabase.setShouldCreateSnapshot(userId, uid, true);
|
if (updatedRows < 0) {
|
Log.e(TAG, "Failed to set the shoudCreateSnapshot flag in the local DB.");
|
}
|
|
return key.getEncoded();
|
}
|
|
/**
|
* Imports an AES key with the given alias.
|
*
|
* <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
|
* persisted to disk so that it can be synced remotely, and then recovered on another device.
|
* The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
|
*
|
* @param platformKey The user's platform key, with which to wrap the generated key.
|
* @param userId The user ID of the profile to which the calling app belongs.
|
* @param uid The uid of the application that will own the key.
|
* @param alias The alias by which the key will be known in the recoverable key store.
|
* @param keyBytes The raw bytes of the AES key to be imported.
|
* @param metadata The optional metadata that will be authenticated (but unencrypted) together
|
* with the key material when the key is uploaded to cloud.
|
* @throws RecoverableKeyStorageException if there is some error persisting the key either to
|
* the database.
|
* @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
|
* @throws InvalidKeyException if the platform key cannot be used to wrap keys.
|
*
|
* @hide
|
*/
|
public void importKey(
|
@NonNull PlatformEncryptionKey platformKey, int userId, int uid, @NonNull String alias,
|
@NonNull byte[] keyBytes, @Nullable byte[] metadata)
|
throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
|
SecretKey key = new SecretKeySpec(keyBytes, SECRET_KEY_ALGORITHM);
|
|
WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key, metadata);
|
long result = mDatabase.insertKey(userId, uid, alias, wrappedKey);
|
|
if (result == RESULT_CANNOT_INSERT_ROW) {
|
throw new RecoverableKeyStorageException(
|
String.format(
|
Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
|
}
|
|
mDatabase.setShouldCreateSnapshot(userId, uid, true);
|
}
|
}
|