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