blob: 09d7541102b52f58032239ae1da64a720c7460bd [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.locksettings.recoverablekeystore;
import android.annotation.Nullable;
import android.security.keystore.recovery.RecoveryController;
import android.util.Log;
import android.util.Pair;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
/**
* A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding.
*
* @hide
*/
public class WrappedKey {
private static final String TAG = "WrappedKey";
private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final String APPLICATION_KEY_ALGORITHM = "AES";
private static final int GCM_TAG_LENGTH_BITS = 128;
private final int mPlatformKeyGenerationId;
private final int mRecoveryStatus;
private final byte[] mNonce;
private final byte[] mKeyMaterial;
private final byte[] mKeyMetadata;
/**
* Returns a wrapped form of {@code key}, using {@code wrappingKey} to encrypt the key material.
*
* @throws InvalidKeyException if {@code wrappingKey} cannot be used to encrypt {@code key}, or
* if {@code key} does not expose its key material. See
* {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does
* not expose its key material.
*/
public static WrappedKey fromSecretKey(PlatformEncryptionKey wrappingKey, SecretKey key,
@Nullable byte[] metadata)
throws InvalidKeyException, KeyStoreException {
if (key.getEncoded() == null) {
throw new InvalidKeyException(
"key does not expose encoded material. It cannot be wrapped.");
}
Cipher cipher;
try {
cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(
"Android does not support AES/GCM/NoPadding. This should never happen.");
}
cipher.init(Cipher.WRAP_MODE, wrappingKey.getKey());
byte[] encryptedKeyMaterial;
try {
encryptedKeyMaterial = cipher.wrap(key);
} catch (IllegalBlockSizeException e) {
Throwable cause = e.getCause();
if (cause instanceof KeyStoreException) {
// If AndroidKeyStore encounters any error here, it throws IllegalBlockSizeException
// with KeyStoreException as the cause. This is due to there being no better option
// here, as the Cipher#wrap only checked throws InvalidKeyException or
// IllegalBlockSizeException. If this is the case, we want to propagate it to the
// caller, so rethrow the cause.
throw (KeyStoreException) cause;
} else {
throw new RuntimeException(
"IllegalBlockSizeException should not be thrown by AES/GCM/NoPadding mode.",
e);
}
}
return new WrappedKey(
/*nonce=*/ cipher.getIV(),
/*keyMaterial=*/ encryptedKeyMaterial,
/*keyMetadata=*/ metadata,
/*platformKeyGenerationId=*/ wrappingKey.getGenerationId(),
RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
}
/**
* A new instance with default recovery status.
*
* @param nonce The nonce with which the key material was encrypted.
* @param keyMaterial The encrypted bytes of the key material.
* @param platformKeyGenerationId The generation ID of the key used to wrap this key.
*
* @see RecoveryController#RECOVERY_STATUS_SYNC_IN_PROGRESS
* @hide
*/
public WrappedKey(byte[] nonce, byte[] keyMaterial, @Nullable byte[] keyMetadata,
int platformKeyGenerationId) {
this(nonce, keyMaterial, keyMetadata, platformKeyGenerationId,
RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
}
/**
* A new instance.
*
* @param nonce The nonce with which the key material was encrypted.
* @param keyMaterial The encrypted bytes of the key material.
* @param keyMetadata The metadata that will be authenticated (but unencrypted) together with
* the key material when the key is uploaded to cloud.
* @param platformKeyGenerationId The generation ID of the key used to wrap this key.
* @param recoveryStatus recovery status of the key.
*
* @hide
*/
public WrappedKey(byte[] nonce, byte[] keyMaterial, @Nullable byte[] keyMetadata,
int platformKeyGenerationId, int recoveryStatus) {
mNonce = nonce;
mKeyMaterial = keyMaterial;
mKeyMetadata = keyMetadata;
mPlatformKeyGenerationId = platformKeyGenerationId;
mRecoveryStatus = recoveryStatus;
}
/**
* Returns the nonce with which the key material was encrypted.
*
* @hide
*/
public byte[] getNonce() {
return mNonce;
}
/**
* Returns the encrypted key material.
*
* @hide
*/
public byte[] getKeyMaterial() {
return mKeyMaterial;
}
/**
* Returns the key metadata.
*
* @hide
*/
public @Nullable byte[] getKeyMetadata() {
return mKeyMetadata;
}
/**
* Returns the generation ID of the platform key, with which this key was wrapped.
*
* @hide
*/
public int getPlatformKeyGenerationId() {
return mPlatformKeyGenerationId;
}
/**
* Returns recovery status of the key.
*
* @hide
*/
public int getRecoveryStatus() {
return mRecoveryStatus;
}
/**
* Unwraps the {@code wrappedKeys} with the {@code platformKey}.
*
* @return The unwrapped keys, indexed by alias.
* @throws NoSuchAlgorithmException if AES/GCM/NoPadding Cipher or AES key type is unavailable.
* @throws BadPlatformKeyException if the {@code platformKey} has a different generation ID to
* any of the {@code wrappedKeys}.
*
* @hide
*/
public static Map<String, Pair<SecretKey, byte[]>> unwrapKeys(
PlatformDecryptionKey platformKey,
Map<String, WrappedKey> wrappedKeys)
throws NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException,
InvalidKeyException, InvalidAlgorithmParameterException {
HashMap<String, Pair<SecretKey, byte[]>> unwrappedKeys = new HashMap<>();
Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
int platformKeyGenerationId = platformKey.getGenerationId();
for (String alias : wrappedKeys.keySet()) {
WrappedKey wrappedKey = wrappedKeys.get(alias);
if (wrappedKey.getPlatformKeyGenerationId() != platformKeyGenerationId) {
throw new BadPlatformKeyException(String.format(
Locale.US,
"WrappedKey with alias '%s' was wrapped with platform key %d, not "
+ "platform key %d",
alias,
wrappedKey.getPlatformKeyGenerationId(),
platformKey.getGenerationId()));
}
cipher.init(
Cipher.UNWRAP_MODE,
platformKey.getKey(),
new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
SecretKey key;
try {
key = (SecretKey) cipher.unwrap(
wrappedKey.getKeyMaterial(), APPLICATION_KEY_ALGORITHM, Cipher.SECRET_KEY);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
Log.e(TAG,
String.format(
Locale.US,
"Error unwrapping recoverable key with alias '%s'",
alias),
e);
continue;
}
unwrappedKeys.put(alias, Pair.create(key, wrappedKey.getKeyMetadata()));
}
return unwrappedKeys;
}
}