1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.locksettings.recoverablekeystore;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.util.Log;
22 
23 import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
24 
25 import java.security.InvalidKeyException;
26 import java.security.KeyStoreException;
27 import java.security.NoSuchAlgorithmException;
28 import java.util.Locale;
29 
30 import javax.crypto.KeyGenerator;
31 import javax.crypto.SecretKey;
32 import javax.crypto.spec.SecretKeySpec;
33 
34 /**
35  * Generates/imports keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
36  *
37  * <p>Generates/imports 256-bit AES keys, which can be used for encrypt and decrypt with AES-GCM.
38  * They are synced to disk wrapped by a platform key. This allows them to be exported to a remote
39  * service.
40  *
41  * @hide
42  */
43 public class RecoverableKeyGenerator {
44 
45     private static final String TAG = "PlatformKeyGen";
46     private static final int RESULT_CANNOT_INSERT_ROW = -1;
47     private static final String SECRET_KEY_ALGORITHM = "AES";
48 
49     static final int KEY_SIZE_BITS = 256;
50 
51     /**
52      * A new {@link RecoverableKeyGenerator} instance.
53      *
54      * @throws NoSuchAlgorithmException if "AES" key generation or "AES/GCM/NoPadding" cipher is
55      *     unavailable. Should never happen.
56      *
57      * @hide
58      */
newInstance(RecoverableKeyStoreDb database)59     public static RecoverableKeyGenerator newInstance(RecoverableKeyStoreDb database)
60             throws NoSuchAlgorithmException {
61         // NB: This cannot use AndroidKeyStore as the provider, as we need access to the raw key
62         // material, so that it can be synced to disk in encrypted form.
63         KeyGenerator keyGenerator = KeyGenerator.getInstance(SECRET_KEY_ALGORITHM);
64         return new RecoverableKeyGenerator(keyGenerator, database);
65     }
66 
67     private final KeyGenerator mKeyGenerator;
68     private final RecoverableKeyStoreDb mDatabase;
69 
RecoverableKeyGenerator( KeyGenerator keyGenerator, RecoverableKeyStoreDb recoverableKeyStoreDb)70     private RecoverableKeyGenerator(
71             KeyGenerator keyGenerator,
72             RecoverableKeyStoreDb recoverableKeyStoreDb) {
73         mKeyGenerator = keyGenerator;
74         mDatabase = recoverableKeyStoreDb;
75     }
76 
77     /**
78      * Generates a 256-bit AES key with the given alias.
79      *
80      * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
81      * persisted to disk so that it can be synced remotely, and then recovered on another device.
82      * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
83      *
84      * @param platformKey The user's platform key, with which to wrap the generated key.
85      * @param userId The user ID of the profile to which the calling app belongs.
86      * @param uid The uid of the application that will own the key.
87      * @param alias The alias by which the key will be known in the recoverable key store.
88      * @param metadata The optional metadata that will be authenticated (but unencrypted) together
89      *     with the key material when the key is uploaded to cloud.
90      * @throws RecoverableKeyStorageException if there is some error persisting the key either to
91      *     the database.
92      * @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
93      * @throws InvalidKeyException if the platform key cannot be used to wrap keys.
94      *
95      * @hide
96      */
generateAndStoreKey( PlatformEncryptionKey platformKey, int userId, int uid, String alias, @Nullable byte[] metadata)97     public byte[] generateAndStoreKey(
98             PlatformEncryptionKey platformKey, int userId, int uid, String alias,
99             @Nullable byte[] metadata)
100             throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
101         mKeyGenerator.init(KEY_SIZE_BITS);
102         SecretKey key = mKeyGenerator.generateKey();
103 
104         WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key, metadata);
105         long result = mDatabase.insertKey(userId, uid, alias, wrappedKey);
106 
107         if (result == RESULT_CANNOT_INSERT_ROW) {
108             throw new RecoverableKeyStorageException(
109                     String.format(
110                             Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
111         }
112 
113         long updatedRows = mDatabase.setShouldCreateSnapshot(userId, uid, true);
114         if (updatedRows < 0) {
115             Log.e(TAG, "Failed to set the shoudCreateSnapshot flag in the local DB.");
116         }
117 
118         return key.getEncoded();
119     }
120 
121     /**
122      * Imports an AES key with the given alias.
123      *
124      * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
125      * persisted to disk so that it can be synced remotely, and then recovered on another device.
126      * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
127      *
128      * @param platformKey The user's platform key, with which to wrap the generated key.
129      * @param userId The user ID of the profile to which the calling app belongs.
130      * @param uid The uid of the application that will own the key.
131      * @param alias The alias by which the key will be known in the recoverable key store.
132      * @param keyBytes The raw bytes of the AES key to be imported.
133      * @param metadata The optional metadata that will be authenticated (but unencrypted) together
134      *     with the key material when the key is uploaded to cloud.
135      * @throws RecoverableKeyStorageException if there is some error persisting the key either to
136      *     the database.
137      * @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
138      * @throws InvalidKeyException if the platform key cannot be used to wrap keys.
139      *
140      * @hide
141      */
importKey( @onNull PlatformEncryptionKey platformKey, int userId, int uid, @NonNull String alias, @NonNull byte[] keyBytes, @Nullable byte[] metadata)142     public void importKey(
143             @NonNull PlatformEncryptionKey platformKey, int userId, int uid, @NonNull String alias,
144             @NonNull byte[] keyBytes, @Nullable byte[] metadata)
145             throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
146         SecretKey key = new SecretKeySpec(keyBytes, SECRET_KEY_ALGORITHM);
147 
148         WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key, metadata);
149         long result = mDatabase.insertKey(userId, uid, alias, wrappedKey);
150 
151         if (result == RESULT_CANNOT_INSERT_ROW) {
152             throw new RecoverableKeyStorageException(
153                     String.format(
154                             Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
155         }
156 
157         mDatabase.setShouldCreateSnapshot(userId, uid, true);
158     }
159 }
160