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