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.security.keystore.recovery.RecoveryController; 20 import android.util.Log; 21 22 import java.security.InvalidAlgorithmParameterException; 23 import java.security.InvalidKeyException; 24 import java.security.KeyStoreException; 25 import java.security.NoSuchAlgorithmException; 26 import java.util.HashMap; 27 import java.util.Locale; 28 import java.util.Map; 29 30 import javax.crypto.Cipher; 31 import javax.crypto.IllegalBlockSizeException; 32 import javax.crypto.NoSuchPaddingException; 33 import javax.crypto.SecretKey; 34 import javax.crypto.spec.GCMParameterSpec; 35 36 /** 37 * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding. 38 * 39 * @hide 40 */ 41 public 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