blob: 51c42520ccc9657a7788396e47dd86600ef76db0 [file] [log] [blame]
/*
* Copyright (C) 2012 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 android.security.keystore;
import android.security.Credentials;
import android.security.GateKeeper;
import android.security.KeyStore;
import android.security.KeyStoreParameter;
import android.security.keymaster.KeyCharacteristics;
import android.security.keymaster.KeymasterArguments;
import android.security.keymaster.KeymasterDefs;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
import android.security.keystore.SecureKeyImportUnavailableException;
import android.security.keystore.WrappedKeyEntry;
import android.util.Log;
import libcore.util.EmptyArray;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyStore.Entry;
import java.security.KeyStore.LoadStoreParameter;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStore.ProtectionParameter;
import java.security.KeyStore.SecretKeyEntry;
import java.security.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.ProviderException;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.crypto.SecretKey;
/**
* A java.security.KeyStore interface for the Android KeyStore. An instance of
* it can be created via the {@link java.security.KeyStore#getInstance(String)
* KeyStore.getInstance("AndroidKeyStore")} interface. This returns a
* java.security.KeyStore backed by this "AndroidKeyStore" implementation.
* <p>
* This is built on top of Android's keystore daemon. The convention of alias
* use is:
* <p>
* PrivateKeyEntry will have a Credentials.USER_PRIVATE_KEY as the private key,
* Credentials.USER_CERTIFICATE as the first certificate in the chain (the one
* that corresponds to the private key), and then a Credentials.CA_CERTIFICATE
* entry which will have the rest of the chain concatenated in BER format.
* <p>
* TrustedCertificateEntry will just have a Credentials.CA_CERTIFICATE entry
* with a single certificate.
*
* @hide
*/
public class AndroidKeyStoreSpi extends KeyStoreSpi {
public static final String NAME = "AndroidKeyStore";
private KeyStore mKeyStore;
private int mUid = KeyStore.UID_SELF;
@Override
public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException,
UnrecoverableKeyException {
String userKeyAlias = Credentials.USER_PRIVATE_KEY + alias;
AndroidKeyStoreKey key;
if (!mKeyStore.contains(userKeyAlias, mUid)) {
// try legacy prefix for backward compatibility
userKeyAlias = Credentials.USER_SECRET_KEY + alias;
if (!mKeyStore.contains(userKeyAlias, mUid)) return null;
}
try {
key = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore,
userKeyAlias,
mUid);
} catch (KeyPermanentlyInvalidatedException e) {
throw new UnrecoverableKeyException(e.getMessage());
}
return key;
}
@Override
public Certificate[] engineGetCertificateChain(String alias) {
if (alias == null) {
throw new NullPointerException("alias == null");
}
final X509Certificate leaf = (X509Certificate) engineGetCertificate(alias);
if (leaf == null) {
return null;
}
final Certificate[] caList;
// Suppress the key not found warning for this call. It seems that this error is exclusively
// being thrown when there is a self signed certificate chain, so when the keystore service
// attempts to query for the CA details, it obviously fails to find them and returns a
// key not found exception. This is WAI, and throwing a stack trace here can be very
// misleading since the trace is not clear.
final byte[] caBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias,
mUid,
true /* suppressKeyNotFoundWarning */);
if (caBytes != null) {
final Collection<X509Certificate> caChain = toCertificates(caBytes);
caList = new Certificate[caChain.size() + 1];
final Iterator<X509Certificate> it = caChain.iterator();
int i = 1;
while (it.hasNext()) {
caList[i++] = it.next();
}
} else {
caList = new Certificate[1];
}
caList[0] = leaf;
return caList;
}
@Override
public Certificate engineGetCertificate(String alias) {
if (alias == null) {
throw new NullPointerException("alias == null");
}
byte[] encodedCert = mKeyStore.get(Credentials.USER_CERTIFICATE + alias, mUid);
if (encodedCert != null) {
return getCertificateForPrivateKeyEntry(alias, encodedCert);
}
encodedCert = mKeyStore.get(Credentials.CA_CERTIFICATE + alias, mUid);
if (encodedCert != null) {
return getCertificateForTrustedCertificateEntry(encodedCert);
}
// This entry/alias does not contain a certificate.
return null;
}
private Certificate getCertificateForTrustedCertificateEntry(byte[] encodedCert) {
// For this certificate there shouldn't be a private key in this KeyStore entry. Thus,
// there's no need to wrap this certificate as opposed to the certificate associated with
// a private key entry.
return toCertificate(encodedCert);
}
private Certificate getCertificateForPrivateKeyEntry(String alias, byte[] encodedCert) {
// All crypto algorithms offered by Android Keystore for its private keys must also
// be offered for the corresponding public keys stored in the Android Keystore. The
// complication is that the underlying keystore service operates only on full key pairs,
// rather than just public keys or private keys. As a result, Android Keystore-backed
// crypto can only be offered for public keys for which keystore contains the
// corresponding private key. This is not the case for certificate-only entries (e.g.,
// trusted certificates).
//
// getCertificate().getPublicKey() is the only way to obtain the public key
// corresponding to the private key stored in the KeyStore. Thus, we need to make sure
// that the returned public key points to the underlying key pair / private key
// when available.
X509Certificate cert = toCertificate(encodedCert);
if (cert == null) {
// Failed to parse the certificate.
return null;
}
String privateKeyAlias = Credentials.USER_PRIVATE_KEY + alias;
if (mKeyStore.contains(privateKeyAlias, mUid)) {
// As expected, keystore contains the private key corresponding to this public key. Wrap
// the certificate so that its getPublicKey method returns an Android Keystore
// PublicKey. This key will delegate crypto operations involving this public key to
// Android Keystore when higher-priority providers do not offer these crypto
// operations for this key.
return wrapIntoKeyStoreCertificate(privateKeyAlias, mUid, cert);
} else {
// This KeyStore entry/alias is supposed to contain the private key corresponding to
// the public key in this certificate, but it does not for some reason. It's probably a
// bug. Let other providers handle crypto operations involving the public key returned
// by this certificate's getPublicKey.
return cert;
}
}
/**
* Wraps the provided cerificate into {@link KeyStoreX509Certificate} so that the public key
* returned by the certificate contains information about the alias of the private key in
* keystore. This is needed so that Android Keystore crypto operations using public keys can
* find out which key alias to use. These operations cannot work without an alias.
*/
private static KeyStoreX509Certificate wrapIntoKeyStoreCertificate(
String privateKeyAlias, int uid, X509Certificate certificate) {
return (certificate != null)
? new KeyStoreX509Certificate(privateKeyAlias, uid, certificate) : null;
}
private static X509Certificate toCertificate(byte[] bytes) {
try {
final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
return (X509Certificate) certFactory.generateCertificate(
new ByteArrayInputStream(bytes));
} catch (CertificateException e) {
Log.w(NAME, "Couldn't parse certificate in keystore", e);
return null;
}
}
@SuppressWarnings("unchecked")
private static Collection<X509Certificate> toCertificates(byte[] bytes) {
try {
final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
return (Collection<X509Certificate>) certFactory.generateCertificates(
new ByteArrayInputStream(bytes));
} catch (CertificateException e) {
Log.w(NAME, "Couldn't parse certificates in keystore", e);
return new ArrayList<X509Certificate>();
}
}
private Date getModificationDate(String alias) {
final long epochMillis = mKeyStore.getmtime(alias, mUid);
if (epochMillis == -1L) {
return null;
}
return new Date(epochMillis);
}
@Override
public Date engineGetCreationDate(String alias) {
if (alias == null) {
throw new NullPointerException("alias == null");
}
Date d = getModificationDate(Credentials.USER_PRIVATE_KEY + alias);
if (d != null) {
return d;
}
d = getModificationDate(Credentials.USER_SECRET_KEY + alias);
if (d != null) {
return d;
}
d = getModificationDate(Credentials.USER_CERTIFICATE + alias);
if (d != null) {
return d;
}
return getModificationDate(Credentials.CA_CERTIFICATE + alias);
}
@Override
public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
throws KeyStoreException {
if ((password != null) && (password.length > 0)) {
throw new KeyStoreException("entries cannot be protected with passwords");
}
if (key instanceof PrivateKey) {
setPrivateKeyEntry(alias, (PrivateKey) key, chain, null);
} else if (key instanceof SecretKey) {
setSecretKeyEntry(alias, (SecretKey) key, null);
} else {
throw new KeyStoreException("Only PrivateKey and SecretKey are supported");
}
}
private static KeyProtection getLegacyKeyProtectionParameter(PrivateKey key)
throws KeyStoreException {
String keyAlgorithm = key.getAlgorithm();
KeyProtection.Builder specBuilder;
if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) {
specBuilder =
new KeyProtection.Builder(
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY);
// Authorized to be used with any digest (including no digest).
// MD5 was never offered for Android Keystore for ECDSA.
specBuilder.setDigests(
KeyProperties.DIGEST_NONE,
KeyProperties.DIGEST_SHA1,
KeyProperties.DIGEST_SHA224,
KeyProperties.DIGEST_SHA256,
KeyProperties.DIGEST_SHA384,
KeyProperties.DIGEST_SHA512);
} else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) {
specBuilder =
new KeyProtection.Builder(
KeyProperties.PURPOSE_ENCRYPT
| KeyProperties.PURPOSE_DECRYPT
| KeyProperties.PURPOSE_SIGN
| KeyProperties.PURPOSE_VERIFY);
// Authorized to be used with any digest (including no digest).
specBuilder.setDigests(
KeyProperties.DIGEST_NONE,
KeyProperties.DIGEST_MD5,
KeyProperties.DIGEST_SHA1,
KeyProperties.DIGEST_SHA224,
KeyProperties.DIGEST_SHA256,
KeyProperties.DIGEST_SHA384,
KeyProperties.DIGEST_SHA512);
// Authorized to be used with any encryption and signature padding
// schemes (including no padding).
specBuilder.setEncryptionPaddings(
KeyProperties.ENCRYPTION_PADDING_NONE,
KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1,
KeyProperties.ENCRYPTION_PADDING_RSA_OAEP);
specBuilder.setSignaturePaddings(
KeyProperties.SIGNATURE_PADDING_RSA_PKCS1,
KeyProperties.SIGNATURE_PADDING_RSA_PSS);
// Disable randomized encryption requirement to support encryption
// padding NONE above.
specBuilder.setRandomizedEncryptionRequired(false);
} else {
throw new KeyStoreException("Unsupported key algorithm: " + keyAlgorithm);
}
specBuilder.setUserAuthenticationRequired(false);
return specBuilder.build();
}
private void setPrivateKeyEntry(String alias, PrivateKey key, Certificate[] chain,
java.security.KeyStore.ProtectionParameter param) throws KeyStoreException {
int flags = 0;
KeyProtection spec;
if (param == null) {
spec = getLegacyKeyProtectionParameter(key);
} else if (param instanceof KeyStoreParameter) {
spec = getLegacyKeyProtectionParameter(key);
KeyStoreParameter legacySpec = (KeyStoreParameter) param;
if (legacySpec.isEncryptionRequired()) {
flags = KeyStore.FLAG_ENCRYPTED;
}
} else if (param instanceof KeyProtection) {
spec = (KeyProtection) param;
if (spec.isCriticalToDeviceEncryption()) {
flags |= KeyStore.FLAG_CRITICAL_TO_DEVICE_ENCRYPTION;
}
if (spec.isStrongBoxBacked()) {
flags |= KeyStore.FLAG_STRONGBOX;
}
} else {
throw new KeyStoreException(
"Unsupported protection parameter class:" + param.getClass().getName()
+ ". Supported: " + KeyProtection.class.getName() + ", "
+ KeyStoreParameter.class.getName());
}
// Make sure the chain exists since this is a PrivateKey
if ((chain == null) || (chain.length == 0)) {
throw new KeyStoreException("Must supply at least one Certificate with PrivateKey");
}
// Do chain type checking.
X509Certificate[] x509chain = new X509Certificate[chain.length];
for (int i = 0; i < chain.length; i++) {
if (!"X.509".equals(chain[i].getType())) {
throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #"
+ i);
}
if (!(chain[i] instanceof X509Certificate)) {
throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #"
+ i);
}
x509chain[i] = (X509Certificate) chain[i];
}
final byte[] userCertBytes;
try {
userCertBytes = x509chain[0].getEncoded();
} catch (CertificateEncodingException e) {
throw new KeyStoreException("Failed to encode certificate #0", e);
}
/*
* If we have a chain, store it in the CA certificate slot for this
* alias as concatenated DER-encoded certificates. These can be
* deserialized by {@link CertificateFactory#generateCertificates}.
*/
final byte[] chainBytes;
if (chain.length > 1) {
/*
* The chain is passed in as {user_cert, ca_cert_1, ca_cert_2, ...}
* so we only need the certificates starting at index 1.
*/
final byte[][] certsBytes = new byte[x509chain.length - 1][];
int totalCertLength = 0;
for (int i = 0; i < certsBytes.length; i++) {
try {
certsBytes[i] = x509chain[i + 1].getEncoded();
totalCertLength += certsBytes[i].length;
} catch (CertificateEncodingException e) {
throw new KeyStoreException("Failed to encode certificate #" + i, e);
}
}
/*
* Serialize this into one byte array so we can later call
* CertificateFactory#generateCertificates to recover them.
*/
chainBytes = new byte[totalCertLength];
int outputOffset = 0;
for (int i = 0; i < certsBytes.length; i++) {
final int certLength = certsBytes[i].length;
System.arraycopy(certsBytes[i], 0, chainBytes, outputOffset, certLength);
outputOffset += certLength;
certsBytes[i] = null;
}
} else {
chainBytes = null;
}
final String pkeyAlias;
if (key instanceof AndroidKeyStorePrivateKey) {
pkeyAlias = ((AndroidKeyStoreKey) key).getAlias();
} else {
pkeyAlias = null;
}
byte[] pkcs8EncodedPrivateKeyBytes;
KeymasterArguments importArgs;
final boolean shouldReplacePrivateKey;
if (pkeyAlias != null && pkeyAlias.startsWith(Credentials.USER_PRIVATE_KEY)) {
final String keySubalias = pkeyAlias.substring(Credentials.USER_PRIVATE_KEY.length());
if (!alias.equals(keySubalias)) {
throw new KeyStoreException("Can only replace keys with same alias: " + alias
+ " != " + keySubalias);
}
shouldReplacePrivateKey = false;
importArgs = null;
pkcs8EncodedPrivateKeyBytes = null;
} else {
shouldReplacePrivateKey = true;
// Make sure the PrivateKey format is the one we support.
final String keyFormat = key.getFormat();
if ((keyFormat == null) || (!"PKCS#8".equals(keyFormat))) {
throw new KeyStoreException(
"Unsupported private key export format: " + keyFormat
+ ". Only private keys which export their key material in PKCS#8 format are"
+ " supported.");
}
// Make sure we can actually encode the key.
pkcs8EncodedPrivateKeyBytes = key.getEncoded();
if (pkcs8EncodedPrivateKeyBytes == null) {
throw new KeyStoreException("Private key did not export any key material");
}
importArgs = new KeymasterArguments();
try {
importArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM,
KeyProperties.KeyAlgorithm.toKeymasterAsymmetricKeyAlgorithm(
key.getAlgorithm()));
@KeyProperties.PurposeEnum int purposes = spec.getPurposes();
importArgs.addEnums(KeymasterDefs.KM_TAG_PURPOSE,
KeyProperties.Purpose.allToKeymaster(purposes));
if (spec.isDigestsSpecified()) {
importArgs.addEnums(KeymasterDefs.KM_TAG_DIGEST,
KeyProperties.Digest.allToKeymaster(spec.getDigests()));
}
importArgs.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE,
KeyProperties.BlockMode.allToKeymaster(spec.getBlockModes()));
int[] keymasterEncryptionPaddings =
KeyProperties.EncryptionPadding.allToKeymaster(
spec.getEncryptionPaddings());
if (((purposes & KeyProperties.PURPOSE_ENCRYPT) != 0)
&& (spec.isRandomizedEncryptionRequired())) {
for (int keymasterPadding : keymasterEncryptionPaddings) {
if (!KeymasterUtils
.isKeymasterPaddingSchemeIndCpaCompatibleWithAsymmetricCrypto(
keymasterPadding)) {
throw new KeyStoreException(
"Randomized encryption (IND-CPA) required but is violated by"
+ " encryption padding mode: "
+ KeyProperties.EncryptionPadding.fromKeymaster(
keymasterPadding)
+ ". See KeyProtection documentation.");
}
}
}
importArgs.addEnums(KeymasterDefs.KM_TAG_PADDING, keymasterEncryptionPaddings);
importArgs.addEnums(KeymasterDefs.KM_TAG_PADDING,
KeyProperties.SignaturePadding.allToKeymaster(spec.getSignaturePaddings()));
KeymasterUtils.addUserAuthArgs(importArgs, spec);
importArgs.addDateIfNotNull(KeymasterDefs.KM_TAG_ACTIVE_DATETIME,
spec.getKeyValidityStart());
importArgs.addDateIfNotNull(KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME,
spec.getKeyValidityForOriginationEnd());
importArgs.addDateIfNotNull(KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME,
spec.getKeyValidityForConsumptionEnd());
} catch (IllegalArgumentException | IllegalStateException e) {
throw new KeyStoreException(e);
}
}
boolean success = false;
try {
// Store the private key, if necessary
if (shouldReplacePrivateKey) {
// Delete the stored private key and any related entries before importing the
// provided key
Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid);
KeyCharacteristics resultingKeyCharacteristics = new KeyCharacteristics();
int errorCode = mKeyStore.importKey(
Credentials.USER_PRIVATE_KEY + alias,
importArgs,
KeymasterDefs.KM_KEY_FORMAT_PKCS8,
pkcs8EncodedPrivateKeyBytes,
mUid,
flags,
resultingKeyCharacteristics);
if (errorCode != KeyStore.NO_ERROR) {
throw new KeyStoreException("Failed to store private key",
KeyStore.getKeyStoreException(errorCode));
}
} else {
// Keep the stored private key around -- delete all other entry types
Credentials.deleteCertificateTypesForAlias(mKeyStore, alias, mUid);
Credentials.deleteLegacyKeyForAlias(mKeyStore, alias, mUid);
}
// Store the leaf certificate
int errorCode = mKeyStore.insert(Credentials.USER_CERTIFICATE + alias, userCertBytes,
mUid, flags);
if (errorCode != KeyStore.NO_ERROR) {
throw new KeyStoreException("Failed to store certificate #0",
KeyStore.getKeyStoreException(errorCode));
}
// Store the certificate chain
errorCode = mKeyStore.insert(Credentials.CA_CERTIFICATE + alias, chainBytes,
mUid, flags);
if (errorCode != KeyStore.NO_ERROR) {
throw new KeyStoreException("Failed to store certificate chain",
KeyStore.getKeyStoreException(errorCode));
}
success = true;
} finally {
if (!success) {
if (shouldReplacePrivateKey) {
Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid);
} else {
Credentials.deleteCertificateTypesForAlias(mKeyStore, alias, mUid);
Credentials.deleteLegacyKeyForAlias(mKeyStore, alias, mUid);
}
}
}
}
private void setSecretKeyEntry(String entryAlias, SecretKey key,
java.security.KeyStore.ProtectionParameter param)
throws KeyStoreException {
if ((param != null) && (!(param instanceof KeyProtection))) {
throw new KeyStoreException(
"Unsupported protection parameter class: " + param.getClass().getName()
+ ". Supported: " + KeyProtection.class.getName());
}
KeyProtection params = (KeyProtection) param;
if (key instanceof AndroidKeyStoreSecretKey) {
// KeyStore-backed secret key. It cannot be duplicated into another entry and cannot
// overwrite its own entry.
String keyAliasInKeystore = ((AndroidKeyStoreSecretKey) key).getAlias();
if (keyAliasInKeystore == null) {
throw new KeyStoreException("KeyStore-backed secret key does not have an alias");
}
String keyAliasPrefix = Credentials.USER_PRIVATE_KEY;
if (!keyAliasInKeystore.startsWith(keyAliasPrefix)) {
// try legacy prefix
keyAliasPrefix = Credentials.USER_SECRET_KEY;
if (!keyAliasInKeystore.startsWith(keyAliasPrefix)) {
throw new KeyStoreException("KeyStore-backed secret key has invalid alias: "
+ keyAliasInKeystore);
}
}
String keyEntryAlias =
keyAliasInKeystore.substring(keyAliasPrefix.length());
if (!entryAlias.equals(keyEntryAlias)) {
throw new KeyStoreException("Can only replace KeyStore-backed keys with same"
+ " alias: " + entryAlias + " != " + keyEntryAlias);
}
// This is the entry where this key is already stored. No need to do anything.
if (params != null) {
throw new KeyStoreException("Modifying KeyStore-backed key using protection"
+ " parameters not supported");
}
return;
}
if (params == null) {
throw new KeyStoreException(
"Protection parameters must be specified when importing a symmetric key");
}
// Not a KeyStore-backed secret key -- import its key material into keystore.
String keyExportFormat = key.getFormat();
if (keyExportFormat == null) {
throw new KeyStoreException(
"Only secret keys that export their key material are supported");
} else if (!"RAW".equals(keyExportFormat)) {
throw new KeyStoreException(
"Unsupported secret key material export format: " + keyExportFormat);
}
byte[] keyMaterial = key.getEncoded();
if (keyMaterial == null) {
throw new KeyStoreException("Key did not export its key material despite supporting"
+ " RAW format export");
}
KeymasterArguments args = new KeymasterArguments();
try {
int keymasterAlgorithm =
KeyProperties.KeyAlgorithm.toKeymasterSecretKeyAlgorithm(key.getAlgorithm());
args.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, keymasterAlgorithm);
int[] keymasterDigests;
if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC) {
// JCA HMAC key algorithm implies a digest (e.g., HmacSHA256 key algorithm
// implies SHA-256 digest). Because keymaster HMAC key is authorized only for one
// digest, we don't let import parameters override the digest implied by the key.
// If the parameters specify digests at all, they must specify only one digest, the
// only implied by key algorithm.
int keymasterImpliedDigest =
KeyProperties.KeyAlgorithm.toKeymasterDigest(key.getAlgorithm());
if (keymasterImpliedDigest == -1) {
throw new ProviderException(
"HMAC key algorithm digest unknown for key algorithm "
+ key.getAlgorithm());
}
keymasterDigests = new int[] {keymasterImpliedDigest};
if (params.isDigestsSpecified()) {
// Digest(s) explicitly specified in params -- check that the list consists of
// exactly one digest, the one implied by key algorithm.
int[] keymasterDigestsFromParams =
KeyProperties.Digest.allToKeymaster(params.getDigests());
if ((keymasterDigestsFromParams.length != 1)
|| (keymasterDigestsFromParams[0] != keymasterImpliedDigest)) {
throw new KeyStoreException(
"Unsupported digests specification: "
+ Arrays.asList(params.getDigests()) + ". Only "
+ KeyProperties.Digest.fromKeymaster(keymasterImpliedDigest)
+ " supported for HMAC key algorithm " + key.getAlgorithm());
}
}
} else {
// Key algorithm does not imply a digest.
if (params.isDigestsSpecified()) {
keymasterDigests = KeyProperties.Digest.allToKeymaster(params.getDigests());
} else {
keymasterDigests = EmptyArray.INT;
}
}
args.addEnums(KeymasterDefs.KM_TAG_DIGEST, keymasterDigests);
@KeyProperties.PurposeEnum int purposes = params.getPurposes();
int[] keymasterBlockModes =
KeyProperties.BlockMode.allToKeymaster(params.getBlockModes());
if (((purposes & KeyProperties.PURPOSE_ENCRYPT) != 0)
&& (params.isRandomizedEncryptionRequired())) {
for (int keymasterBlockMode : keymasterBlockModes) {
if (!KeymasterUtils.isKeymasterBlockModeIndCpaCompatibleWithSymmetricCrypto(
keymasterBlockMode)) {
throw new KeyStoreException(
"Randomized encryption (IND-CPA) required but may be violated by"
+ " block mode: "
+ KeyProperties.BlockMode.fromKeymaster(keymasterBlockMode)
+ ". See KeyProtection documentation.");
}
}
}
args.addEnums(KeymasterDefs.KM_TAG_PURPOSE,
KeyProperties.Purpose.allToKeymaster(purposes));
args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, keymasterBlockModes);
if (params.getSignaturePaddings().length > 0) {
throw new KeyStoreException("Signature paddings not supported for symmetric keys");
}
int[] keymasterPaddings = KeyProperties.EncryptionPadding.allToKeymaster(
params.getEncryptionPaddings());
args.addEnums(KeymasterDefs.KM_TAG_PADDING, keymasterPaddings);
KeymasterUtils.addUserAuthArgs(args, params);
KeymasterUtils.addMinMacLengthAuthorizationIfNecessary(
args,
keymasterAlgorithm,
keymasterBlockModes,
keymasterDigests);
args.addDateIfNotNull(KeymasterDefs.KM_TAG_ACTIVE_DATETIME,
params.getKeyValidityStart());
args.addDateIfNotNull(KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME,
params.getKeyValidityForOriginationEnd());
args.addDateIfNotNull(KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME,
params.getKeyValidityForConsumptionEnd());
if (((purposes & KeyProperties.PURPOSE_ENCRYPT) != 0)
&& (!params.isRandomizedEncryptionRequired())) {
// Permit caller-provided IV when encrypting with this key
args.addBoolean(KeymasterDefs.KM_TAG_CALLER_NONCE);
}
} catch (IllegalArgumentException | IllegalStateException e) {
throw new KeyStoreException(e);
}
int flags = 0;
if (params.isCriticalToDeviceEncryption()) {
flags |= KeyStore.FLAG_CRITICAL_TO_DEVICE_ENCRYPTION;
}
if (params.isStrongBoxBacked()) {
flags |= KeyStore.FLAG_STRONGBOX;
}
Credentials.deleteAllTypesForAlias(mKeyStore, entryAlias, mUid);
String keyAliasInKeystore = Credentials.USER_PRIVATE_KEY + entryAlias;
int errorCode = mKeyStore.importKey(
keyAliasInKeystore,
args,
KeymasterDefs.KM_KEY_FORMAT_RAW,
keyMaterial,
mUid,
flags,
new KeyCharacteristics());
if (errorCode != KeyStore.NO_ERROR) {
throw new KeyStoreException("Failed to import secret key. Keystore error code: "
+ errorCode);
}
}
private void setWrappedKeyEntry(String alias, WrappedKeyEntry entry,
java.security.KeyStore.ProtectionParameter param) throws KeyStoreException {
if (param != null) {
throw new KeyStoreException("Protection parameters are specified inside wrapped keys");
}
byte[] maskingKey = new byte[32];
KeymasterArguments args = new KeymasterArguments();
String[] parts = entry.getTransformation().split("/");
String algorithm = parts[0];
if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(algorithm)) {
args.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA);
} else if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(algorithm)) {
args.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA);
}
if (parts.length > 1) {
String mode = parts[1];
if (KeyProperties.BLOCK_MODE_ECB.equalsIgnoreCase(mode)) {
args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, KeymasterDefs.KM_MODE_ECB);
} else if (KeyProperties.BLOCK_MODE_CBC.equalsIgnoreCase(mode)) {
args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, KeymasterDefs.KM_MODE_CBC);
} else if (KeyProperties.BLOCK_MODE_CTR.equalsIgnoreCase(mode)) {
args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, KeymasterDefs.KM_MODE_CTR);
} else if (KeyProperties.BLOCK_MODE_GCM.equalsIgnoreCase(mode)) {
args.addEnums(KeymasterDefs.KM_TAG_BLOCK_MODE, KeymasterDefs.KM_MODE_GCM);
}
}
if (parts.length > 2) {
String padding = parts[2];
if (KeyProperties.ENCRYPTION_PADDING_NONE.equalsIgnoreCase(padding)) {
// Noop
} else if (KeyProperties.ENCRYPTION_PADDING_PKCS7.equalsIgnoreCase(padding)) {
args.addEnums(KeymasterDefs.KM_TAG_PADDING, KeymasterDefs.KM_PAD_PKCS7);
} else if (KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1.equalsIgnoreCase(padding)) {
args.addEnums(KeymasterDefs.KM_TAG_PADDING,
KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_ENCRYPT);
} else if (KeyProperties.ENCRYPTION_PADDING_RSA_OAEP.equalsIgnoreCase(padding)) {
args.addEnums(KeymasterDefs.KM_TAG_PADDING, KeymasterDefs.KM_PAD_RSA_OAEP);
}
}
KeyGenParameterSpec spec = (KeyGenParameterSpec) entry.getAlgorithmParameterSpec();
if (spec.isDigestsSpecified()) {
String digest = spec.getDigests()[0];
if (KeyProperties.DIGEST_NONE.equalsIgnoreCase(digest)) {
// Noop
} else if (KeyProperties.DIGEST_MD5.equalsIgnoreCase(digest)) {
args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_MD5);
} else if (KeyProperties.DIGEST_SHA1.equalsIgnoreCase(digest)) {
args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA1);
} else if (KeyProperties.DIGEST_SHA224.equalsIgnoreCase(digest)) {
args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA_2_224);
} else if (KeyProperties.DIGEST_SHA256.equalsIgnoreCase(digest)) {
args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA_2_256);
} else if (KeyProperties.DIGEST_SHA384.equalsIgnoreCase(digest)) {
args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA_2_384);
} else if (KeyProperties.DIGEST_SHA512.equalsIgnoreCase(digest)) {
args.addEnums(KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_SHA_2_512);
}
}
int errorCode = mKeyStore.importWrappedKey(
Credentials.USER_PRIVATE_KEY + alias,
entry.getWrappedKeyBytes(),
Credentials.USER_PRIVATE_KEY + entry.getWrappingKeyAlias(),
maskingKey,
args,
GateKeeper.getSecureUserId(),
0, // FIXME fingerprint id?
mUid,
new KeyCharacteristics());
if (errorCode == KeymasterDefs.KM_ERROR_UNIMPLEMENTED) {
throw new SecureKeyImportUnavailableException("Could not import wrapped key");
} else if (errorCode != KeyStore.NO_ERROR) {
throw new KeyStoreException("Failed to import wrapped key. Keystore error code: "
+ errorCode);
}
}
@Override
public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain)
throws KeyStoreException {
throw new KeyStoreException("Operation not supported because key encoding is unknown");
}
@Override
public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
if (isKeyEntry(alias)) {
throw new KeyStoreException("Entry exists and is not a trusted certificate");
}
// We can't set something to null.
if (cert == null) {
throw new NullPointerException("cert == null");
}
final byte[] encoded;
try {
encoded = cert.getEncoded();
} catch (CertificateEncodingException e) {
throw new KeyStoreException(e);
}
if (!mKeyStore.put(Credentials.CA_CERTIFICATE + alias, encoded, mUid, KeyStore.FLAG_NONE)) {
throw new KeyStoreException("Couldn't insert certificate; is KeyStore initialized?");
}
}
@Override
public void engineDeleteEntry(String alias) throws KeyStoreException {
if (!Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid)) {
throw new KeyStoreException("Failed to delete entry: " + alias);
}
}
private Set<String> getUniqueAliases() {
final String[] rawAliases = mKeyStore.list("", mUid);
if (rawAliases == null) {
return new HashSet<String>();
}
final Set<String> aliases = new HashSet<String>(rawAliases.length);
for (String alias : rawAliases) {
final int idx = alias.indexOf('_');
if ((idx == -1) || (alias.length() <= idx)) {
Log.e(NAME, "invalid alias: " + alias);
continue;
}
aliases.add(new String(alias.substring(idx + 1)));
}
return aliases;
}
@Override
public Enumeration<String> engineAliases() {
return Collections.enumeration(getUniqueAliases());
}
@Override
public boolean engineContainsAlias(String alias) {
if (alias == null) {
throw new NullPointerException("alias == null");
}
return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias, mUid)
|| mKeyStore.contains(Credentials.USER_SECRET_KEY + alias, mUid)
|| mKeyStore.contains(Credentials.USER_CERTIFICATE + alias, mUid)
|| mKeyStore.contains(Credentials.CA_CERTIFICATE + alias, mUid);
}
@Override
public int engineSize() {
return getUniqueAliases().size();
}
@Override
public boolean engineIsKeyEntry(String alias) {
return isKeyEntry(alias);
}
private boolean isKeyEntry(String alias) {
return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias, mUid) ||
mKeyStore.contains(Credentials.USER_SECRET_KEY + alias, mUid);
}
private boolean isCertificateEntry(String alias) {
if (alias == null) {
throw new NullPointerException("alias == null");
}
return mKeyStore.contains(Credentials.CA_CERTIFICATE + alias, mUid);
}
@Override
public boolean engineIsCertificateEntry(String alias) {
return !isKeyEntry(alias) && isCertificateEntry(alias);
}
@Override
public String engineGetCertificateAlias(Certificate cert) {
if (cert == null) {
return null;
}
if (!"X.509".equalsIgnoreCase(cert.getType())) {
// Only X.509 certificates supported
return null;
}
byte[] targetCertBytes;
try {
targetCertBytes = cert.getEncoded();
} catch (CertificateEncodingException e) {
return null;
}
if (targetCertBytes == null) {
return null;
}
final Set<String> nonCaEntries = new HashSet<String>();
/*
* First scan the PrivateKeyEntry types. The KeyStoreSpi documentation
* says to only compare the first certificate in the chain which is
* equivalent to the USER_CERTIFICATE prefix for the Android keystore
* convention.
*/
final String[] certAliases = mKeyStore.list(Credentials.USER_CERTIFICATE, mUid);
if (certAliases != null) {
for (String alias : certAliases) {
final byte[] certBytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias, mUid);
if (certBytes == null) {
continue;
}
nonCaEntries.add(alias);
if (Arrays.equals(certBytes, targetCertBytes)) {
return alias;
}
}
}
/*
* Look at all the TrustedCertificateEntry types. Skip all the
* PrivateKeyEntry we looked at above.
*/
final String[] caAliases = mKeyStore.list(Credentials.CA_CERTIFICATE, mUid);
if (certAliases != null) {
for (String alias : caAliases) {
if (nonCaEntries.contains(alias)) {
continue;
}
final byte[] certBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias, mUid);
if (certBytes == null) {
continue;
}
if (Arrays.equals(certBytes, targetCertBytes)) {
return alias;
}
}
}
return null;
}
@Override
public void engineStore(OutputStream stream, char[] password) throws IOException,
NoSuchAlgorithmException, CertificateException {
throw new UnsupportedOperationException("Can not serialize AndroidKeyStore to OutputStream");
}
@Override
public void engineLoad(InputStream stream, char[] password) throws IOException,
NoSuchAlgorithmException, CertificateException {
if (stream != null) {
throw new IllegalArgumentException("InputStream not supported");
}
if (password != null) {
throw new IllegalArgumentException("password not supported");
}
// Unfortunate name collision.
mKeyStore = KeyStore.getInstance();
mUid = KeyStore.UID_SELF;
}
@Override
public void engineLoad(LoadStoreParameter param) throws IOException,
NoSuchAlgorithmException, CertificateException {
int uid = KeyStore.UID_SELF;
if (param != null) {
if (param instanceof AndroidKeyStoreLoadStoreParameter) {
uid = ((AndroidKeyStoreLoadStoreParameter) param).getUid();
} else {
throw new IllegalArgumentException(
"Unsupported param type: " + param.getClass());
}
}
mKeyStore = KeyStore.getInstance();
mUid = uid;
}
@Override
public void engineSetEntry(String alias, Entry entry, ProtectionParameter param)
throws KeyStoreException {
if (entry == null) {
throw new KeyStoreException("entry == null");
}
Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid);
if (entry instanceof java.security.KeyStore.TrustedCertificateEntry) {
java.security.KeyStore.TrustedCertificateEntry trE =
(java.security.KeyStore.TrustedCertificateEntry) entry;
engineSetCertificateEntry(alias, trE.getTrustedCertificate());
return;
}
if (entry instanceof PrivateKeyEntry) {
PrivateKeyEntry prE = (PrivateKeyEntry) entry;
setPrivateKeyEntry(alias, prE.getPrivateKey(), prE.getCertificateChain(), param);
} else if (entry instanceof SecretKeyEntry) {
SecretKeyEntry secE = (SecretKeyEntry) entry;
setSecretKeyEntry(alias, secE.getSecretKey(), param);
} else if (entry instanceof WrappedKeyEntry) {
WrappedKeyEntry wke = (WrappedKeyEntry) entry;
setWrappedKeyEntry(alias, wke, param);
} else {
throw new KeyStoreException(
"Entry must be a PrivateKeyEntry, SecretKeyEntry or TrustedCertificateEntry"
+ "; was " + entry);
}
}
/**
* {@link X509Certificate} which returns {@link AndroidKeyStorePublicKey} from
* {@link #getPublicKey()}. This is so that crypto operations on these public keys contain
* can find out which keystore private key entry to use. This is needed so that Android Keystore
* crypto operations using public keys can find out which key alias to use. These operations
* require an alias.
*/
static class KeyStoreX509Certificate extends DelegatingX509Certificate {
private final String mPrivateKeyAlias;
private final int mPrivateKeyUid;
KeyStoreX509Certificate(String privateKeyAlias, int privateKeyUid,
X509Certificate delegate) {
super(delegate);
mPrivateKeyAlias = privateKeyAlias;
mPrivateKeyUid = privateKeyUid;
}
@Override
public PublicKey getPublicKey() {
PublicKey original = super.getPublicKey();
return AndroidKeyStoreProvider.getAndroidKeyStorePublicKey(
mPrivateKeyAlias, mPrivateKeyUid,
original.getAlgorithm(), original.getEncoded());
}
}
}