summaryrefslogtreecommitdiff
path: root/keystore/java/android/security/AndroidKeyStore.java
diff options
context:
space:
mode:
authorKenny Root <kroot@google.com>2012-08-10 08:28:37 -0700
committerKenny Root <kroot@google.com>2012-08-20 12:44:40 -0700
commite29df16cb57b69995df597e8a6d95d986c1c43fc (patch)
tree7cb7fb03ffff118dab968b483bb6d52270cf4ac9 /keystore/java/android/security/AndroidKeyStore.java
parent473c712b19bad992ab4eafcd43175fdce77b913d (diff)
Add AndroidKeyStore provider for KeyStore API
This introduces a public API for the Android keystore that is accessible via java.security.KeyStore API. This allows programs to store PrivateKeyEntry and TrustedCertificateEntry items visible only to themselves. Future work should include: * Implement KeyStore.CallbackHandlerProtection parameter to allow the caller to request that the keystore daemon unlock itself via the system password input dialog. * Implement SecretKeyEntry once that support is in keystore daemon Change-Id: I382ffdf742d3f9f7647c5f5a429244a340b6bb0a
Diffstat (limited to 'keystore/java/android/security/AndroidKeyStore.java')
-rw-r--r--keystore/java/android/security/AndroidKeyStore.java463
1 files changed, 463 insertions, 0 deletions
diff --git a/keystore/java/android/security/AndroidKeyStore.java b/keystore/java/android/security/AndroidKeyStore.java
new file mode 100644
index 000000000000..a629f8dfa676
--- /dev/null
+++ b/keystore/java/android/security/AndroidKeyStore.java
@@ -0,0 +1,463 @@
+/*
+ * 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;
+
+import org.apache.harmony.xnet.provider.jsse.OpenSSLEngine;
+
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyStoreException;
+import java.security.KeyStoreSpi;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+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.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;
+
+/**
+ * A java.security.KeyStore interface for the Android KeyStore. This class is
+ * hidden from the Android API, but 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 AndroidKeyStore extends KeyStoreSpi {
+ public static final String NAME = "AndroidKeyStore";
+
+ private android.security.KeyStore mKeyStore;
+
+ @Override
+ public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException,
+ UnrecoverableKeyException {
+ if (!isKeyEntry(alias)) {
+ return null;
+ }
+
+ final OpenSSLEngine engine = OpenSSLEngine.getInstance("keystore");
+ try {
+ return engine.getPrivateKeyById(Credentials.USER_PRIVATE_KEY + alias);
+ } catch (InvalidKeyException e) {
+ UnrecoverableKeyException t = new UnrecoverableKeyException("Can't get key");
+ t.initCause(e);
+ throw t;
+ }
+ }
+
+ @Override
+ public Certificate[] engineGetCertificateChain(String alias) {
+ final X509Certificate leaf = (X509Certificate) engineGetCertificate(alias);
+ if (leaf == null) {
+ return null;
+ }
+
+ final Certificate[] caList;
+
+ final byte[] caBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias);
+ 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) {
+ byte[] certificate = mKeyStore.get(Credentials.USER_CERTIFICATE + alias);
+ if (certificate != null) {
+ return toCertificate(certificate);
+ }
+
+ certificate = mKeyStore.get(Credentials.CA_CERTIFICATE + alias);
+ if (certificate != null) {
+ return toCertificate(certificate);
+ }
+
+ return 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);
+ if (epochMillis == -1L) {
+ return null;
+ }
+
+ return new Date(epochMillis);
+ }
+
+ @Override
+ public Date engineGetCreationDate(String alias) {
+ Date d = getModificationDate(Credentials.USER_PRIVATE_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);
+ } else {
+ throw new KeyStoreException("Only PrivateKeys are supported");
+ }
+ }
+
+ private void setPrivateKeyEntry(String alias, PrivateKey key, Certificate[] chain)
+ throws KeyStoreException {
+ // 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(
+ "Only PrivateKeys that can be encoded into PKCS#8 are supported");
+ }
+
+ // Make sure we can actually encode the key.
+ final byte[] keyBytes = key.getEncoded();
+ if (keyBytes == null) {
+ throw new KeyStoreException("PrivateKey has no encoding");
+ }
+
+ // 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("Couldn't encode certificate #1", 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("Can't 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;
+ }
+
+ /*
+ * Make sure we clear out all the types we know about before trying to
+ * write.
+ */
+ deleteAllTypesForAlias(alias);
+
+ if (!mKeyStore.importKey(Credentials.USER_PRIVATE_KEY + alias, keyBytes)) {
+ throw new KeyStoreException("Couldn't put private key in keystore");
+ } else if (!mKeyStore.put(Credentials.USER_CERTIFICATE + alias, userCertBytes)) {
+ throw new KeyStoreException("Couldn't put certificate #1 in keystore");
+ } else if (chainBytes != null
+ && !mKeyStore.put(Credentials.CA_CERTIFICATE + alias, chainBytes)) {
+ throw new KeyStoreException("Couldn't put certificate chain in keystore");
+ }
+ }
+
+ @Override
+ public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain)
+ throws KeyStoreException {
+ throw new RuntimeException("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");
+ }
+
+ final byte[] encoded;
+ try {
+ encoded = cert.getEncoded();
+ } catch (CertificateEncodingException e) {
+ throw new KeyStoreException(e);
+ }
+
+ if (!mKeyStore.put(Credentials.CA_CERTIFICATE + alias, encoded)) {
+ throw new KeyStoreException("Couldn't insert certificate; is KeyStore initialized?");
+ }
+ }
+
+ @Override
+ public void engineDeleteEntry(String alias) throws KeyStoreException {
+ if (!deleteAllTypesForAlias(alias)) {
+ throw new KeyStoreException("No such entry " + alias);
+ }
+ }
+
+ /**
+ * Delete all types (private key, certificate, CA certificate) for a
+ * particular {@code alias}. All three can exist for any given alias.
+ * Returns {@code true} if there was at least one of those types.
+ */
+ private boolean deleteAllTypesForAlias(String alias) {
+ /*
+ * Make sure every type is deleted. There can be all three types, so
+ * don't use a conditional here.
+ */
+ return mKeyStore.delKey(Credentials.USER_PRIVATE_KEY + alias)
+ | mKeyStore.delete(Credentials.USER_CERTIFICATE + alias)
+ | mKeyStore.delete(Credentials.CA_CERTIFICATE + alias);
+ }
+
+ private Set<String> getUniqueAliases() {
+ final String[] rawAliases = mKeyStore.saw("");
+ 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) {
+ return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias)
+ || mKeyStore.contains(Credentials.USER_CERTIFICATE + alias)
+ || mKeyStore.contains(Credentials.CA_CERTIFICATE + alias);
+ }
+
+ @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);
+ }
+
+ @Override
+ public boolean engineIsCertificateEntry(String alias) {
+ return !isKeyEntry(alias) && mKeyStore.contains(Credentials.CA_CERTIFICATE + alias);
+ }
+
+ @Override
+ public String engineGetCertificateAlias(Certificate cert) {
+ if (cert == 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.saw(Credentials.USER_CERTIFICATE);
+ for (String alias : certAliases) {
+ final byte[] certBytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias);
+ if (certBytes == null) {
+ continue;
+ }
+
+ final Certificate c = toCertificate(certBytes);
+ nonCaEntries.add(alias);
+
+ if (cert.equals(c)) {
+ return alias;
+ }
+ }
+
+ /*
+ * Look at all the TrustedCertificateEntry types. Skip all the
+ * PrivateKeyEntry we looked at above.
+ */
+ final String[] caAliases = mKeyStore.saw(Credentials.CA_CERTIFICATE);
+ for (String alias : caAliases) {
+ if (nonCaEntries.contains(alias)) {
+ continue;
+ }
+
+ final byte[] certBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias);
+ if (certBytes == null) {
+ continue;
+ }
+
+ final Certificate c = toCertificate(mKeyStore.get(Credentials.CA_CERTIFICATE + alias));
+ if (cert.equals(c)) {
+ 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 = android.security.KeyStore.getInstance();
+ }
+
+}