/*
|
* 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;
|
}
|
}
|