KeyChain.java revision 9d7faa91be6661eccf73494f1ab96ae9a28d42d7
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.PrivateKey;
40import java.security.cert.Certificate;
41import java.security.cert.CertificateException;
42import java.security.cert.CertificateFactory;
43import java.security.cert.X509Certificate;
44import java.security.spec.InvalidKeySpecException;
45import java.security.spec.PKCS8EncodedKeySpec;
46import java.util.concurrent.BlockingQueue;
47import java.util.concurrent.LinkedBlockingQueue;
48
49/**
50 * @hide
51 */
52public final class KeyChain {
53
54    private static final String TAG = "KeyChain";
55
56    /**
57     * @hide Also used by KeyChainService implementation
58     */
59    public static final String ACCOUNT_TYPE = "com.android.keychain";
60
61    /**
62     * @hide Also used by KeyChainActivity implementation
63     */
64    public static final String EXTRA_RESPONSE = "response";
65
66    /**
67     * Launches an {@code Activity} for the user to select the alias
68     * for a private key and certificate pair for authentication. The
69     * selected alias or null will be returned via the
70     * IKeyChainAliasResponse callback.
71     */
72    public static void choosePrivateKeyAlias(Activity activity, KeyChainAliasResponse response) {
73        if (activity == null) {
74            throw new NullPointerException("activity == null");
75        }
76        if (response == null) {
77            throw new NullPointerException("response == null");
78        }
79        Intent intent = new Intent("com.android.keychain.CHOOSER");
80        intent.putExtra(EXTRA_RESPONSE, new AliasResponse(activity, response));
81        activity.startActivity(intent);
82    }
83
84    private static class AliasResponse extends IKeyChainAliasResponse.Stub {
85        private final Activity activity;
86        private final KeyChainAliasResponse keyChainAliasResponse;
87        private AliasResponse(Activity activity, KeyChainAliasResponse keyChainAliasResponse) {
88            this.activity = activity;
89            this.keyChainAliasResponse = keyChainAliasResponse;
90        }
91        @Override public void alias(String alias) {
92            if (alias == null) {
93                keyChainAliasResponse.alias(null);
94                return;
95            }
96            AccountManager accountManager = AccountManager.get(activity);
97            accountManager.getAuthToken(getAccount(activity),
98                                        alias,
99                                        null,
100                                        activity,
101                                        new AliasAccountManagerCallback(keyChainAliasResponse,
102                                                                        alias),
103                                        null);
104        }
105    }
106
107    private static class AliasAccountManagerCallback implements AccountManagerCallback<Bundle> {
108        private final KeyChainAliasResponse keyChainAliasResponse;
109        private final String alias;
110        private AliasAccountManagerCallback(KeyChainAliasResponse keyChainAliasResponse,
111                                            String alias) {
112            this.keyChainAliasResponse = keyChainAliasResponse;
113            this.alias = alias;
114        }
115        @Override public void run(AccountManagerFuture<Bundle> future) {
116            Bundle bundle;
117            try {
118                bundle = future.getResult();
119            } catch (OperationCanceledException e) {
120                keyChainAliasResponse.alias(null);
121                return;
122            } catch (IOException e) {
123                keyChainAliasResponse.alias(null);
124                return;
125            } catch (AuthenticatorException e) {
126                keyChainAliasResponse.alias(null);
127                return;
128            }
129            String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
130            if (authToken != null) {
131                keyChainAliasResponse.alias(alias);
132            } else {
133                keyChainAliasResponse.alias(null);
134            }
135        }
136    }
137
138    /**
139     * Returns the {@code PrivateKey} for the requested alias, or null
140     * if no there is no result.
141     */
142    public static PrivateKey getPrivateKey(Context context, String alias)
143            throws InterruptedException, RemoteException {
144        if (alias == null) {
145            throw new NullPointerException("alias == null");
146        }
147        KeyChainConnection keyChainConnection = bind(context);
148        try {
149            String authToken = authToken(context, alias);
150            if (authToken == null) {
151                return null;
152            }
153            IKeyChainService keyChainService = keyChainConnection.getService();
154            byte[] privateKeyBytes = keyChainService.getPrivateKey(alias, authToken);
155            return toPrivateKey(privateKeyBytes);
156        } finally {
157            keyChainConnection.close();
158        }
159    }
160
161    /**
162     * Returns the {@code X509Certificate} chain for the requested
163     * alias, or null if no there is no result.
164     */
165    public static X509Certificate[] getCertificateChain(Context context, String alias)
166            throws InterruptedException, RemoteException {
167        if (alias == null) {
168            throw new NullPointerException("alias == null");
169        }
170        KeyChainConnection keyChainConnection = bind(context);
171        try {
172            String authToken = authToken(context, alias);
173            if (authToken == null) {
174                return null;
175            }
176            IKeyChainService keyChainService = keyChainConnection.getService();
177            byte[] certificateBytes = keyChainService.getCertificate(alias, authToken);
178            return new X509Certificate[] { toCertificate(certificateBytes) };
179        } finally {
180            keyChainConnection.close();
181        }
182    }
183
184    private static PrivateKey toPrivateKey(byte[] bytes) {
185        if (bytes == null) {
186            throw new IllegalArgumentException("bytes == null");
187        }
188        try {
189            KeyPair keyPair = (KeyPair) Credentials.convertFromPem(bytes).get(0);
190            return keyPair.getPrivate();
191        } catch (IOException e) {
192            throw new AssertionError(e);
193        }
194    }
195
196    private static X509Certificate toCertificate(byte[] bytes) {
197        if (bytes == null) {
198            throw new IllegalArgumentException("bytes == null");
199        }
200        try {
201            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
202            Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
203            return (X509Certificate) cert;
204        } catch (CertificateException e) {
205            throw new AssertionError(e);
206        }
207    }
208
209    private static String authToken(Context context, String alias) {
210        AccountManager accountManager = AccountManager.get(context);
211        AccountManagerFuture<Bundle> future = accountManager.getAuthToken(getAccount(context),
212                                                                          alias,
213                                                                          false,
214                                                                          null,
215                                                                          null);
216        Bundle bundle;
217        try {
218            bundle = future.getResult();
219        } catch (OperationCanceledException e) {
220            throw new AssertionError(e);
221        } catch (IOException e) {
222            // KeyChainAccountAuthenticator doesn't do I/O
223            throw new AssertionError(e);
224        } catch (AuthenticatorException e) {
225            throw new AssertionError(e);
226        }
227        Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
228        if (intent != null) {
229            return null;
230        }
231        String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
232        if (authToken == null) {
233            throw new AssertionError("Invalid authtoken");
234        }
235        return authToken;
236    }
237
238    private static Account getAccount(Context context) {
239        AccountManager accountManager = AccountManager.get(context);
240        Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
241        if (accounts.length == 0) {
242            try {
243                // Account is created if necessary during binding of the IKeyChainService
244                bind(context).close();
245            } catch (InterruptedException e) {
246                throw new AssertionError(e);
247            }
248            accounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
249        }
250        return accounts[0];
251    }
252
253    /**
254     * @hide for reuse by CertInstaller and Settings.
255     * @see KeyChain#bind
256     */
257    public final static class KeyChainConnection implements Closeable {
258        private final Context context;
259        private final ServiceConnection serviceConnection;
260        private final IKeyChainService service;
261        private KeyChainConnection(Context context,
262                                   ServiceConnection serviceConnection,
263                                   IKeyChainService service) {
264            this.context = context;
265            this.serviceConnection = serviceConnection;
266            this.service = service;
267        }
268        @Override public void close() {
269            context.unbindService(serviceConnection);
270        }
271        public IKeyChainService getService() {
272            return service;
273        }
274    }
275
276    /**
277     * @hide for reuse by CertInstaller and Settings.
278     *
279     * Caller should call unbindService on the result when finished.
280     */
281    public static KeyChainConnection bind(Context context) throws InterruptedException {
282        if (context == null) {
283            throw new NullPointerException("context == null");
284        }
285        ensureNotOnMainThread(context);
286        final BlockingQueue<IKeyChainService> q = new LinkedBlockingQueue<IKeyChainService>(1);
287        ServiceConnection keyChainServiceConnection = new ServiceConnection() {
288            @Override public void onServiceConnected(ComponentName name, IBinder service) {
289                try {
290                    q.put(IKeyChainService.Stub.asInterface(service));
291                } catch (InterruptedException e) {
292                    throw new AssertionError(e);
293                }
294            }
295            @Override public void onServiceDisconnected(ComponentName name) {}
296        };
297        boolean isBound = context.bindService(new Intent(IKeyChainService.class.getName()),
298                                              keyChainServiceConnection,
299                                              Context.BIND_AUTO_CREATE);
300        if (!isBound) {
301            throw new AssertionError("could not bind to KeyChainService");
302        }
303        return new KeyChainConnection(context, keyChainServiceConnection, q.take());
304    }
305
306    private static void ensureNotOnMainThread(Context context) {
307        Looper looper = Looper.myLooper();
308        if (looper != null && looper == context.getMainLooper()) {
309            throw new IllegalStateException(
310                    "calling this from your main thread can lead to deadlock");
311        }
312    }
313}
314