blob: 69847bf0eba3be0d72b97c2820e0ac07f02ab345 [file] [log] [blame]
Brian Carlstromb9a07c12011-04-11 09:03:51 -07001/*
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.AccountManagerFuture;
21import android.accounts.AuthenticatorException;
22import android.accounts.OperationCanceledException;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.ServiceConnection;
27import android.os.Bundle;
28import android.os.IBinder;
29import android.os.Looper;
30import android.os.RemoteException;
31import android.util.Log;
32import dalvik.system.CloseGuard;
33import java.io.ByteArrayInputStream;
34import java.io.IOException;
35import java.security.KeyFactory;
36import java.security.NoSuchAlgorithmException;
37import java.security.PrivateKey;
38import java.security.cert.CertPathValidatorException;
39import java.security.cert.Certificate;
40import java.security.cert.CertificateEncodingException;
41import java.security.cert.CertificateException;
42import java.security.cert.CertificateFactory;
43import java.security.cert.TrustAnchor;
44import java.security.cert.X509Certificate;
45import java.security.spec.InvalidKeySpecException;
46import java.security.spec.PKCS8EncodedKeySpec;
47import org.apache.harmony.xnet.provider.jsse.IndexedPKIXParameters;
48import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl;
49
50/**
51 * @hide
52 */
53public final class KeyChain {
54
55 private static final String TAG = "KeyChain";
56
57 /**
58 * @hide Also used by KeyChainService implementation
59 */
60 public static final String ACCOUNT_TYPE = "com.android.keychain";
61
62 /**
63 * @hide Also used by KeyChainService implementation
64 */
65 // TODO This non-localized CA string to be removed when CAs moved out of keystore
66 public static final String CA_SUFFIX = " CA";
67
68 public static final String KEY_INTENT = "intent";
69
70 /**
71 * Intentionally not public to leave open the future possibility
72 * of hardware based keys. Callers should use {@link #toPrivateKey
73 * toPrivateKey} in order to convert a bundle to a {@code
74 * PrivateKey}
75 */
76 private static final String KEY_PKCS8 = "pkcs8";
77
78 /**
79 * Intentionally not public to leave open the future possibility
80 * of hardware based certs. Callers should use {@link
81 * #toCertificate toCertificate} in order to convert a bundle to a
82 * {@code PrivateKey}
83 */
84 private static final String KEY_X509 = "x509";
85
86 /**
87 * Returns an {@code Intent} for use with {@link
88 * android.app.Activity#startActivityForResult
89 * startActivityForResult}. The result will be returned via {@link
90 * android.app.Activity#onActivityResult onActivityResult} with
91 * {@link android.app.Activity#RESULT_OK RESULT_OK} and the alias
92 * in the returned {@code Intent}'s extra data with key {@link
93 * android.content.Intent#EXTRA_TEXT Intent.EXTRA_TEXT}.
94 */
95 public static Intent chooseAlias() {
96 return new Intent("com.android.keychain.CHOOSER");
97 }
98
99 /**
100 * Returns a new {@code KeyChain} instance. When the caller is
101 * done using the {@code KeyChain}, it must be closed with {@link
102 * #close()} or resource leaks will occur.
103 */
104 public static KeyChain getInstance(Context context) throws InterruptedException {
105 return new KeyChain(context);
106 }
107
108 private final AccountManager mAccountManager;
109
110 private final Object mServiceLock = new Object();
111 private IKeyChainService mService;
112 private boolean mIsBound;
113
114 private Account mAccount;
115
116 private ServiceConnection mServiceConnection = new ServiceConnection() {
117 @Override public void onServiceConnected(ComponentName name, IBinder service) {
118 synchronized (mServiceLock) {
119 mService = IKeyChainService.Stub.asInterface(service);
120 mServiceLock.notifyAll();
121
122 // Account is created if necessary during binding of the IKeyChainService
123 mAccount = mAccountManager.getAccountsByType(ACCOUNT_TYPE)[0];
124 }
125 }
126
127 @Override public void onServiceDisconnected(ComponentName name) {
128 synchronized (mServiceLock) {
129 mService = null;
130 }
131 }
132 };
133
134 private final Context mContext;
135
136 private final CloseGuard mGuard = CloseGuard.get();
137
138 private KeyChain(Context context) throws InterruptedException {
139 if (context == null) {
140 throw new NullPointerException("context == null");
141 }
142 mContext = context;
143 ensureNotOnMainThread();
144 mAccountManager = AccountManager.get(mContext);
145 mIsBound = mContext.bindService(new Intent(IKeyChainService.class.getName()),
146 mServiceConnection,
147 Context.BIND_AUTO_CREATE);
148 if (!mIsBound) {
149 throw new AssertionError();
150 }
151 synchronized (mServiceLock) {
152 // there is a race between binding on this thread and the
153 // callback on the main thread. wait until binding is done
154 // to be sure we have the mAccount initialized.
155 if (mService == null) {
156 mServiceLock.wait();
157 }
158 }
159 mGuard.open("close");
160 }
161
162 /**
163 * {@code Bundle} will contain {@link #KEY_INTENT} if user needs
164 * to confirm application access to requested key. In the alias
165 * does not exist or there is an error, null is
166 * returned. Otherwise the {@code Bundle} contains information
167 * representing the private key which can be interpreted with
168 * {@link #toPrivateKey toPrivateKey}.
169 *
170 * non-null alias
171 */
172 public Bundle getPrivate(String alias) {
173 return get(alias, Credentials.USER_PRIVATE_KEY);
174 }
175
176 public Bundle getCertificate(String alias) {
177 return get(alias, Credentials.USER_CERTIFICATE);
178 }
179
180 public Bundle getCaCertificate(String alias) {
181 return get(alias, Credentials.CA_CERTIFICATE);
182 }
183
184 private Bundle get(String alias, String type) {
185 if (alias == null) {
186 throw new NullPointerException("alias == null");
187 }
188 ensureNotOnMainThread();
189
190 String authAlias = (type.equals(Credentials.CA_CERTIFICATE)) ? (alias + CA_SUFFIX) : alias;
191 AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(mAccount,
192 authAlias,
193 false,
194 null,
195 null);
196 Bundle bundle;
197 try {
198 bundle = future.getResult();
199 } catch (OperationCanceledException e) {
200 throw new AssertionError(e);
201 } catch (IOException e) {
202 throw new AssertionError(e);
203 } catch (AuthenticatorException e) {
204 throw new AssertionError(e);
205 }
206 Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
207 if (intent != null) {
208 Bundle result = new Bundle();
209 // we don't want this Eclair compatability flag,
210 // it will prevent onActivityResult from being called
211 intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
212 result.putParcelable(KEY_INTENT, intent);
213 return result;
214 }
215 String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
216 if (authToken == null) {
217 throw new AssertionError("Invalid authtoken");
218 }
219
220 byte[] bytes;
221 try {
222 if (type.equals(Credentials.USER_PRIVATE_KEY)) {
223 bytes = mService.getPrivate(alias, authToken);
224 } else if (type.equals(Credentials.USER_CERTIFICATE)) {
225 bytes = mService.getCertificate(alias, authToken);
226 } else if (type.equals(Credentials.CA_CERTIFICATE)) {
227 bytes = mService.getCaCertificate(alias, authToken);
228 } else {
229 throw new AssertionError();
230 }
231 } catch (RemoteException e) {
232 throw new AssertionError(e);
233 }
234 if (bytes == null) {
235 throw new AssertionError();
236 }
237 Bundle result = new Bundle();
238 if (type.equals(Credentials.USER_PRIVATE_KEY)) {
239 result.putByteArray(KEY_PKCS8, bytes);
240 } else if (type.equals(Credentials.USER_CERTIFICATE)) {
241 result.putByteArray(KEY_X509, bytes);
242 } else if (type.equals(Credentials.CA_CERTIFICATE)) {
243 result.putByteArray(KEY_X509, bytes);
244 } else {
245 throw new AssertionError();
246 }
247 return result;
248 }
249
250 public static PrivateKey toPrivateKey(Bundle bundle) {
251 byte[] bytes = bundle.getByteArray(KEY_PKCS8);
252 if (bytes == null) {
253 throw new IllegalArgumentException("not a private key bundle");
254 }
255 try {
256 KeyFactory keyFactory = KeyFactory.getInstance("RSA");
257 return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(bytes));
258 } catch (NoSuchAlgorithmException e) {
259 throw new AssertionError(e);
260 } catch (InvalidKeySpecException e) {
261 throw new AssertionError(e);
262 }
263 }
264
265 public static Bundle fromPrivateKey(PrivateKey privateKey) {
266 Bundle bundle = new Bundle();
267 String format = privateKey.getFormat();
268 if (!format.equals("PKCS#8")) {
269 throw new IllegalArgumentException("Unsupported private key format " + format);
270 }
271 bundle.putByteArray(KEY_PKCS8, privateKey.getEncoded());
272 return bundle;
273 }
274
275 public static X509Certificate toCertificate(Bundle bundle) {
276 byte[] bytes = bundle.getByteArray(KEY_X509);
277 if (bytes == null) {
278 throw new IllegalArgumentException("not a certificate bundle");
279 }
280 try {
281 CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
282 Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
283 return (X509Certificate) cert;
284 } catch (CertificateException e) {
285 throw new AssertionError(e);
286 }
287 }
288
289 public static Bundle fromCertificate(Certificate cert) {
290 Bundle bundle = new Bundle();
291 String type = cert.getType();
292 if (!type.equals("X.509")) {
293 throw new IllegalArgumentException("Unsupported certificate type " + type);
294 }
295 try {
296 bundle.putByteArray(KEY_X509, cert.getEncoded());
297 } catch (CertificateEncodingException e) {
298 throw new AssertionError(e);
299 }
300 return bundle;
301 }
302
303 private void ensureNotOnMainThread() {
304 Looper looper = Looper.myLooper();
305 if (looper != null && looper == mContext.getMainLooper()) {
306 throw new IllegalStateException(
307 "calling this from your main thread can lead to deadlock");
308 }
309 }
310
311 public Bundle findIssuer(X509Certificate cert) {
312 if (cert == null) {
313 throw new NullPointerException("cert == null");
314 }
315 ensureNotOnMainThread();
316
317 // check and see if the issuer is already known to the default IndexedPKIXParameters
318 IndexedPKIXParameters index = SSLParametersImpl.getDefaultIndexedPKIXParameters();
319 try {
320 TrustAnchor anchor = index.findTrustAnchor(cert);
321 if (anchor != null && anchor.getTrustedCert() != null) {
322 X509Certificate ca = anchor.getTrustedCert();
323 return fromCertificate(ca);
324 }
325 } catch (CertPathValidatorException ignored) {
326 }
327
328 // otherwise, it might be a user installed CA in the keystore
329 String alias;
330 try {
331 alias = mService.findIssuer(fromCertificate(cert));
332 } catch (RemoteException e) {
333 throw new AssertionError(e);
334 }
335 if (alias == null) {
336 Log.w(TAG, "Lookup failed for issuer");
337 return null;
338 }
339
340 Bundle bundle = get(alias, Credentials.CA_CERTIFICATE);
341 Intent intent = bundle.getParcelable(KEY_INTENT);
342 if (intent != null) {
343 // permission still required
344 return bundle;
345 }
346 // add the found CA to the index for next time
347 X509Certificate ca = toCertificate(bundle);
348 index.index(new TrustAnchor(ca, null));
349 return bundle;
350 }
351
352 public void close() {
353 if (mIsBound) {
354 mContext.unbindService(mServiceConnection);
355 mIsBound = false;
356 mGuard.close();
357 }
358 }
359
360 protected void finalize() throws Throwable {
361 // note we don't close, we just warn.
362 // shouldn't be doing I/O in a finalizer,
363 // which the unbind would cause.
364 try {
365 if (mGuard != null) {
366 mGuard.warnIfOpen();
367 }
368 } finally {
369 super.finalize();
370 }
371 }
372}