| /* |
| * 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 libcore.util.EmptyArray; |
| 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 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; |
| |
| final byte[] caBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias, mUid); |
| 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()); |
| } |
| } |
| } |