Home | History | Annotate | Download | only in recoverablekeystore
      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