blob: 1190368a5ab434c915a1b1f59262ae1e96f43ce3 [file] [log] [blame]
Brian Carlstrom3e6251d2011-04-11 09:05:06 -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 */
16
17package com.android.keychain;
18
19import android.accounts.AbstractAccountAuthenticator;
20import android.accounts.Account;
21import android.accounts.AccountAuthenticatorResponse;
22import android.accounts.AccountManager;
Brian Carlstroma58db542011-05-11 23:02:20 -070023import android.accounts.AccountsException;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070024import android.accounts.NetworkErrorException;
25import android.app.Service;
26import android.content.Context;
27import android.content.Intent;
28import android.os.Bundle;
29import android.os.IBinder;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070030import android.security.Credentials;
31import android.security.IKeyChainService;
32import android.security.KeyChain;
33import android.security.KeyStore;
34import android.util.Log;
35import java.io.ByteArrayInputStream;
Brian Carlstroma58db542011-05-11 23:02:20 -070036import java.io.IOException;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070037import java.nio.charset.Charsets;
38import java.security.SecureRandom;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070039import java.security.cert.CertificateException;
40import java.security.cert.CertificateFactory;
41import java.security.cert.X509Certificate;
Brian Carlstroma58db542011-05-11 23:02:20 -070042import java.util.Collections;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070043import javax.security.auth.x500.X500Principal;
44import org.apache.harmony.luni.util.Base64;
Brian Carlstroma58db542011-05-11 23:02:20 -070045import org.apache.harmony.xnet.provider.jsse.TrustedCertificateStore;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070046
47public class KeyChainService extends Service {
48
49 private static final String TAG = "KeyChainService";
50
51 private AccountManager mAccountManager;
52
53 private final Object mAccountLock = new Object();
54 private Account mAccount;
55
56 @Override public void onCreate() {
57 super.onCreate();
58 mAccountManager = AccountManager.get(this);
59 }
60
61 private final IKeyChainService.Stub mIKeyChainService = new IKeyChainService.Stub() {
62
63 private final KeyStore mKeyStore = KeyStore.getInstance();
Brian Carlstroma58db542011-05-11 23:02:20 -070064 private final TrustedCertificateStore mTrustedCertificateStore
65 = new TrustedCertificateStore();
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070066
67 private boolean isKeyStoreUnlocked() {
68 return (mKeyStore.test() == KeyStore.NO_ERROR);
69 }
70
Brian Carlstroma58db542011-05-11 23:02:20 -070071 @Override public byte[] getPrivate(String alias, String authToken) {
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070072 if (alias == null) {
73 throw new NullPointerException("alias == null");
74 }
75 if (authToken == null) {
76 throw new NullPointerException("authToken == null");
77 }
78 if (!isKeyStoreUnlocked()) {
79 throw new IllegalStateException("keystore locked");
80 }
81 if (!mAccountManager.peekAuthToken(mAccount, alias).equals(authToken)) {
82 throw new IllegalStateException("authtoken mismatch");
83 }
84 String key = Credentials.USER_PRIVATE_KEY + alias;
85 byte[] bytes = mKeyStore.get(key.getBytes(Charsets.UTF_8));
86 if (bytes == null) {
87 throw new IllegalStateException("keystore value missing");
88 }
89 return bytes;
90 }
91
Brian Carlstroma58db542011-05-11 23:02:20 -070092 @Override public byte[] getCertificate(String alias, String authToken) {
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070093 return getCert(Credentials.USER_CERTIFICATE, alias, authToken);
94 }
Brian Carlstroma58db542011-05-11 23:02:20 -070095 @Override public byte[] getCaCertificate(String alias, String authToken) {
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070096 return getCert(Credentials.CA_CERTIFICATE, alias, authToken);
97 }
98
Brian Carlstroma58db542011-05-11 23:02:20 -070099 private byte[] getCert(String type, String alias, String authToken) {
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700100 if (alias == null) {
101 throw new NullPointerException("alias == null");
102 }
103 if (authToken == null) {
104 throw new NullPointerException("authtoken == null");
105 }
106 if (!isKeyStoreUnlocked()) {
107 throw new IllegalStateException("keystore locked");
108 }
109 String authAlias = (type.equals(Credentials.CA_CERTIFICATE))
110 ? (alias + KeyChain.CA_SUFFIX)
111 : alias;
112 if (!mAccountManager.peekAuthToken(mAccount, authAlias).equals(authToken)) {
113 throw new IllegalStateException("authtoken mismatch");
114 }
115 String key = type + alias;
116 byte[] bytes = mKeyStore.get(key.getBytes(Charsets.UTF_8));
117 if (bytes == null) {
118 throw new IllegalStateException("keystore value missing");
119 }
120 return bytes;
121 }
122
123 @Override public String findIssuer(Bundle bundle) {
124 if (bundle == null) {
125 throw new NullPointerException("bundle == null");
126 }
127 X509Certificate cert = KeyChain.toCertificate(bundle);
128 if (cert == null) {
129 throw new IllegalArgumentException("no cert in bundle");
130 }
131 X500Principal issuer = cert.getIssuerX500Principal();
132 if (issuer == null) {
133 throw new IllegalStateException();
134 }
135 byte[] aliasPrefix = Credentials.CA_CERTIFICATE.getBytes(Charsets.UTF_8);
136 byte[][] aliasSuffixes = mKeyStore.saw(aliasPrefix);
137 if (aliasSuffixes == null) {
138 return null;
139 }
140
141 // TODO if the keystore would notify us of changes, we
142 // could cache the certs and perform a lookup by issuer
143 for (byte[] aliasSuffix : aliasSuffixes) {
144 byte[] alias = concatenate(aliasPrefix, aliasSuffix);
145 byte[] bytes = mKeyStore.get(alias);
146 try {
147 // TODO we could at least cache the byte to cert parsing
Brian Carlstroma58db542011-05-11 23:02:20 -0700148 X509Certificate caCert = parseCertificate(bytes);
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700149 if (issuer.equals(caCert.getSubjectX500Principal())) {
150 // will throw exception on failure to verify.
151 // this can happen if there are two CAs with
152 // the same name but with different public
153 // keys, which does in fact happen, so we will
154 // try to continue and not just fail fast.
155 cert.verify(caCert.getPublicKey());
156 return new String(aliasSuffix, Charsets.UTF_8);
157 }
158 } catch (Exception ignored) {
159 }
160 }
161 return null;
162 }
163
Brian Carlstroma58db542011-05-11 23:02:20 -0700164 private X509Certificate parseCertificate(byte[] bytes) throws CertificateException {
165 CertificateFactory cf = CertificateFactory.getInstance("X.509");
166 return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bytes));
167 }
168
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700169 private byte[] concatenate(byte[] a, byte[] b) {
170 byte[] result = new byte[a.length + b.length];
171 System.arraycopy(a, 0, result, 0, a.length);
172 System.arraycopy(b, 0, result, a.length, b.length);
173 return result;
174 }
Brian Carlstroma58db542011-05-11 23:02:20 -0700175
176 @Override public void installCaCertificate(byte[] caCertificate) {
177 // only the CertInstaller should be able to add new trusted CAs
178 final String expectedPackage = "com.android.certinstaller";
179 final String actualPackage = getPackageManager().getNameForUid(getCallingUid());
180 if (!expectedPackage.equals(actualPackage)) {
181 throw new IllegalStateException(actualPackage);
182 }
183 try {
184 synchronized (mTrustedCertificateStore) {
185 mTrustedCertificateStore.installCertificate(parseCertificate(caCertificate));
186 }
187 } catch (IOException e) {
188 throw new IllegalStateException(e);
189 } catch (CertificateException e) {
190 throw new IllegalStateException(e);
191 }
192 }
193 @Override public boolean reset() {
194 // only Settings should be able to reset
195 final String expectedPackage = "android.uid.system:1000";
196 final String actualPackage = getPackageManager().getNameForUid(getCallingUid());
197 if (!expectedPackage.equals(actualPackage)) {
198 throw new IllegalStateException(actualPackage);
199 }
200 boolean ok = true;
201
202 synchronized (mAccountLock) {
203 // remote Accounts from AccountManager to revoke any
204 // granted credential grants to applications
205 Account[] accounts = mAccountManager.getAccountsByType(KeyChain.ACCOUNT_TYPE);
206 for (Account a : accounts) {
207 try {
208 if (!mAccountManager.removeAccount(a, null, null).getResult()) {
209 ok = false;
210 }
211 } catch (AccountsException e) {
212 Log.w(TAG, "Problem removing account " + a, e);
213 ok = false;
214 } catch (IOException e) {
215 Log.w(TAG, "Problem removing account " + a, e);
216 ok = false;
217 }
218 }
219 }
220
221 synchronized (mTrustedCertificateStore) {
222 // delete user-installed CA certs
223 for (String alias : mTrustedCertificateStore.aliases()) {
224 if (TrustedCertificateStore.isUser(alias)) {
225 try {
226 mTrustedCertificateStore.deleteCertificateEntry(alias);
227 } catch (IOException e) {
228 Log.w(TAG, "Problem removing CA certificate " + alias, e);
229 ok = false;
230 } catch (CertificateException e) {
231 Log.w(TAG, "Problem removing CA certificate " + alias, e);
232 ok = false;
233 }
234 }
235 }
236 return ok;
237 }
238 }
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700239 };
240
241 private class KeyChainAccountAuthenticator extends AbstractAccountAuthenticator {
242
243 /**
244 * 264 was picked becuase it is the length in bytes of Google
245 * authtokens which seems sufficiently long and guaranteed to
246 * be storable by AccountManager.
247 */
248 private final int AUTHTOKEN_LENGTH = 264;
249 private final SecureRandom mSecureRandom = new SecureRandom();
250
251 private KeyChainAccountAuthenticator(Context context) {
252 super(context);
253 }
254
255 @Override public Bundle editProperties(AccountAuthenticatorResponse response,
256 String accountType) {
257 return null;
258 }
259
260 @Override public Bundle addAccount(AccountAuthenticatorResponse response,
261 String accountType,
262 String authTokenType,
263 String[] requiredFeatures,
264 Bundle options) throws NetworkErrorException {
265 return null;
266 }
267
268 @Override public Bundle confirmCredentials(AccountAuthenticatorResponse response,
269 Account account,
270 Bundle options) throws NetworkErrorException {
271 return null;
272 }
273
274 /**
275 * Called on an AccountManager cache miss, so generate a new value.
276 */
277 @Override public Bundle getAuthToken(AccountAuthenticatorResponse response,
278 Account account,
279 String authTokenType,
280 Bundle options) throws NetworkErrorException {
281 byte[] bytes = new byte[AUTHTOKEN_LENGTH];
282 mSecureRandom.nextBytes(bytes);
283 String authToken = Base64.encode(bytes, Charsets.US_ASCII);
284 Bundle bundle = new Bundle();
285 bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
286 bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, KeyChain.ACCOUNT_TYPE);
287 bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);
288 return bundle;
289 }
290
291 @Override public String getAuthTokenLabel(String authTokenType) {
292 // return authTokenType unchanged, it was a user specified
293 // alias name, doesn't need to be localized
294 return authTokenType;
295 }
296
297 @Override public Bundle updateCredentials(AccountAuthenticatorResponse response,
298 Account account,
299 String authTokenType,
300 Bundle options) throws NetworkErrorException {
301 return null;
302 }
303
304 @Override public Bundle hasFeatures(AccountAuthenticatorResponse response,
305 Account account,
306 String[] features) throws NetworkErrorException {
307 return null;
308 }
309 };
310
311 private final IBinder mAuthenticator = new KeyChainAccountAuthenticator(this).getIBinder();
312
313 @Override public IBinder onBind(Intent intent) {
314 if (IKeyChainService.class.getName().equals(intent.getAction())) {
315
316 // ensure singleton keychain account exists
317 synchronized (mAccountLock) {
318 Account[] accounts = mAccountManager.getAccountsByType(KeyChain.ACCOUNT_TYPE);
319 if (accounts.length == 0) {
320 // TODO localize account name
321 mAccount = new Account("Android Key Chain", KeyChain.ACCOUNT_TYPE);
322 mAccountManager.addAccountExplicitly(mAccount, null, null);
323 } else if (accounts.length == 1) {
324 mAccount = accounts[0];
325 } else {
326 throw new IllegalStateException();
327 }
328 }
329
330 return mIKeyChainService;
331 }
332
333 if (AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
334 return mAuthenticator;
335 }
336
337 return null;
338 }
339}