KeyChain.java revision 42f6528b988e3ae320cda63a2bd63d30d9e56183
1/*
2 * Copyright (C) 2011 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 */
16package android.security;
17
18import android.accounts.Account;
19import android.accounts.AccountManager;
20import android.accounts.AccountManagerCallback;
21import android.accounts.AccountManagerFuture;
22import android.accounts.AuthenticatorException;
23import android.accounts.OperationCanceledException;
24import android.app.Activity;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.ServiceConnection;
29import android.os.Bundle;
30import android.os.IBinder;
31import android.os.Looper;
32import android.os.RemoteException;
33import java.io.ByteArrayInputStream;
34import java.io.Closeable;
35import java.io.IOException;
36import java.security.KeyFactory;
37import java.security.KeyPair;
38import java.security.NoSuchAlgorithmException;
39import java.security.Principal;
40import java.security.PrivateKey;
41import java.security.cert.Certificate;
42import java.security.cert.CertificateException;
43import java.security.cert.CertificateFactory;
44import java.security.cert.X509Certificate;
45import java.security.spec.InvalidKeySpecException;
46import java.security.spec.PKCS8EncodedKeySpec;
47import java.util.concurrent.BlockingQueue;
48import java.util.concurrent.LinkedBlockingQueue;
49
50/**
51 * The {@code KeyChain} class provides access to private keys and
52 * their corresponding certificate chains in credential storage.
53 *
54 * <p>Applications accessing the {@code KeyChain} normally go through
55 * these steps:
56 *
57 * <ol>
58 *
59 * <li>Receive a callback from an {@link javax.net.ssl.X509KeyManager
60 * X509KeyManager} that a private key is requested.
61 *
62 * <li>Call {@link #choosePrivateKeyAlias
63 * choosePrivateKeyAlias} to allow the user to select from a
64 * list of currently available private keys and corresponding
65 * certificate chains. The chosen alias will be returned by the
66 * callback {@link KeyChainAliasCallback#alias}, or null if no private
67 * key is available or the user cancels the request.
68 *
69 * <li>Call {@link #getPrivateKey} and {@link #getCertificateChain} to
70 * retrieve the credentials to return to the corresponding {@link
71 * javax.net.ssl.X509KeyManager} callbacks.
72 *
73 * </ol>
74 *
75 * <p>An application may remember the value of a selected alias to
76 * avoid prompting the user with {@link #choosePrivateKeyAlias
77 * choosePrivateKeyAlias} on subsequent connections. If the alias is
78 * no longer valid, null will be returned on lookups using that value
79 */
80// TODO reference intent for credential installation when public
81public final class KeyChain {
82
83    private static final String TAG = "KeyChain";
84
85    /**
86     * @hide Also used by KeyChainService implementation
87     */
88    public static final String ACCOUNT_TYPE = "com.android.keychain";
89
90    /**
91     * @hide Also used by KeyChainActivity implementation
92     */
93    public static final String EXTRA_RESPONSE = "response";
94
95    /**
96     * Launches an {@code Activity} for the user to select the alias
97     * for a private key and certificate pair for authentication. The
98     * selected alias or null will be returned via the
99     * KeyChainAliasCallback callback.
100     *
101     * <p>{@code keyTypes} and {@code issuers} may be used to
102     * highlight suggested choices to the user, although to cope with
103     * sometimes erroneous values provided by servers, the user may be
104     * able to override these suggestions.
105     *
106     * <p>{@code host} and {@code port} may be used to give the user
107     * more context about the server requesting the credentials.
108     *
109     * <p>This method requires the caller to hold the permission
110     * {@link android.Manifest.permission#USE_CREDENTIALS}.
111     *
112     * @param activity The {@link Activity} context to use for
113     *     launching the new sub-Activity to prompt the user to select
114     *     a private key; used only to call startActivity(); must not
115     *     be null.
116     * @param response Callback to invoke when the request completes;
117     *     must not be null
118     * @param keyTypes The acceptable types of asymmetric keys such as
119     *     "RSA" or "DSA", or a null array.
120     * @param issuers The acceptable certificate issuers for the
121     *     certificate matching the private key, or null.
122     * @param host The host name of the server requesting the
123     *     certificate, or null if unavailable.
124     * @param port The port number of the server requesting the
125     *     certificate, or -1 if unavailable.
126     */
127    public static void choosePrivateKeyAlias(Activity activity, KeyChainAliasCallback response,
128                                             String[] keyTypes, Principal[] issuers,
129                                             String host, int port) {
130        /*
131         * TODO currently keyTypes, issuers, host, and port are
132         * unused. They are meant to follow the semantics and purpose
133         * of X509KeyManager method arguments.
134         *
135         * keyTypes would allow the list to be filtered and typically
136         * will be set correctly by the server. In practice today,
137         * most all users will want only RSA, rarely DSA, and usually
138         * only a small number of certs will be available.
139         *
140         * issuers is typically not useful. Some servers historically
141         * will send the entire list of public CAs known to the
142         * server. Others will send none. If this is used, if there
143         * are no matches after applying the constraint, it should be
144         * ignored.
145         *
146         * host and port may be shown to the user if available, but it
147         * should be clear that they are not validated values, perhaps
148         * shown along with requesting application identity to clarify
149         * the source of the request.
150         */
151        if (activity == null) {
152            throw new NullPointerException("activity == null");
153        }
154        if (response == null) {
155            throw new NullPointerException("response == null");
156        }
157        Intent intent = new Intent("com.android.keychain.CHOOSER");
158        intent.putExtra(EXTRA_RESPONSE, new AliasResponse(activity, response));
159        activity.startActivity(intent);
160    }
161
162    private static class AliasResponse extends IKeyChainAliasCallback.Stub {
163        private final Activity activity;
164        private final KeyChainAliasCallback keyChainAliasResponse;
165        private AliasResponse(Activity activity, KeyChainAliasCallback keyChainAliasResponse) {
166            this.activity = activity;
167            this.keyChainAliasResponse = keyChainAliasResponse;
168        }
169        @Override public void alias(String alias) {
170            if (alias == null) {
171                keyChainAliasResponse.alias(null);
172                return;
173            }
174            AccountManager accountManager = AccountManager.get(activity);
175            accountManager.getAuthToken(getAccount(activity),
176                                        alias,
177                                        null,
178                                        activity,
179                                        new AliasAccountManagerCallback(keyChainAliasResponse,
180                                                                        alias),
181                                        null);
182        }
183    }
184
185    private static class AliasAccountManagerCallback implements AccountManagerCallback<Bundle> {
186        private final KeyChainAliasCallback keyChainAliasResponse;
187        private final String alias;
188        private AliasAccountManagerCallback(KeyChainAliasCallback keyChainAliasResponse,
189                                            String alias) {
190            this.keyChainAliasResponse = keyChainAliasResponse;
191            this.alias = alias;
192        }
193        @Override public void run(AccountManagerFuture<Bundle> future) {
194            Bundle bundle;
195            try {
196                bundle = future.getResult();
197            } catch (OperationCanceledException e) {
198                keyChainAliasResponse.alias(null);
199                return;
200            } catch (IOException e) {
201                keyChainAliasResponse.alias(null);
202                return;
203            } catch (AuthenticatorException e) {
204                keyChainAliasResponse.alias(null);
205                return;
206            }
207            String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
208            if (authToken != null) {
209                keyChainAliasResponse.alias(alias);
210            } else {
211                keyChainAliasResponse.alias(null);
212            }
213        }
214    }
215
216    /**
217     * Returns the {@code PrivateKey} for the requested alias, or null
218     * if no there is no result.
219     *
220     * <p>This method requires the caller to hold the permission
221     * {@link android.Manifest.permission#USE_CREDENTIALS}.
222     *
223     * @param alias The alias of the desired private key, typically
224     * returned via {@link KeyChainAliasCallback#alias}.
225     * @throws KeyChainException if the alias was valid but there was some problem accessing it.
226     */
227    public static PrivateKey getPrivateKey(Context context, String alias)
228            throws KeyChainException, InterruptedException {
229        if (alias == null) {
230            throw new NullPointerException("alias == null");
231        }
232        KeyChainConnection keyChainConnection = bind(context);
233        try {
234            String authToken = authToken(context, alias);
235            if (authToken == null) {
236                return null;
237            }
238            IKeyChainService keyChainService = keyChainConnection.getService();
239            byte[] privateKeyBytes = keyChainService.getPrivateKey(alias, authToken);
240            return toPrivateKey(privateKeyBytes);
241        } catch (RemoteException e) {
242            throw new KeyChainException(e);
243        } catch (RuntimeException e) {
244            // only certain RuntimeExceptions can be propagated across the IKeyChainService call
245            throw new KeyChainException(e);
246        } finally {
247            keyChainConnection.close();
248        }
249    }
250
251    /**
252     * Returns the {@code X509Certificate} chain for the requested
253     * alias, or null if no there is no result.
254     *
255     * <p>This method requires the caller to hold the permission
256     * {@link android.Manifest.permission#USE_CREDENTIALS}.
257     *
258     * @param alias The alias of the desired certificate chain, typically
259     * returned via {@link KeyChainAliasCallback#alias}.
260     * @throws KeyChainException if the alias was valid but there was some problem accessing it.
261     */
262    public static X509Certificate[] getCertificateChain(Context context, String alias)
263            throws KeyChainException, InterruptedException {
264        if (alias == null) {
265            throw new NullPointerException("alias == null");
266        }
267        KeyChainConnection keyChainConnection = bind(context);
268        try {
269            String authToken = authToken(context, alias);
270            if (authToken == null) {
271                return null;
272            }
273            IKeyChainService keyChainService = keyChainConnection.getService();
274            byte[] certificateBytes = keyChainService.getCertificate(alias, authToken);
275            return new X509Certificate[] { toCertificate(certificateBytes) };
276        } catch (RemoteException e) {
277            throw new KeyChainException(e);
278        } catch (RuntimeException e) {
279            // only certain RuntimeExceptions can be propagated across the IKeyChainService call
280            throw new KeyChainException(e);
281        } finally {
282            keyChainConnection.close();
283        }
284    }
285
286    private static PrivateKey toPrivateKey(byte[] bytes) {
287        if (bytes == null) {
288            throw new IllegalArgumentException("bytes == null");
289        }
290        try {
291            KeyPair keyPair = (KeyPair) Credentials.convertFromPem(bytes).get(0);
292            return keyPair.getPrivate();
293        } catch (IOException e) {
294            throw new AssertionError(e);
295        }
296    }
297
298    private static X509Certificate toCertificate(byte[] bytes) {
299        if (bytes == null) {
300            throw new IllegalArgumentException("bytes == null");
301        }
302        try {
303            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
304            Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
305            return (X509Certificate) cert;
306        } catch (CertificateException e) {
307            throw new AssertionError(e);
308        }
309    }
310
311    private static String authToken(Context context, String alias) {
312        AccountManager accountManager = AccountManager.get(context);
313        AccountManagerFuture<Bundle> future = accountManager.getAuthToken(getAccount(context),
314                                                                          alias,
315                                                                          false,
316                                                                          null,
317                                                                          null);
318        Bundle bundle;
319        try {
320            bundle = future.getResult();
321        } catch (OperationCanceledException e) {
322            throw new AssertionError(e);
323        } catch (IOException e) {
324            // KeyChainAccountAuthenticator doesn't do I/O
325            throw new AssertionError(e);
326        } catch (AuthenticatorException e) {
327            throw new AssertionError(e);
328        }
329        Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
330        if (intent != null) {
331            return null;
332        }
333        String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
334        if (authToken == null) {
335            throw new AssertionError("Invalid authtoken");
336        }
337        return authToken;
338    }
339
340    private static Account getAccount(Context context) {
341        AccountManager accountManager = AccountManager.get(context);
342        Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
343        if (accounts.length == 0) {
344            try {
345                // Account is created if necessary during binding of the IKeyChainService
346                bind(context).close();
347            } catch (InterruptedException e) {
348                throw new AssertionError(e);
349            }
350            accounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
351        }
352        return accounts[0];
353    }
354
355    /**
356     * @hide for reuse by CertInstaller and Settings.
357     * @see KeyChain#bind
358     */
359    public final static class KeyChainConnection implements Closeable {
360        private final Context context;
361        private final ServiceConnection serviceConnection;
362        private final IKeyChainService service;
363        private KeyChainConnection(Context context,
364                                   ServiceConnection serviceConnection,
365                                   IKeyChainService service) {
366            this.context = context;
367            this.serviceConnection = serviceConnection;
368            this.service = service;
369        }
370        @Override public void close() {
371            context.unbindService(serviceConnection);
372        }
373        public IKeyChainService getService() {
374            return service;
375        }
376    }
377
378    /**
379     * @hide for reuse by CertInstaller and Settings.
380     *
381     * Caller should call unbindService on the result when finished.
382     */
383    public static KeyChainConnection bind(Context context) throws InterruptedException {
384        if (context == null) {
385            throw new NullPointerException("context == null");
386        }
387        ensureNotOnMainThread(context);
388        final BlockingQueue<IKeyChainService> q = new LinkedBlockingQueue<IKeyChainService>(1);
389        ServiceConnection keyChainServiceConnection = new ServiceConnection() {
390            @Override public void onServiceConnected(ComponentName name, IBinder service) {
391                try {
392                    q.put(IKeyChainService.Stub.asInterface(service));
393                } catch (InterruptedException e) {
394                    throw new AssertionError(e);
395                }
396            }
397            @Override public void onServiceDisconnected(ComponentName name) {}
398        };
399        boolean isBound = context.bindService(new Intent(IKeyChainService.class.getName()),
400                                              keyChainServiceConnection,
401                                              Context.BIND_AUTO_CREATE);
402        if (!isBound) {
403            throw new AssertionError("could not bind to KeyChainService");
404        }
405        return new KeyChainConnection(context, keyChainServiceConnection, q.take());
406    }
407
408    private static void ensureNotOnMainThread(Context context) {
409        Looper looper = Looper.myLooper();
410        if (looper != null && looper == context.getMainLooper()) {
411            throw new IllegalStateException(
412                    "calling this from your main thread can lead to deadlock");
413        }
414    }
415}
416