AndroidKeyStore.java revision 802768dd86c4e8a933dbfbac2e9f1a1daa5f93fa
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.security;
18
19import org.apache.harmony.xnet.provider.jsse.OpenSSLDSAPrivateKey;
20import org.apache.harmony.xnet.provider.jsse.OpenSSLEngine;
21import org.apache.harmony.xnet.provider.jsse.OpenSSLRSAPrivateKey;
22
23import android.util.Log;
24
25import java.io.ByteArrayInputStream;
26import java.io.IOException;
27import java.io.InputStream;
28import java.io.OutputStream;
29import java.security.InvalidKeyException;
30import java.security.Key;
31import java.security.KeyStoreException;
32import java.security.KeyStoreSpi;
33import java.security.NoSuchAlgorithmException;
34import java.security.PrivateKey;
35import java.security.UnrecoverableKeyException;
36import java.security.cert.Certificate;
37import java.security.cert.CertificateEncodingException;
38import java.security.cert.CertificateException;
39import java.security.cert.CertificateFactory;
40import java.security.cert.X509Certificate;
41import java.util.ArrayList;
42import java.util.Collection;
43import java.util.Collections;
44import java.util.Date;
45import java.util.Enumeration;
46import java.util.HashSet;
47import java.util.Iterator;
48import java.util.Set;
49
50/**
51 * A java.security.KeyStore interface for the Android KeyStore. An instance of
52 * it can be created via the {@link java.security.KeyStore#getInstance(String)
53 * KeyStore.getInstance("AndroidKeyStore")} interface. This returns a
54 * java.security.KeyStore backed by this "AndroidKeyStore" implementation.
55 * <p>
56 * This is built on top of Android's keystore daemon. The convention of alias
57 * use is:
58 * <p>
59 * PrivateKeyEntry will have a Credentials.USER_PRIVATE_KEY as the private key,
60 * Credentials.USER_CERTIFICATE as the first certificate in the chain (the one
61 * that corresponds to the private key), and then a Credentials.CA_CERTIFICATE
62 * entry which will have the rest of the chain concatenated in BER format.
63 * <p>
64 * TrustedCertificateEntry will just have a Credentials.CA_CERTIFICATE entry
65 * with a single certificate.
66 *
67 * @hide
68 */
69public class AndroidKeyStore extends KeyStoreSpi {
70    public static final String NAME = "AndroidKeyStore";
71
72    private android.security.KeyStore mKeyStore;
73
74    @Override
75    public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException,
76            UnrecoverableKeyException {
77        if (!isKeyEntry(alias)) {
78            return null;
79        }
80
81        final OpenSSLEngine engine = OpenSSLEngine.getInstance("keystore");
82        try {
83            return engine.getPrivateKeyById(Credentials.USER_PRIVATE_KEY + alias);
84        } catch (InvalidKeyException e) {
85            UnrecoverableKeyException t = new UnrecoverableKeyException("Can't get key");
86            t.initCause(e);
87            throw t;
88        }
89    }
90
91    @Override
92    public Certificate[] engineGetCertificateChain(String alias) {
93        final X509Certificate leaf = (X509Certificate) engineGetCertificate(alias);
94        if (leaf == null) {
95            return null;
96        }
97
98        final Certificate[] caList;
99
100        final byte[] caBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias);
101        if (caBytes != null) {
102            final Collection<X509Certificate> caChain = toCertificates(caBytes);
103
104            caList = new Certificate[caChain.size() + 1];
105
106            final Iterator<X509Certificate> it = caChain.iterator();
107            int i = 1;
108            while (it.hasNext()) {
109                caList[i++] = it.next();
110            }
111        } else {
112            caList = new Certificate[1];
113        }
114
115        caList[0] = leaf;
116
117        return caList;
118    }
119
120    @Override
121    public Certificate engineGetCertificate(String alias) {
122        byte[] certificate = mKeyStore.get(Credentials.USER_CERTIFICATE + alias);
123        if (certificate != null) {
124            return toCertificate(certificate);
125        }
126
127        certificate = mKeyStore.get(Credentials.CA_CERTIFICATE + alias);
128        if (certificate != null) {
129            return toCertificate(certificate);
130        }
131
132        return null;
133    }
134
135    private static X509Certificate toCertificate(byte[] bytes) {
136        try {
137            final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
138            return (X509Certificate) certFactory
139                    .generateCertificate(new ByteArrayInputStream(bytes));
140        } catch (CertificateException e) {
141            Log.w(NAME, "Couldn't parse certificate in keystore", e);
142            return null;
143        }
144    }
145
146    @SuppressWarnings("unchecked")
147    private static Collection<X509Certificate> toCertificates(byte[] bytes) {
148        try {
149            final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
150            return (Collection<X509Certificate>) certFactory
151                    .generateCertificates(new ByteArrayInputStream(bytes));
152        } catch (CertificateException e) {
153            Log.w(NAME, "Couldn't parse certificates in keystore", e);
154            return new ArrayList<X509Certificate>();
155        }
156    }
157
158    private Date getModificationDate(String alias) {
159        final long epochMillis = mKeyStore.getmtime(alias);
160        if (epochMillis == -1L) {
161            return null;
162        }
163
164        return new Date(epochMillis);
165    }
166
167    @Override
168    public Date engineGetCreationDate(String alias) {
169        Date d = getModificationDate(Credentials.USER_PRIVATE_KEY + alias);
170        if (d != null) {
171            return d;
172        }
173
174        d = getModificationDate(Credentials.USER_CERTIFICATE + alias);
175        if (d != null) {
176            return d;
177        }
178
179        return getModificationDate(Credentials.CA_CERTIFICATE + alias);
180    }
181
182    @Override
183    public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
184            throws KeyStoreException {
185        if ((password != null) && (password.length > 0)) {
186            throw new KeyStoreException("entries cannot be protected with passwords");
187        }
188
189        if (key instanceof PrivateKey) {
190            setPrivateKeyEntry(alias, (PrivateKey) key, chain);
191        } else {
192            throw new KeyStoreException("Only PrivateKeys are supported");
193        }
194    }
195
196    private void setPrivateKeyEntry(String alias, PrivateKey key, Certificate[] chain)
197            throws KeyStoreException {
198        byte[] keyBytes = null;
199
200        final String pkeyAlias;
201        if (key instanceof OpenSSLRSAPrivateKey) {
202            pkeyAlias = ((OpenSSLRSAPrivateKey) key).getPkeyAlias();
203        } else if (key instanceof OpenSSLDSAPrivateKey) {
204            pkeyAlias = ((OpenSSLDSAPrivateKey) key).getPkeyAlias();
205        } else {
206            pkeyAlias = null;
207        }
208
209        final boolean shouldReplacePrivateKey;
210        if (pkeyAlias != null && pkeyAlias.startsWith(Credentials.USER_PRIVATE_KEY)) {
211            final String keySubalias = pkeyAlias.substring(Credentials.USER_PRIVATE_KEY.length());
212            if (!alias.equals(keySubalias)) {
213                throw new KeyStoreException("Can only replace keys with same alias: " + alias
214                        + " != " + keySubalias);
215            }
216
217            shouldReplacePrivateKey = false;
218        } else {
219            // Make sure the PrivateKey format is the one we support.
220            final String keyFormat = key.getFormat();
221            if ((keyFormat == null) || (!"PKCS#8".equals(keyFormat))) {
222                throw new KeyStoreException(
223                        "Only PrivateKeys that can be encoded into PKCS#8 are supported");
224            }
225
226            // Make sure we can actually encode the key.
227            keyBytes = key.getEncoded();
228            if (keyBytes == null) {
229                throw new KeyStoreException("PrivateKey has no encoding");
230            }
231
232            shouldReplacePrivateKey = true;
233        }
234
235        // Make sure the chain exists since this is a PrivateKey
236        if ((chain == null) || (chain.length == 0)) {
237            throw new KeyStoreException("Must supply at least one Certificate with PrivateKey");
238        }
239
240        // Do chain type checking.
241        X509Certificate[] x509chain = new X509Certificate[chain.length];
242        for (int i = 0; i < chain.length; i++) {
243            if (!"X.509".equals(chain[i].getType())) {
244                throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #"
245                        + i);
246            }
247
248            if (!(chain[i] instanceof X509Certificate)) {
249                throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #"
250                        + i);
251            }
252
253            x509chain[i] = (X509Certificate) chain[i];
254        }
255
256        final byte[] userCertBytes;
257        try {
258            userCertBytes = x509chain[0].getEncoded();
259        } catch (CertificateEncodingException e) {
260            throw new KeyStoreException("Couldn't encode certificate #1", e);
261        }
262
263        /*
264         * If we have a chain, store it in the CA certificate slot for this
265         * alias as concatenated DER-encoded certificates. These can be
266         * deserialized by {@link CertificateFactory#generateCertificates}.
267         */
268        final byte[] chainBytes;
269        if (chain.length > 1) {
270            /*
271             * The chain is passed in as {user_cert, ca_cert_1, ca_cert_2, ...}
272             * so we only need the certificates starting at index 1.
273             */
274            final byte[][] certsBytes = new byte[x509chain.length - 1][];
275            int totalCertLength = 0;
276            for (int i = 0; i < certsBytes.length; i++) {
277                try {
278                    certsBytes[i] = x509chain[i + 1].getEncoded();
279                    totalCertLength += certsBytes[i].length;
280                } catch (CertificateEncodingException e) {
281                    throw new KeyStoreException("Can't encode Certificate #" + i, e);
282                }
283            }
284
285            /*
286             * Serialize this into one byte array so we can later call
287             * CertificateFactory#generateCertificates to recover them.
288             */
289            chainBytes = new byte[totalCertLength];
290            int outputOffset = 0;
291            for (int i = 0; i < certsBytes.length; i++) {
292                final int certLength = certsBytes[i].length;
293                System.arraycopy(certsBytes[i], 0, chainBytes, outputOffset, certLength);
294                outputOffset += certLength;
295                certsBytes[i] = null;
296            }
297        } else {
298            chainBytes = null;
299        }
300
301        /*
302         * Make sure we clear out all the appropriate types before trying to
303         * write.
304         */
305        if (shouldReplacePrivateKey) {
306            Credentials.deleteAllTypesForAlias(mKeyStore, alias);
307        } else {
308            Credentials.deleteCertificateTypesForAlias(mKeyStore, alias);
309        }
310
311        if (shouldReplacePrivateKey
312                && !mKeyStore.importKey(Credentials.USER_PRIVATE_KEY + alias, keyBytes)) {
313            Credentials.deleteAllTypesForAlias(mKeyStore, alias);
314            throw new KeyStoreException("Couldn't put private key in keystore");
315        } else if (!mKeyStore.put(Credentials.USER_CERTIFICATE + alias, userCertBytes)) {
316            Credentials.deleteAllTypesForAlias(mKeyStore, alias);
317            throw new KeyStoreException("Couldn't put certificate #1 in keystore");
318        } else if (chainBytes != null
319                && !mKeyStore.put(Credentials.CA_CERTIFICATE + alias, chainBytes)) {
320            Credentials.deleteAllTypesForAlias(mKeyStore, alias);
321            throw new KeyStoreException("Couldn't put certificate chain in keystore");
322        }
323    }
324
325    @Override
326    public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain)
327            throws KeyStoreException {
328        throw new RuntimeException("Operation not supported because key encoding is unknown");
329    }
330
331    @Override
332    public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
333        if (isKeyEntry(alias)) {
334            throw new KeyStoreException("Entry exists and is not a trusted certificate");
335        }
336
337        final byte[] encoded;
338        try {
339            encoded = cert.getEncoded();
340        } catch (CertificateEncodingException e) {
341            throw new KeyStoreException(e);
342        }
343
344        if (!mKeyStore.put(Credentials.CA_CERTIFICATE + alias, encoded)) {
345            throw new KeyStoreException("Couldn't insert certificate; is KeyStore initialized?");
346        }
347    }
348
349    @Override
350    public void engineDeleteEntry(String alias) throws KeyStoreException {
351        if (!Credentials.deleteAllTypesForAlias(mKeyStore, alias)) {
352            throw new KeyStoreException("No such entry " + alias);
353        }
354    }
355
356    private Set<String> getUniqueAliases() {
357        final String[] rawAliases = mKeyStore.saw("");
358        if (rawAliases == null) {
359            return new HashSet<String>();
360        }
361
362        final Set<String> aliases = new HashSet<String>(rawAliases.length);
363        for (String alias : rawAliases) {
364            final int idx = alias.indexOf('_');
365            if ((idx == -1) || (alias.length() <= idx)) {
366                Log.e(NAME, "invalid alias: " + alias);
367                continue;
368            }
369
370            aliases.add(new String(alias.substring(idx + 1)));
371        }
372
373        return aliases;
374    }
375
376    @Override
377    public Enumeration<String> engineAliases() {
378        return Collections.enumeration(getUniqueAliases());
379    }
380
381    @Override
382    public boolean engineContainsAlias(String alias) {
383        return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias)
384                || mKeyStore.contains(Credentials.USER_CERTIFICATE + alias)
385                || mKeyStore.contains(Credentials.CA_CERTIFICATE + alias);
386    }
387
388    @Override
389    public int engineSize() {
390        return getUniqueAliases().size();
391    }
392
393    @Override
394    public boolean engineIsKeyEntry(String alias) {
395        return isKeyEntry(alias);
396    }
397
398    private boolean isKeyEntry(String alias) {
399        return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias);
400    }
401
402    @Override
403    public boolean engineIsCertificateEntry(String alias) {
404        return !isKeyEntry(alias) && mKeyStore.contains(Credentials.CA_CERTIFICATE + alias);
405    }
406
407    @Override
408    public String engineGetCertificateAlias(Certificate cert) {
409        if (cert == null) {
410            return null;
411        }
412
413        final Set<String> nonCaEntries = new HashSet<String>();
414
415        /*
416         * First scan the PrivateKeyEntry types. The KeyStoreSpi documentation
417         * says to only compare the first certificate in the chain which is
418         * equivalent to the USER_CERTIFICATE prefix for the Android keystore
419         * convention.
420         */
421        final String[] certAliases = mKeyStore.saw(Credentials.USER_CERTIFICATE);
422        for (String alias : certAliases) {
423            final byte[] certBytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias);
424            if (certBytes == null) {
425                continue;
426            }
427
428            final Certificate c = toCertificate(certBytes);
429            nonCaEntries.add(alias);
430
431            if (cert.equals(c)) {
432                return alias;
433            }
434        }
435
436        /*
437         * Look at all the TrustedCertificateEntry types. Skip all the
438         * PrivateKeyEntry we looked at above.
439         */
440        final String[] caAliases = mKeyStore.saw(Credentials.CA_CERTIFICATE);
441        for (String alias : caAliases) {
442            if (nonCaEntries.contains(alias)) {
443                continue;
444            }
445
446            final byte[] certBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias);
447            if (certBytes == null) {
448                continue;
449            }
450
451            final Certificate c = toCertificate(mKeyStore.get(Credentials.CA_CERTIFICATE + alias));
452            if (cert.equals(c)) {
453                return alias;
454            }
455        }
456
457        return null;
458    }
459
460    @Override
461    public void engineStore(OutputStream stream, char[] password) throws IOException,
462            NoSuchAlgorithmException, CertificateException {
463        throw new UnsupportedOperationException("Can not serialize AndroidKeyStore to OutputStream");
464    }
465
466    @Override
467    public void engineLoad(InputStream stream, char[] password) throws IOException,
468            NoSuchAlgorithmException, CertificateException {
469        if (stream != null) {
470            throw new IllegalArgumentException("InputStream not supported");
471        }
472
473        if (password != null) {
474            throw new IllegalArgumentException("password not supported");
475        }
476
477        // Unfortunate name collision.
478        mKeyStore = android.security.KeyStore.getInstance();
479    }
480
481}
482