blob: 8b0b0a1b890068d3826231e1af67f4fb53d05332 [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;
23import android.accounts.NetworkErrorException;
24import android.app.Service;
25import android.content.Context;
26import android.content.Intent;
27import android.os.Bundle;
28import android.os.IBinder;
29import android.os.RemoteException;
30import android.security.Credentials;
31import android.security.IKeyChainService;
32import android.security.KeyChain;
33import android.security.KeyStore;
34import android.util.Log;
35import java.io.ByteArrayInputStream;
36import java.nio.charset.Charsets;
37import java.security.SecureRandom;
38import java.security.cert.Certificate;
39import java.security.cert.CertificateException;
40import java.security.cert.CertificateFactory;
41import java.security.cert.X509Certificate;
42import javax.security.auth.x500.X500Principal;
43import org.apache.harmony.luni.util.Base64;
44
45public class KeyChainService extends Service {
46
47 private static final String TAG = "KeyChainService";
48
49 private AccountManager mAccountManager;
50
51 private final Object mAccountLock = new Object();
52 private Account mAccount;
53
54 @Override public void onCreate() {
55 super.onCreate();
56 mAccountManager = AccountManager.get(this);
57 }
58
59 private final IKeyChainService.Stub mIKeyChainService = new IKeyChainService.Stub() {
60
61 private final KeyStore mKeyStore = KeyStore.getInstance();
62
63 private boolean isKeyStoreUnlocked() {
64 return (mKeyStore.test() == KeyStore.NO_ERROR);
65 }
66
67 @Override public byte[] getPrivate(String alias, String authToken) throws RemoteException {
68 if (alias == null) {
69 throw new NullPointerException("alias == null");
70 }
71 if (authToken == null) {
72 throw new NullPointerException("authToken == null");
73 }
74 if (!isKeyStoreUnlocked()) {
75 throw new IllegalStateException("keystore locked");
76 }
77 if (!mAccountManager.peekAuthToken(mAccount, alias).equals(authToken)) {
78 throw new IllegalStateException("authtoken mismatch");
79 }
80 String key = Credentials.USER_PRIVATE_KEY + alias;
81 byte[] bytes = mKeyStore.get(key.getBytes(Charsets.UTF_8));
82 if (bytes == null) {
83 throw new IllegalStateException("keystore value missing");
84 }
85 return bytes;
86 }
87
88 @Override public byte[] getCertificate(String alias, String authToken)
89 throws RemoteException {
90 return getCert(Credentials.USER_CERTIFICATE, alias, authToken);
91 }
92 @Override public byte[] getCaCertificate(String alias, String authToken)
93 throws RemoteException {
94 return getCert(Credentials.CA_CERTIFICATE, alias, authToken);
95 }
96
97 private byte[] getCert(String type, String alias, String authToken)
98 throws RemoteException {
99 if (alias == null) {
100 throw new NullPointerException("alias == null");
101 }
102 if (authToken == null) {
103 throw new NullPointerException("authtoken == null");
104 }
105 if (!isKeyStoreUnlocked()) {
106 throw new IllegalStateException("keystore locked");
107 }
108 String authAlias = (type.equals(Credentials.CA_CERTIFICATE))
109 ? (alias + KeyChain.CA_SUFFIX)
110 : alias;
111 if (!mAccountManager.peekAuthToken(mAccount, authAlias).equals(authToken)) {
112 throw new IllegalStateException("authtoken mismatch");
113 }
114 String key = type + alias;
115 byte[] bytes = mKeyStore.get(key.getBytes(Charsets.UTF_8));
116 if (bytes == null) {
117 throw new IllegalStateException("keystore value missing");
118 }
119 return bytes;
120 }
121
122 @Override public String findIssuer(Bundle bundle) {
123 if (bundle == null) {
124 throw new NullPointerException("bundle == null");
125 }
126 X509Certificate cert = KeyChain.toCertificate(bundle);
127 if (cert == null) {
128 throw new IllegalArgumentException("no cert in bundle");
129 }
130 X500Principal issuer = cert.getIssuerX500Principal();
131 if (issuer == null) {
132 throw new IllegalStateException();
133 }
134 byte[] aliasPrefix = Credentials.CA_CERTIFICATE.getBytes(Charsets.UTF_8);
135 byte[][] aliasSuffixes = mKeyStore.saw(aliasPrefix);
136 if (aliasSuffixes == null) {
137 return null;
138 }
139
140 // TODO if the keystore would notify us of changes, we
141 // could cache the certs and perform a lookup by issuer
142 for (byte[] aliasSuffix : aliasSuffixes) {
143 byte[] alias = concatenate(aliasPrefix, aliasSuffix);
144 byte[] bytes = mKeyStore.get(alias);
145 try {
146 // TODO we could at least cache the byte to cert parsing
147 CertificateFactory cf = CertificateFactory.getInstance("X.509");
148 Certificate ca = cf.generateCertificate(new ByteArrayInputStream(bytes));
149 X509Certificate caCert = (X509Certificate) ca;
150 if (issuer.equals(caCert.getSubjectX500Principal())) {
151 // will throw exception on failure to verify.
152 // this can happen if there are two CAs with
153 // the same name but with different public
154 // keys, which does in fact happen, so we will
155 // try to continue and not just fail fast.
156 cert.verify(caCert.getPublicKey());
157 return new String(aliasSuffix, Charsets.UTF_8);
158 }
159 } catch (Exception ignored) {
160 }
161 }
162 return null;
163 }
164
165 private byte[] concatenate(byte[] a, byte[] b) {
166 byte[] result = new byte[a.length + b.length];
167 System.arraycopy(a, 0, result, 0, a.length);
168 System.arraycopy(b, 0, result, a.length, b.length);
169 return result;
170 }
171 };
172
173 private class KeyChainAccountAuthenticator extends AbstractAccountAuthenticator {
174
175 /**
176 * 264 was picked becuase it is the length in bytes of Google
177 * authtokens which seems sufficiently long and guaranteed to
178 * be storable by AccountManager.
179 */
180 private final int AUTHTOKEN_LENGTH = 264;
181 private final SecureRandom mSecureRandom = new SecureRandom();
182
183 private KeyChainAccountAuthenticator(Context context) {
184 super(context);
185 }
186
187 @Override public Bundle editProperties(AccountAuthenticatorResponse response,
188 String accountType) {
189 return null;
190 }
191
192 @Override public Bundle addAccount(AccountAuthenticatorResponse response,
193 String accountType,
194 String authTokenType,
195 String[] requiredFeatures,
196 Bundle options) throws NetworkErrorException {
197 return null;
198 }
199
200 @Override public Bundle confirmCredentials(AccountAuthenticatorResponse response,
201 Account account,
202 Bundle options) throws NetworkErrorException {
203 return null;
204 }
205
206 /**
207 * Called on an AccountManager cache miss, so generate a new value.
208 */
209 @Override public Bundle getAuthToken(AccountAuthenticatorResponse response,
210 Account account,
211 String authTokenType,
212 Bundle options) throws NetworkErrorException {
213 byte[] bytes = new byte[AUTHTOKEN_LENGTH];
214 mSecureRandom.nextBytes(bytes);
215 String authToken = Base64.encode(bytes, Charsets.US_ASCII);
216 Bundle bundle = new Bundle();
217 bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
218 bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, KeyChain.ACCOUNT_TYPE);
219 bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);
220 return bundle;
221 }
222
223 @Override public String getAuthTokenLabel(String authTokenType) {
224 // return authTokenType unchanged, it was a user specified
225 // alias name, doesn't need to be localized
226 return authTokenType;
227 }
228
229 @Override public Bundle updateCredentials(AccountAuthenticatorResponse response,
230 Account account,
231 String authTokenType,
232 Bundle options) throws NetworkErrorException {
233 return null;
234 }
235
236 @Override public Bundle hasFeatures(AccountAuthenticatorResponse response,
237 Account account,
238 String[] features) throws NetworkErrorException {
239 return null;
240 }
241 };
242
243 private final IBinder mAuthenticator = new KeyChainAccountAuthenticator(this).getIBinder();
244
245 @Override public IBinder onBind(Intent intent) {
246 if (IKeyChainService.class.getName().equals(intent.getAction())) {
247
248 // ensure singleton keychain account exists
249 synchronized (mAccountLock) {
250 Account[] accounts = mAccountManager.getAccountsByType(KeyChain.ACCOUNT_TYPE);
251 if (accounts.length == 0) {
252 // TODO localize account name
253 mAccount = new Account("Android Key Chain", KeyChain.ACCOUNT_TYPE);
254 mAccountManager.addAccountExplicitly(mAccount, null, null);
255 } else if (accounts.length == 1) {
256 mAccount = accounts[0];
257 } else {
258 throw new IllegalStateException();
259 }
260 }
261
262 return mIKeyChainService;
263 }
264
265 if (AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
266 return mAuthenticator;
267 }
268
269 return null;
270 }
271}