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.security.keystore.recovery.RecoveryController;
20import android.util.Log;
21
22import java.security.InvalidAlgorithmParameterException;
23import java.security.InvalidKeyException;
24import java.security.KeyStoreException;
25import java.security.NoSuchAlgorithmException;
26import java.util.HashMap;
27import java.util.Locale;
28import java.util.Map;
29
30import javax.crypto.Cipher;
31import javax.crypto.IllegalBlockSizeException;
32import javax.crypto.NoSuchPaddingException;
33import javax.crypto.SecretKey;
34import javax.crypto.spec.GCMParameterSpec;
35
36/**
37 * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding.
38 *
39 * @hide
40 */
41public class WrappedKey {
42    private static final String TAG = "WrappedKey";
43
44    private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
45    private static final String APPLICATION_KEY_ALGORITHM = "AES";
46    private static final int GCM_TAG_LENGTH_BITS = 128;
47
48    private final int mPlatformKeyGenerationId;
49    private final int mRecoveryStatus;
50    private final byte[] mNonce;
51    private final byte[] mKeyMaterial;
52
53    /**
54     * Returns a wrapped form of {@code key}, using {@code wrappingKey} to encrypt the key material.
55     *
56     * @throws InvalidKeyException if {@code wrappingKey} cannot be used to encrypt {@code key}, or
57     *     if {@code key} does not expose its key material. See
58     *     {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does
59     *     not expose its key material.
60     */
61    public static WrappedKey fromSecretKey(PlatformEncryptionKey wrappingKey, SecretKey key)
62            throws InvalidKeyException, KeyStoreException {
63        if (key.getEncoded() == null) {
64            throw new InvalidKeyException(
65                    "key does not expose encoded material. It cannot be wrapped.");
66        }
67
68        Cipher cipher;
69        try {
70            cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
71        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
72            throw new RuntimeException(
73                    "Android does not support AES/GCM/NoPadding. This should never happen.");
74        }
75
76        cipher.init(Cipher.WRAP_MODE, wrappingKey.getKey());
77        byte[] encryptedKeyMaterial;
78        try {
79            encryptedKeyMaterial = cipher.wrap(key);
80        } catch (IllegalBlockSizeException e) {
81            Throwable cause = e.getCause();
82            if (cause instanceof KeyStoreException) {
83                // If AndroidKeyStore encounters any error here, it throws IllegalBlockSizeException
84                // with KeyStoreException as the cause. This is due to there being no better option
85                // here, as the Cipher#wrap only checked throws InvalidKeyException or
86                // IllegalBlockSizeException. If this is the case, we want to propagate it to the
87                // caller, so rethrow the cause.
88                throw (KeyStoreException) cause;
89            } else {
90                throw new RuntimeException(
91                        "IllegalBlockSizeException should not be thrown by AES/GCM/NoPadding mode.",
92                        e);
93            }
94        }
95
96        return new WrappedKey(
97                /*nonce=*/ cipher.getIV(),
98                /*keyMaterial=*/ encryptedKeyMaterial,
99                /*platformKeyGenerationId=*/ wrappingKey.getGenerationId(),
100                RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
101    }
102
103    /**
104     * A new instance with default recovery status.
105     *
106     * @param nonce The nonce with which the key material was encrypted.
107     * @param keyMaterial The encrypted bytes of the key material.
108     * @param platformKeyGenerationId The generation ID of the key used to wrap this key.
109     *
110     * @see RecoveryController#RECOVERY_STATUS_SYNC_IN_PROGRESS
111     * @hide
112     */
113    public WrappedKey(byte[] nonce, byte[] keyMaterial, int platformKeyGenerationId) {
114        mNonce = nonce;
115        mKeyMaterial = keyMaterial;
116        mPlatformKeyGenerationId = platformKeyGenerationId;
117        mRecoveryStatus = RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS;
118    }
119
120    /**
121     * A new instance.
122     *
123     * @param nonce The nonce with which the key material was encrypted.
124     * @param keyMaterial The encrypted bytes of the key material.
125     * @param platformKeyGenerationId The generation ID of the key used to wrap this key.
126     * @param recoveryStatus recovery status of the key.
127     *
128     * @hide
129     */
130    public WrappedKey(byte[] nonce, byte[] keyMaterial, int platformKeyGenerationId,
131            int recoveryStatus) {
132        mNonce = nonce;
133        mKeyMaterial = keyMaterial;
134        mPlatformKeyGenerationId = platformKeyGenerationId;
135        mRecoveryStatus = recoveryStatus;
136    }
137
138    /**
139     * Returns the nonce with which the key material was encrypted.
140     *
141     * @hide
142     */
143    public byte[] getNonce() {
144        return mNonce;
145    }
146
147    /**
148     * Returns the encrypted key material.
149     *
150     * @hide
151     */
152    public byte[] getKeyMaterial() {
153        return mKeyMaterial;
154    }
155
156    /**
157     * Returns the generation ID of the platform key, with which this key was wrapped.
158     *
159     * @hide
160     */
161    public int getPlatformKeyGenerationId() {
162        return mPlatformKeyGenerationId;
163    }
164
165    /**
166     * Returns recovery status of the key.
167     *
168     * @hide
169     */
170    public int getRecoveryStatus() {
171        return mRecoveryStatus;
172    }
173
174    /**
175     * Unwraps the {@code wrappedKeys} with the {@code platformKey}.
176     *
177     * @return The unwrapped keys, indexed by alias.
178     * @throws NoSuchAlgorithmException if AES/GCM/NoPadding Cipher or AES key type is unavailable.
179     * @throws BadPlatformKeyException if the {@code platformKey} has a different generation ID to
180     *     any of the {@code wrappedKeys}.
181     *
182     * @hide
183     */
184    public static Map<String, SecretKey> unwrapKeys(
185            PlatformDecryptionKey platformKey,
186            Map<String, WrappedKey> wrappedKeys)
187            throws NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException,
188            InvalidKeyException, InvalidAlgorithmParameterException {
189        HashMap<String, SecretKey> unwrappedKeys = new HashMap<>();
190        Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
191        int platformKeyGenerationId = platformKey.getGenerationId();
192
193        for (String alias : wrappedKeys.keySet()) {
194            WrappedKey wrappedKey = wrappedKeys.get(alias);
195            if (wrappedKey.getPlatformKeyGenerationId() != platformKeyGenerationId) {
196                throw new BadPlatformKeyException(String.format(
197                        Locale.US,
198                        "WrappedKey with alias '%s' was wrapped with platform key %d, not "
199                                + "platform key %d",
200                        alias,
201                        wrappedKey.getPlatformKeyGenerationId(),
202                        platformKey.getGenerationId()));
203            }
204
205            cipher.init(
206                    Cipher.UNWRAP_MODE,
207                    platformKey.getKey(),
208                    new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
209            SecretKey key;
210            try {
211                key = (SecretKey) cipher.unwrap(
212                        wrappedKey.getKeyMaterial(), APPLICATION_KEY_ALGORITHM, Cipher.SECRET_KEY);
213            } catch (InvalidKeyException | NoSuchAlgorithmException e) {
214                Log.e(TAG,
215                        String.format(
216                                Locale.US,
217                                "Error unwrapping recoverable key with alias '%s'",
218                                alias),
219                        e);
220                continue;
221            }
222            unwrappedKeys.put(alias, key);
223        }
224
225        return unwrappedKeys;
226    }
227}
228