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
17package com.android.server.locksettings.recoverablekeystore;
18
19import android.annotation.NonNull;
20
21import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
22
23import java.security.InvalidKeyException;
24import java.security.KeyStoreException;
25import java.security.NoSuchAlgorithmException;
26import java.util.Locale;
27import android.util.Log;
28
29import javax.crypto.KeyGenerator;
30import javax.crypto.SecretKey;
31import javax.crypto.spec.SecretKeySpec;
32
33/**
34 * Generates/imports keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
35 *
36 * <p>Generates/imports 256-bit AES keys, which can be used for encrypt and decrypt with AES-GCM.
37 * They are synced to disk wrapped by a platform key. This allows them to be exported to a remote
38 * service.
39 *
40 * @hide
41 */
42public class RecoverableKeyGenerator {
43
44    private static final String TAG = "PlatformKeyGen";
45    private static final int RESULT_CANNOT_INSERT_ROW = -1;
46    private static final String SECRET_KEY_ALGORITHM = "AES";
47
48    static final int KEY_SIZE_BITS = 256;
49
50    /**
51     * A new {@link RecoverableKeyGenerator} instance.
52     *
53     * @throws NoSuchAlgorithmException if "AES" key generation or "AES/GCM/NoPadding" cipher is
54     *     unavailable. Should never happen.
55     *
56     * @hide
57     */
58    public static RecoverableKeyGenerator newInstance(RecoverableKeyStoreDb database)
59            throws NoSuchAlgorithmException {
60        // NB: This cannot use AndroidKeyStore as the provider, as we need access to the raw key
61        // material, so that it can be synced to disk in encrypted form.
62        KeyGenerator keyGenerator = KeyGenerator.getInstance(SECRET_KEY_ALGORITHM);
63        return new RecoverableKeyGenerator(keyGenerator, database);
64    }
65
66    private final KeyGenerator mKeyGenerator;
67    private final RecoverableKeyStoreDb mDatabase;
68
69    private RecoverableKeyGenerator(
70            KeyGenerator keyGenerator,
71            RecoverableKeyStoreDb recoverableKeyStoreDb) {
72        mKeyGenerator = keyGenerator;
73        mDatabase = recoverableKeyStoreDb;
74    }
75
76    /**
77     * Generates a 256-bit AES key with the given alias.
78     *
79     * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
80     * persisted to disk so that it can be synced remotely, and then recovered on another device.
81     * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
82     *
83     * @param platformKey The user's platform key, with which to wrap the generated key.
84     * @param userId The user ID of the profile to which the calling app belongs.
85     * @param uid The uid of the application that will own the key.
86     * @param alias The alias by which the key will be known in the recoverable key store.
87     * @throws RecoverableKeyStorageException if there is some error persisting the key either to
88     *     the database.
89     * @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
90     * @throws InvalidKeyException if the platform key cannot be used to wrap keys.
91     *
92     * @hide
93     */
94    public byte[] generateAndStoreKey(
95            PlatformEncryptionKey platformKey, int userId, int uid, String alias)
96            throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
97        mKeyGenerator.init(KEY_SIZE_BITS);
98        SecretKey key = mKeyGenerator.generateKey();
99
100        WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key);
101        long result = mDatabase.insertKey(userId, uid, alias, wrappedKey);
102
103        if (result == RESULT_CANNOT_INSERT_ROW) {
104            throw new RecoverableKeyStorageException(
105                    String.format(
106                            Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
107        }
108
109        long updatedRows = mDatabase.setShouldCreateSnapshot(userId, uid, true);
110        if (updatedRows < 0) {
111            Log.e(TAG, "Failed to set the shoudCreateSnapshot flag in the local DB.");
112        }
113
114        return key.getEncoded();
115    }
116
117    /**
118     * Imports an AES key with the given alias.
119     *
120     * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
121     * persisted to disk so that it can be synced remotely, and then recovered on another device.
122     * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
123     *
124     * @param platformKey The user's platform key, with which to wrap the generated key.
125     * @param userId The user ID of the profile to which the calling app belongs.
126     * @param uid The uid of the application that will own the key.
127     * @param alias The alias by which the key will be known in the recoverable key store.
128     * @param keyBytes The raw bytes of the AES key to be imported.
129     * @throws RecoverableKeyStorageException if there is some error persisting the key either to
130     *     the database.
131     * @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
132     * @throws InvalidKeyException if the platform key cannot be used to wrap keys.
133     *
134     * @hide
135     */
136    public void importKey(
137            @NonNull PlatformEncryptionKey platformKey, int userId, int uid, @NonNull String alias,
138            @NonNull byte[] keyBytes)
139            throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
140        SecretKey key = new SecretKeySpec(keyBytes, SECRET_KEY_ALGORITHM);
141
142        WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key);
143        long result = mDatabase.insertKey(userId, uid, alias, wrappedKey);
144
145        if (result == RESULT_CANNOT_INSERT_ROW) {
146            throw new RecoverableKeyStorageException(
147                    String.format(
148                            Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
149        }
150
151        mDatabase.setShouldCreateSnapshot(userId, uid, true);
152    }
153}
154