/* * 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.keystore2; import static android.security.keystore2.AndroidKeyStoreCipherSpiBase.DEFAULT_MGF1_DIGEST; import android.annotation.NonNull; import android.hardware.biometrics.BiometricManager; import android.hardware.security.keymint.EcCurve; import android.hardware.security.keymint.HardwareAuthenticatorType; import android.hardware.security.keymint.KeyParameter; import android.hardware.security.keymint.SecurityLevel; import android.security.GateKeeper; import android.security.KeyStore2; import android.security.KeyStoreParameter; import android.security.KeyStoreSecurityLevel; import android.security.keymaster.KeymasterDefs; import android.security.keystore.KeyGenParameterSpec; 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.system.keystore2.AuthenticatorSpec; import android.system.keystore2.Domain; import android.system.keystore2.IKeystoreSecurityLevel; import android.system.keystore2.KeyDescriptor; import android.system.keystore2.KeyEntryResponse; import android.system.keystore2.KeyMetadata; import android.system.keystore2.ResponseCode; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; 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.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.security.interfaces.ECKey; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.EdECKey; import java.security.interfaces.EdECPrivateKey; import java.security.interfaces.XECKey; import java.security.interfaces.XECPrivateKey; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.ECParameterSpec; import java.security.spec.NamedParameterSpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; 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. *

* This is built on top of Android's keystore daemon. The convention of alias * use is: *

* 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. *

* TrustedCertificateEntry will just have a Credentials.CA_CERTIFICATE entry * with a single certificate. * * @hide */ public class AndroidKeyStoreSpi extends KeyStoreSpi { public static final String TAG = "AndroidKeyStoreSpi"; public static final String NAME = "AndroidKeyStore"; private KeyStore2 mKeyStore; private @KeyProperties.Namespace int mNamespace = KeyProperties.NAMESPACE_APPLICATION; @Override public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException { try { return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore, alias, mNamespace); } catch (KeyPermanentlyInvalidatedException e) { throw new UnrecoverableKeyException(e.getMessage()); } catch (UnrecoverableKeyException e) { Throwable cause = e.getCause(); if (cause instanceof android.security.KeyStoreException) { if (((android.security.KeyStoreException) cause).getErrorCode() == ResponseCode.KEY_NOT_FOUND) { return null; } } throw e; } } /** * Make a key descriptor from the given alias and the mNamespace member. * If mNamespace is -1 it sets the domain field to {@link Domain#APP} and {@link Domain#SELINUX} * otherwise. The blob field is always set to null and the alias field to {@code alias} * @param alias The alias of the new key descriptor. * @return A new key descriptor. */ private KeyDescriptor makeKeyDescriptor(@NonNull String alias) { KeyDescriptor descriptor = new KeyDescriptor(); descriptor.domain = getTargetDomain(); descriptor.nspace = mNamespace; // ignored if Domain.App; descriptor.alias = alias; descriptor.blob = null; return descriptor; } private @Domain int getTargetDomain() { return mNamespace == KeyProperties.NAMESPACE_APPLICATION ? Domain.APP : Domain.SELINUX; } private KeyEntryResponse getKeyMetadata(String alias) { if (alias == null) { throw new NullPointerException("alias == null"); } KeyDescriptor descriptor = makeKeyDescriptor(alias); try { return mKeyStore.getKeyEntry(descriptor); } catch (android.security.KeyStoreException e) { if (e.getErrorCode() != ResponseCode.KEY_NOT_FOUND) { Log.w(TAG, "Could not get key metadata from Keystore.", e); } return null; } } @Override public Certificate[] engineGetCertificateChain(String alias) { KeyEntryResponse response = getKeyMetadata(alias); if (response == null || response.metadata.certificate == null) { return null; } final X509Certificate leaf = (X509Certificate) toCertificate(response.metadata.certificate); if (leaf == null) { return null; } final Certificate[] caList; final byte[] caBytes = response.metadata.certificateChain; if (caBytes != null) { final Collection caChain = toCertificates(caBytes); caList = new Certificate[caChain.size() + 1]; final Iterator 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) { KeyEntryResponse response = getKeyMetadata(alias); if (response == null) { return null; } byte[] encodedCert = response.metadata.certificate; if (encodedCert != null) { return toCertificate(encodedCert); } encodedCert = response.metadata.certificateChain; if (encodedCert != null) { return toCertificate(encodedCert); } // This entry/alias does not contain a certificate. return null; } 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 toCertificates(byte[] bytes) { try { final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); return (Collection) certFactory.generateCertificates( new ByteArrayInputStream(bytes)); } catch (CertificateException e) { Log.w(NAME, "Couldn't parse certificates in keystore", e); return new ArrayList(); } } @Override public Date engineGetCreationDate(String alias) { KeyEntryResponse response = getKeyMetadata(alias); if (response == null) { return null; } if (response.metadata.modificationTimeMs == -1) { return null; } return new Date(response.metadata.modificationTimeMs); } @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 { @SecurityLevel int securitylevel = SecurityLevel.TRUSTED_ENVIRONMENT; int flags = 0; KeyProtection spec; if (param == null) { spec = getLegacyKeyProtectionParameter(key); } else if (param instanceof KeyStoreParameter) { spec = getLegacyKeyProtectionParameter(key); KeyStoreParameter legacySpec = (KeyStoreParameter) param; } else if (param instanceof KeyProtection) { spec = (KeyProtection) param; if (spec.isCriticalToDeviceEncryption()) { // This key is should not be bound to the LSKF even if it is auth bound. // This indicates that this key is used in the derivation for of the // master key, that is used for the LSKF binding of other auth bound // keys. This breaks up a circular dependency while retaining logical // authentication binding of the key. flags |= IKeystoreSecurityLevel .KEY_FLAG_AUTH_BOUND_WITHOUT_CRYPTOGRAPHIC_LSKF_BINDING; } if (spec.isStrongBoxBacked()) { securitylevel = SecurityLevel.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; } @Domain int targetDomain = getTargetDomain(); // If the given key is an AndroidKeyStorePrivateKey, we attempt to update // its subcomponents with the given certificate and certificate chain. if (key instanceof AndroidKeyStorePrivateKey) { AndroidKeyStoreKey ksKey = (AndroidKeyStoreKey) key; KeyDescriptor descriptor = ksKey.getUserKeyDescriptor(); // This throws if the request cannot replace the entry. assertCanReplace(alias, targetDomain, mNamespace, descriptor); try { mKeyStore.updateSubcomponents( ((AndroidKeyStorePrivateKey) key).getKeyIdDescriptor(), userCertBytes, chainBytes); } catch (android.security.KeyStoreException e) { throw new KeyStoreException("Failed to store certificate and certificate chain", e); } return; } // 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. byte[] pkcs8EncodedPrivateKeyBytes = key.getEncoded(); if (pkcs8EncodedPrivateKeyBytes == null) { throw new KeyStoreException("Private key did not export any key material"); } final List importArgs = new ArrayList<>(); try { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_ALGORITHM, KeyProperties.KeyAlgorithm.toKeymasterAsymmetricKeyAlgorithm( key.getAlgorithm())) ); KeyStore2ParameterUtils.forEachSetFlag(spec.getPurposes(), (purpose) -> { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_PURPOSE, KeyProperties.Purpose.toKeymaster(purpose) )); }); if (spec.isDigestsSpecified()) { for (String digest : spec.getDigests()) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_DIGEST, KeyProperties.Digest.toKeymaster(digest) )); } } for (String blockMode : spec.getBlockModes()) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_BLOCK_MODE, KeyProperties.BlockMode.toKeymaster(blockMode) )); } int[] keymasterEncryptionPaddings = KeyProperties.EncryptionPadding.allToKeymaster( spec.getEncryptionPaddings()); if (((spec.getPurposes() & 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."); } } } for (int padding : keymasterEncryptionPaddings) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_PADDING, padding )); if (padding == KeymasterDefs.KM_PAD_RSA_OAEP) { if (spec.isDigestsSpecified()) { boolean hasDefaultMgf1DigestBeenAdded = false; for (String digest : spec.getDigests()) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST, KeyProperties.Digest.toKeymaster(digest) )); hasDefaultMgf1DigestBeenAdded |= digest.equals(DEFAULT_MGF1_DIGEST); } /* Because of default MGF1 digest is SHA-1. It has to be added in Key * characteristics. Otherwise, crypto operations will fail with Incompatible * MGF1 digest. */ if (!hasDefaultMgf1DigestBeenAdded) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST, KeyProperties.Digest.toKeymaster(DEFAULT_MGF1_DIGEST) )); } } } } for (String padding : spec.getSignaturePaddings()) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_PADDING, KeyProperties.SignaturePadding.toKeymaster(padding) )); } KeyStore2ParameterUtils.addUserAuthArgs(importArgs, spec); if (spec.getKeyValidityStart() != null) { importArgs.add(KeyStore2ParameterUtils.makeDate( KeymasterDefs.KM_TAG_ACTIVE_DATETIME, spec.getKeyValidityStart() )); } if (spec.getKeyValidityForOriginationEnd() != null) { importArgs.add(KeyStore2ParameterUtils.makeDate( KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, spec.getKeyValidityForOriginationEnd() )); } if (spec.getKeyValidityForConsumptionEnd() != null) { importArgs.add(KeyStore2ParameterUtils.makeDate( KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, spec.getKeyValidityForConsumptionEnd() )); } if (spec.getMaxUsageCount() != KeyProperties.UNRESTRICTED_USAGE_COUNT) { importArgs.add(KeyStore2ParameterUtils.makeInt( KeymasterDefs.KM_TAG_USAGE_COUNT_LIMIT, spec.getMaxUsageCount() )); } if (KeymasterDefs.KM_ALGORITHM_EC == KeyProperties.KeyAlgorithm.toKeymasterAsymmetricKeyAlgorithm( key.getAlgorithm())) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_EC_CURVE, getKeymasterEcCurve(key) )); } } catch (IllegalArgumentException | IllegalStateException e) { throw new KeyStoreException(e); } try { KeyStoreSecurityLevel securityLevelInterface = mKeyStore.getSecurityLevel( securitylevel); KeyDescriptor descriptor = makeKeyDescriptor(alias); KeyMetadata metadata = securityLevelInterface.importKey(descriptor, null, importArgs, flags, pkcs8EncodedPrivateKeyBytes); try { mKeyStore.updateSubcomponents(metadata.key, userCertBytes, chainBytes); } catch (android.security.KeyStoreException e) { mKeyStore.deleteKey(metadata.key); throw new KeyStoreException("Failed to store certificate and certificate chain", e); } } catch (android.security.KeyStoreException e) { throw new KeyStoreException("Failed to store private key", e); } } private int getKeymasterEcCurve(PrivateKey key) { if (key instanceof ECKey) { ECParameterSpec param = ((ECPrivateKey) key).getParams(); int kmECCurve = KeymasterUtils.getKeymasterEcCurve(KeymasterUtils.getCurveName(param)); if (kmECCurve >= 0) { return kmECCurve; } } else if (key instanceof XECKey) { AlgorithmParameterSpec param = ((XECPrivateKey) key).getParams(); if (param.equals(NamedParameterSpec.X25519)) { return EcCurve.CURVE_25519; } } else if (key.getAlgorithm().equals("XDH")) { // TODO com.android.org.conscrypt.OpenSSLX25519PrivateKey does not implement XECKey, // this case is not required once it implements XECKey interface(b/214203951). return EcCurve.CURVE_25519; } else if (key instanceof EdECKey) { AlgorithmParameterSpec param = ((EdECPrivateKey) key).getParams(); if (param.equals(NamedParameterSpec.ED25519)) { return EcCurve.CURVE_25519; } } throw new IllegalArgumentException("Unexpected Key " + key.getClass().getName()); } private static void assertCanReplace(String alias, @Domain int targetDomain, int targetNamespace, KeyDescriptor descriptor) throws KeyStoreException { // If // * the alias does not match, or // * the domain does not match, or // * the domain is Domain.SELINUX and the namespaces don not match, // then the designated key location is not equivalent to the location of the // given key parameter and cannot be updated. // // Note: mNamespace == KeyProperties.NAMESPACE_APPLICATION implies that the target domain // is Domain.APP and Domain.SELINUX is the target domain otherwise. if (!alias.equals(descriptor.alias) || descriptor.domain != targetDomain || (descriptor.domain == Domain.SELINUX && descriptor.nspace != targetNamespace)) { throw new KeyStoreException("Can only replace keys with same alias: " + alias + " != " + descriptor.alias + " in the same target domain: " + targetDomain + " != " + descriptor.domain + (targetDomain == Domain.SELINUX ? " in the same target namespace: " + targetNamespace + " != " + descriptor.nspace : "") ); } } private void setSecretKeyEntry(String alias, 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; @Domain int targetDomain = (getTargetDomain()); if (key instanceof AndroidKeyStoreSecretKey) { String keyAliasInKeystore = ((AndroidKeyStoreSecretKey) key).getUserKeyDescriptor().alias; KeyDescriptor descriptor = ((AndroidKeyStoreSecretKey) key).getUserKeyDescriptor(); // This throws if the request cannot replace the existing key. assertCanReplace(alias, targetDomain, mNamespace, descriptor); // 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"); } final List importArgs = new ArrayList<>(); try { int keymasterAlgorithm = KeyProperties.KeyAlgorithm.toKeymasterSecretKeyAlgorithm( key.getAlgorithm()); importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_ALGORITHM, keymasterAlgorithm )); 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()); } 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()); } } int outputBits = KeymasterUtils.getDigestOutputSizeBits(keymasterImpliedDigest); if (outputBits == -1) { throw new ProviderException( "HMAC key authorized for unsupported digest: " + KeyProperties.Digest.fromKeymaster(keymasterImpliedDigest)); } importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_DIGEST, keymasterImpliedDigest )); importArgs.add(KeyStore2ParameterUtils.makeInt( KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, outputBits )); } else { if (params.isDigestsSpecified()) { for (String digest : params.getDigests()) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_DIGEST, KeyProperties.Digest.toKeymaster(digest) )); } } } KeyStore2ParameterUtils.forEachSetFlag(params.getPurposes(), (purpose) -> { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_PURPOSE, KeyProperties.Purpose.toKeymaster(purpose) )); }); boolean indCpa = false; if ((params.getPurposes() & KeyProperties.PURPOSE_ENCRYPT) != 0) { if (((KeyProtection) param).isRandomizedEncryptionRequired()) { indCpa = true; } else { importArgs.add(KeyStore2ParameterUtils.makeBool( KeymasterDefs.KM_TAG_CALLER_NONCE )); } } for (String blockMode : params.getBlockModes()) { int keymasterBlockMode = KeyProperties.BlockMode.toKeymaster(blockMode); if (indCpa && !KeymasterUtils.isKeymasterBlockModeIndCpaCompatibleWithSymmetricCrypto( keymasterBlockMode)) { throw new KeyStoreException( "Randomized encryption (IND-CPA) required but may be violated by" + " block mode: " + blockMode + ". See KeyProtection documentation."); } if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES && keymasterBlockMode == KeymasterDefs.KM_MODE_GCM) { importArgs.add(KeyStore2ParameterUtils.makeInt( KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, AndroidKeyStoreAuthenticatedAESCipherSpi.GCM .MIN_SUPPORTED_TAG_LENGTH_BITS )); } importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_BLOCK_MODE, keymasterBlockMode )); } if (params.getSignaturePaddings().length > 0) { throw new KeyStoreException("Signature paddings not supported for symmetric keys"); } for (String padding : params.getEncryptionPaddings()) { importArgs.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_PADDING, KeyProperties.EncryptionPadding.toKeymaster(padding) )); } KeyStore2ParameterUtils.addUserAuthArgs(importArgs, params); if (params.getKeyValidityStart() != null) { importArgs.add(KeyStore2ParameterUtils.makeDate( KeymasterDefs.KM_TAG_ACTIVE_DATETIME, params.getKeyValidityStart() )); } if (params.getKeyValidityForOriginationEnd() != null) { importArgs.add(KeyStore2ParameterUtils.makeDate( KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, params.getKeyValidityForOriginationEnd() )); } if (params.getKeyValidityForConsumptionEnd() != null) { importArgs.add(KeyStore2ParameterUtils.makeDate( KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, params.getKeyValidityForConsumptionEnd() )); } if (params.getMaxUsageCount() != KeyProperties.UNRESTRICTED_USAGE_COUNT) { importArgs.add(KeyStore2ParameterUtils.makeInt( KeymasterDefs.KM_TAG_USAGE_COUNT_LIMIT, params.getMaxUsageCount() )); } if (params.isRollbackResistant()) { importArgs.add(KeyStore2ParameterUtils.makeBool( KeymasterDefs.KM_TAG_ROLLBACK_RESISTANT )); } } catch (IllegalArgumentException | IllegalStateException e) { throw new KeyStoreException(e); } int flags = 0; if (params.isCriticalToDeviceEncryption()) { flags |= IKeystoreSecurityLevel.KEY_FLAG_AUTH_BOUND_WITHOUT_CRYPTOGRAPHIC_LSKF_BINDING; } @SecurityLevel int securityLevel = params.isStrongBoxBacked() ? SecurityLevel.STRONGBOX : SecurityLevel.TRUSTED_ENVIRONMENT; try { KeyStoreSecurityLevel securityLevelInterface = mKeyStore.getSecurityLevel( securityLevel); KeyDescriptor descriptor = makeKeyDescriptor(alias); securityLevelInterface.importKey(descriptor, null /* TODO attestationKey */, importArgs, flags, keyMaterial); } catch (android.security.KeyStoreException e) { throw new KeyStoreException("Failed to import secret key.", e); } } 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]; String[] parts = entry.getTransformation().split("/"); List args = new ArrayList<>(); String algorithm = parts[0]; if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(algorithm)) { args.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA )); } else { throw new KeyStoreException("Algorithm \"" + algorithm + "\" not supported for " + "wrapping. Only RSA wrapping keys are supported."); } if (parts.length > 1) { String mode = parts[1]; args.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_BLOCK_MODE, KeyProperties.BlockMode.toKeymaster(mode) )); } if (parts.length > 2) { @KeyProperties.EncryptionPaddingEnum int padding = KeyProperties.EncryptionPadding.toKeymaster(parts[2]); if (padding != KeymasterDefs.KM_PAD_NONE) { args.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_PADDING, padding )); } } KeyGenParameterSpec spec = (KeyGenParameterSpec) entry.getAlgorithmParameterSpec(); if (spec.isDigestsSpecified()) { @KeyProperties.DigestEnum int digest = KeyProperties.Digest.toKeymaster(spec.getDigests()[0]); if (digest != KeymasterDefs.KM_DIGEST_NONE) { args.add(KeyStore2ParameterUtils.makeEnum( KeymasterDefs.KM_TAG_DIGEST, digest )); } } KeyDescriptor wrappingkey = makeKeyDescriptor(entry.getWrappingKeyAlias()); KeyEntryResponse response = null; try { response = mKeyStore.getKeyEntry(wrappingkey); } catch (android.security.KeyStoreException e) { throw new KeyStoreException("Failed to import wrapped key. Keystore error code: " + e.getErrorCode(), e); } KeyDescriptor wrappedKey = makeKeyDescriptor(alias); KeyStoreSecurityLevel securityLevel = new KeyStoreSecurityLevel(response.iSecurityLevel); final BiometricManager bm = android.app.AppGlobals.getInitialApplication() .getSystemService(BiometricManager.class); long[] biometricSids = bm.getAuthenticatorIds(); List authenticatorSpecs = new ArrayList<>(); AuthenticatorSpec authenticatorSpec = new AuthenticatorSpec(); authenticatorSpec.authenticatorType = HardwareAuthenticatorType.PASSWORD; authenticatorSpec.authenticatorId = GateKeeper.getSecureUserId(); authenticatorSpecs.add(authenticatorSpec); for (long sid : biometricSids) { AuthenticatorSpec authSpec = new AuthenticatorSpec(); authSpec.authenticatorType = HardwareAuthenticatorType.FINGERPRINT; authSpec.authenticatorId = sid; authenticatorSpecs.add(authSpec); } try { securityLevel.importWrappedKey( wrappedKey, wrappingkey, entry.getWrappedKeyBytes(), null /* masking key is set to 32 bytes if null is given here */, args, authenticatorSpecs.toArray(new AuthenticatorSpec[0])); } catch (android.security.KeyStoreException e) { switch (e.getErrorCode()) { case KeymasterDefs.KM_ERROR_UNIMPLEMENTED: { throw new SecureKeyImportUnavailableException("Could not import wrapped key"); } default: throw new KeyStoreException("Failed to import wrapped key. Keystore error " + "code: " + e.getErrorCode(), e); } } } @Override public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain) throws KeyStoreException { throw new KeyStoreException("Operation not supported because key encoding is unknown"); } /** * This function sets a trusted certificate entry. It fails if the given * alias is already taken by an actual key entry. However, if the entry is a * trusted certificate it will get silently replaced. * @param alias the alias name * @param cert the certificate * * @throws KeyStoreException if the alias is already taken by a secret or private * key entry. * @throws KeyStoreException with a nested {@link CertificateEncodingException} * if the {@code cert.getEncoded()} throws. * @throws KeyStoreException with a nested {@link android.security.KeyStoreException} if * something went wrong while inserting the certificate into keystore. * @throws NullPointerException if cert or alias is null. * * @hide */ @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); } try { mKeyStore.updateSubcomponents(makeKeyDescriptor(alias), null /* publicCert - unused when used as pure certificate store. */, encoded); } catch (android.security.KeyStoreException e) { throw new KeyStoreException("Couldn't insert certificate.", e); } } @Override public void engineDeleteEntry(String alias) throws KeyStoreException { KeyDescriptor descriptor = makeKeyDescriptor(alias); try { mKeyStore.deleteKey(descriptor); } catch (android.security.KeyStoreException e) { if (e.getErrorCode() != ResponseCode.KEY_NOT_FOUND) { throw new KeyStoreException("Failed to delete entry: " + alias, e); } } } private KeyDescriptor[] getAliasesBatch(String startPastAlias) { try { return mKeyStore.listBatch( getTargetDomain(), mNamespace, startPastAlias ); } catch (android.security.KeyStoreException e) { Log.e(TAG, "Failed to list keystore entries.", e); return new KeyDescriptor[0]; } } @Override public Enumeration engineAliases() { return new KeyEntriesEnumerator(); } @Override public boolean engineContainsAlias(String alias) { if (alias == null) { throw new NullPointerException("alias == null"); } return getKeyMetadata(alias) != null; } @Override public int engineSize() { try { return mKeyStore.getNumberOfEntries( getTargetDomain(), mNamespace ); } catch (android.security.KeyStoreException e) { Log.e(TAG, "Failed to get the number of keystore entries.", e); return 0; } } @Override public boolean engineIsKeyEntry(String alias) { return isKeyEntry(alias); } private boolean isKeyEntry(String alias) { if (alias == null) { throw new NullPointerException("alias == null"); } KeyEntryResponse response = getKeyMetadata(alias); // If response is null, there is no such entry. // If response.iSecurityLevel is null, there is no private or secret key material stored. return response != null && response.iSecurityLevel != null; } @Override public boolean engineIsCertificateEntry(String alias) { if (alias == null) { throw new NullPointerException("alias == null"); } KeyEntryResponse response = getKeyMetadata(alias); // If response == null there is no such entry. // If there is no certificateChain, then this is not a certificate entry. // If there is a private key entry, this is the certificate chain for that // key entry and not a CA certificate entry. return response != null && response.metadata.certificateChain != null && response.iSecurityLevel == null; } @Override public String engineGetCertificateAlias(Certificate cert) { if (cert == null) { return null; } if (!"X.509".equalsIgnoreCase(cert.getType())) { Log.e(TAG, "In engineGetCertificateAlias: only X.509 certificates are supported."); return null; } byte[] targetCertBytes; try { targetCertBytes = cert.getEncoded(); } catch (CertificateEncodingException e) { Log.e(TAG, "While trying to get the alias for a certificate.", e); return null; } if (targetCertBytes == null) { return null; } KeyDescriptor[] keyDescriptors = null; try { keyDescriptors = mKeyStore.list( getTargetDomain(), mNamespace ); } catch (android.security.KeyStoreException e) { Log.w(TAG, "Failed to get list of keystore entries.", e); } String caAlias = null; for (KeyDescriptor d : keyDescriptors) { KeyEntryResponse response = getKeyMetadata(d.alias); if (response == null) { continue; } /* * The KeyStoreSpi documentation says to only compare the first certificate in the * chain which is equivalent to the {@code response.metadata.certificate} field. * So we look for a hit in this field first. For pure CA certificate entries, * we check the {@code response.metadata.certificateChain} field. But we only * return a CA alias if there was no hit in the certificate field of any other * entry. */ if (response.metadata.certificate != null) { if (Arrays.equals(response.metadata.certificate, targetCertBytes)) { return d.alias; } } else if (response.metadata.certificateChain != null && caAlias == null) { if (Arrays.equals(response.metadata.certificateChain, targetCertBytes)) { caAlias = d.alias; } } } return caAlias; } /** * Used by Tests to initialize with a fake KeyStore2. * @hide * @param keystore */ @VisibleForTesting public void initForTesting(KeyStore2 keystore) { mKeyStore = keystore; mNamespace = KeyProperties.NAMESPACE_APPLICATION; } @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 = KeyStore2.getInstance(); mNamespace = KeyProperties.NAMESPACE_APPLICATION; } @Override public void engineLoad(LoadStoreParameter param) throws IOException, NoSuchAlgorithmException, CertificateException { @KeyProperties.Namespace int namespace = KeyProperties.NAMESPACE_APPLICATION; if (param != null) { if (param instanceof AndroidKeyStoreLoadStoreParameter) { namespace = ((AndroidKeyStoreLoadStoreParameter) param).getNamespace(); } else { throw new IllegalArgumentException( "Unsupported param type: " + param.getClass()); } } mKeyStore = KeyStore2.getInstance(); mNamespace = namespace; } @Override public void engineSetEntry(String alias, Entry entry, ProtectionParameter param) throws KeyStoreException { if (entry == null) { throw new KeyStoreException("entry == null"); } if (entry instanceof java.security.KeyStore.TrustedCertificateEntry) { java.security.KeyStore.TrustedCertificateEntry trE = (java.security.KeyStore.TrustedCertificateEntry) entry; // engineSetCertificateEntry does not overwrite if the existing entry // is a key entry, but the semantic of engineSetEntry is such that it // overwrites any existing entry. Thus we delete any possible existing // entry by this alias. engineDeleteEntry(alias); 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, WrappedKeyEntry " + "or TrustedCertificateEntry; was " + entry); } } private class KeyEntriesEnumerator implements Enumeration { private KeyDescriptor[] mCurrentBatch; private int mCurrentEntry = 0; private String mLastAlias = null; private KeyEntriesEnumerator() { getAndValidateNextBatch(); } private void getAndValidateNextBatch() { mCurrentBatch = getAliasesBatch(mLastAlias); mCurrentEntry = 0; } public boolean hasMoreElements() { return (mCurrentBatch != null) && (mCurrentBatch.length > 0); } public String nextElement() { if ((mCurrentBatch == null) || (mCurrentBatch.length == 0)) { throw new NoSuchElementException("Error while fetching entries."); } final KeyDescriptor currentEntry = mCurrentBatch[mCurrentEntry]; mLastAlias = currentEntry.alias; mCurrentEntry++; // This was the last entry in the batch. if (mCurrentEntry >= mCurrentBatch.length) { getAndValidateNextBatch(); } return mLastAlias; } } }