AndroidKeyStore.java revision cc1fc6b6adc1edc2acaa42205b4ec5ca00bfd353
1/* 2 * Copyright (C) 2012 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 android.security; 18 19import org.apache.harmony.xnet.provider.jsse.OpenSSLEngine; 20import org.apache.harmony.xnet.provider.jsse.OpenSSLKeyHolder; 21 22import android.util.Log; 23 24import java.io.ByteArrayInputStream; 25import java.io.IOException; 26import java.io.InputStream; 27import java.io.OutputStream; 28import java.security.InvalidKeyException; 29import java.security.Key; 30import java.security.KeyStoreException; 31import java.security.KeyStoreSpi; 32import java.security.NoSuchAlgorithmException; 33import java.security.PrivateKey; 34import java.security.UnrecoverableKeyException; 35import java.security.cert.Certificate; 36import java.security.cert.CertificateEncodingException; 37import java.security.cert.CertificateException; 38import java.security.cert.CertificateFactory; 39import java.security.cert.X509Certificate; 40import java.util.ArrayList; 41import java.util.Collection; 42import java.util.Collections; 43import java.util.Date; 44import java.util.Enumeration; 45import java.util.HashSet; 46import java.util.Iterator; 47import java.util.Set; 48 49/** 50 * A java.security.KeyStore interface for the Android KeyStore. An instance of 51 * it can be created via the {@link java.security.KeyStore#getInstance(String) 52 * KeyStore.getInstance("AndroidKeyStore")} interface. This returns a 53 * java.security.KeyStore backed by this "AndroidKeyStore" implementation. 54 * <p> 55 * This is built on top of Android's keystore daemon. The convention of alias 56 * use is: 57 * <p> 58 * PrivateKeyEntry will have a Credentials.USER_PRIVATE_KEY as the private key, 59 * Credentials.USER_CERTIFICATE as the first certificate in the chain (the one 60 * that corresponds to the private key), and then a Credentials.CA_CERTIFICATE 61 * entry which will have the rest of the chain concatenated in BER format. 62 * <p> 63 * TrustedCertificateEntry will just have a Credentials.CA_CERTIFICATE entry 64 * with a single certificate. 65 * 66 * @hide 67 */ 68public class AndroidKeyStore extends KeyStoreSpi { 69 public static final String NAME = "AndroidKeyStore"; 70 71 private android.security.KeyStore mKeyStore; 72 73 @Override 74 public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, 75 UnrecoverableKeyException { 76 if (!isKeyEntry(alias)) { 77 return null; 78 } 79 80 final OpenSSLEngine engine = OpenSSLEngine.getInstance("keystore"); 81 try { 82 return engine.getPrivateKeyById(Credentials.USER_PRIVATE_KEY + alias); 83 } catch (InvalidKeyException e) { 84 UnrecoverableKeyException t = new UnrecoverableKeyException("Can't get key"); 85 t.initCause(e); 86 throw t; 87 } 88 } 89 90 @Override 91 public Certificate[] engineGetCertificateChain(String alias) { 92 if (alias == null) { 93 throw new NullPointerException("alias == null"); 94 } 95 96 final X509Certificate leaf = (X509Certificate) engineGetCertificate(alias); 97 if (leaf == null) { 98 return null; 99 } 100 101 final Certificate[] caList; 102 103 final byte[] caBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias); 104 if (caBytes != null) { 105 final Collection<X509Certificate> caChain = toCertificates(caBytes); 106 107 caList = new Certificate[caChain.size() + 1]; 108 109 final Iterator<X509Certificate> it = caChain.iterator(); 110 int i = 1; 111 while (it.hasNext()) { 112 caList[i++] = it.next(); 113 } 114 } else { 115 caList = new Certificate[1]; 116 } 117 118 caList[0] = leaf; 119 120 return caList; 121 } 122 123 @Override 124 public Certificate engineGetCertificate(String alias) { 125 if (alias == null) { 126 throw new NullPointerException("alias == null"); 127 } 128 129 byte[] certificate = mKeyStore.get(Credentials.USER_CERTIFICATE + alias); 130 if (certificate != null) { 131 return toCertificate(certificate); 132 } 133 134 certificate = mKeyStore.get(Credentials.CA_CERTIFICATE + alias); 135 if (certificate != null) { 136 return toCertificate(certificate); 137 } 138 139 return null; 140 } 141 142 private static X509Certificate toCertificate(byte[] bytes) { 143 try { 144 final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); 145 return (X509Certificate) certFactory 146 .generateCertificate(new ByteArrayInputStream(bytes)); 147 } catch (CertificateException e) { 148 Log.w(NAME, "Couldn't parse certificate in keystore", e); 149 return null; 150 } 151 } 152 153 @SuppressWarnings("unchecked") 154 private static Collection<X509Certificate> toCertificates(byte[] bytes) { 155 try { 156 final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); 157 return (Collection<X509Certificate>) certFactory 158 .generateCertificates(new ByteArrayInputStream(bytes)); 159 } catch (CertificateException e) { 160 Log.w(NAME, "Couldn't parse certificates in keystore", e); 161 return new ArrayList<X509Certificate>(); 162 } 163 } 164 165 private Date getModificationDate(String alias) { 166 final long epochMillis = mKeyStore.getmtime(alias); 167 if (epochMillis == -1L) { 168 return null; 169 } 170 171 return new Date(epochMillis); 172 } 173 174 @Override 175 public Date engineGetCreationDate(String alias) { 176 if (alias == null) { 177 throw new NullPointerException("alias == null"); 178 } 179 180 Date d = getModificationDate(Credentials.USER_PRIVATE_KEY + alias); 181 if (d != null) { 182 return d; 183 } 184 185 d = getModificationDate(Credentials.USER_CERTIFICATE + alias); 186 if (d != null) { 187 return d; 188 } 189 190 return getModificationDate(Credentials.CA_CERTIFICATE + alias); 191 } 192 193 @Override 194 public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) 195 throws KeyStoreException { 196 if ((password != null) && (password.length > 0)) { 197 throw new KeyStoreException("entries cannot be protected with passwords"); 198 } 199 200 if (key instanceof PrivateKey) { 201 setPrivateKeyEntry(alias, (PrivateKey) key, chain); 202 } else { 203 throw new KeyStoreException("Only PrivateKeys are supported"); 204 } 205 } 206 207 private void setPrivateKeyEntry(String alias, PrivateKey key, Certificate[] chain) 208 throws KeyStoreException { 209 byte[] keyBytes = null; 210 211 final String pkeyAlias; 212 if (key instanceof OpenSSLKeyHolder) { 213 pkeyAlias = ((OpenSSLKeyHolder) key).getOpenSSLKey().getAlias(); 214 } else { 215 pkeyAlias = null; 216 } 217 218 final boolean shouldReplacePrivateKey; 219 if (pkeyAlias != null && pkeyAlias.startsWith(Credentials.USER_PRIVATE_KEY)) { 220 final String keySubalias = pkeyAlias.substring(Credentials.USER_PRIVATE_KEY.length()); 221 if (!alias.equals(keySubalias)) { 222 throw new KeyStoreException("Can only replace keys with same alias: " + alias 223 + " != " + keySubalias); 224 } 225 226 shouldReplacePrivateKey = false; 227 } else { 228 // Make sure the PrivateKey format is the one we support. 229 final String keyFormat = key.getFormat(); 230 if ((keyFormat == null) || (!"PKCS#8".equals(keyFormat))) { 231 throw new KeyStoreException( 232 "Only PrivateKeys that can be encoded into PKCS#8 are supported"); 233 } 234 235 // Make sure we can actually encode the key. 236 keyBytes = key.getEncoded(); 237 if (keyBytes == null) { 238 throw new KeyStoreException("PrivateKey has no encoding"); 239 } 240 241 shouldReplacePrivateKey = true; 242 } 243 244 // Make sure the chain exists since this is a PrivateKey 245 if ((chain == null) || (chain.length == 0)) { 246 throw new KeyStoreException("Must supply at least one Certificate with PrivateKey"); 247 } 248 249 // Do chain type checking. 250 X509Certificate[] x509chain = new X509Certificate[chain.length]; 251 for (int i = 0; i < chain.length; i++) { 252 if (!"X.509".equals(chain[i].getType())) { 253 throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #" 254 + i); 255 } 256 257 if (!(chain[i] instanceof X509Certificate)) { 258 throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #" 259 + i); 260 } 261 262 x509chain[i] = (X509Certificate) chain[i]; 263 } 264 265 final byte[] userCertBytes; 266 try { 267 userCertBytes = x509chain[0].getEncoded(); 268 } catch (CertificateEncodingException e) { 269 throw new KeyStoreException("Couldn't encode certificate #1", e); 270 } 271 272 /* 273 * If we have a chain, store it in the CA certificate slot for this 274 * alias as concatenated DER-encoded certificates. These can be 275 * deserialized by {@link CertificateFactory#generateCertificates}. 276 */ 277 final byte[] chainBytes; 278 if (chain.length > 1) { 279 /* 280 * The chain is passed in as {user_cert, ca_cert_1, ca_cert_2, ...} 281 * so we only need the certificates starting at index 1. 282 */ 283 final byte[][] certsBytes = new byte[x509chain.length - 1][]; 284 int totalCertLength = 0; 285 for (int i = 0; i < certsBytes.length; i++) { 286 try { 287 certsBytes[i] = x509chain[i + 1].getEncoded(); 288 totalCertLength += certsBytes[i].length; 289 } catch (CertificateEncodingException e) { 290 throw new KeyStoreException("Can't encode Certificate #" + i, e); 291 } 292 } 293 294 /* 295 * Serialize this into one byte array so we can later call 296 * CertificateFactory#generateCertificates to recover them. 297 */ 298 chainBytes = new byte[totalCertLength]; 299 int outputOffset = 0; 300 for (int i = 0; i < certsBytes.length; i++) { 301 final int certLength = certsBytes[i].length; 302 System.arraycopy(certsBytes[i], 0, chainBytes, outputOffset, certLength); 303 outputOffset += certLength; 304 certsBytes[i] = null; 305 } 306 } else { 307 chainBytes = null; 308 } 309 310 /* 311 * Make sure we clear out all the appropriate types before trying to 312 * write. 313 */ 314 if (shouldReplacePrivateKey) { 315 Credentials.deleteAllTypesForAlias(mKeyStore, alias); 316 } else { 317 Credentials.deleteCertificateTypesForAlias(mKeyStore, alias); 318 } 319 320 if (shouldReplacePrivateKey 321 && !mKeyStore.importKey(Credentials.USER_PRIVATE_KEY + alias, keyBytes)) { 322 Credentials.deleteAllTypesForAlias(mKeyStore, alias); 323 throw new KeyStoreException("Couldn't put private key in keystore"); 324 } else if (!mKeyStore.put(Credentials.USER_CERTIFICATE + alias, userCertBytes)) { 325 Credentials.deleteAllTypesForAlias(mKeyStore, alias); 326 throw new KeyStoreException("Couldn't put certificate #1 in keystore"); 327 } else if (chainBytes != null 328 && !mKeyStore.put(Credentials.CA_CERTIFICATE + alias, chainBytes)) { 329 Credentials.deleteAllTypesForAlias(mKeyStore, alias); 330 throw new KeyStoreException("Couldn't put certificate chain in keystore"); 331 } 332 } 333 334 @Override 335 public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain) 336 throws KeyStoreException { 337 throw new KeyStoreException("Operation not supported because key encoding is unknown"); 338 } 339 340 @Override 341 public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { 342 if (isKeyEntry(alias)) { 343 throw new KeyStoreException("Entry exists and is not a trusted certificate"); 344 } 345 346 // We can't set something to null. 347 if (cert == null) { 348 throw new NullPointerException("cert == null"); 349 } 350 351 final byte[] encoded; 352 try { 353 encoded = cert.getEncoded(); 354 } catch (CertificateEncodingException e) { 355 throw new KeyStoreException(e); 356 } 357 358 if (!mKeyStore.put(Credentials.CA_CERTIFICATE + alias, encoded)) { 359 throw new KeyStoreException("Couldn't insert certificate; is KeyStore initialized?"); 360 } 361 } 362 363 @Override 364 public void engineDeleteEntry(String alias) throws KeyStoreException { 365 if (!isKeyEntry(alias) && !isCertificateEntry(alias)) { 366 return; 367 } 368 369 if (!Credentials.deleteAllTypesForAlias(mKeyStore, alias)) { 370 throw new KeyStoreException("No such entry " + alias); 371 } 372 } 373 374 private Set<String> getUniqueAliases() { 375 final String[] rawAliases = mKeyStore.saw(""); 376 if (rawAliases == null) { 377 return new HashSet<String>(); 378 } 379 380 final Set<String> aliases = new HashSet<String>(rawAliases.length); 381 for (String alias : rawAliases) { 382 final int idx = alias.indexOf('_'); 383 if ((idx == -1) || (alias.length() <= idx)) { 384 Log.e(NAME, "invalid alias: " + alias); 385 continue; 386 } 387 388 aliases.add(new String(alias.substring(idx + 1))); 389 } 390 391 return aliases; 392 } 393 394 @Override 395 public Enumeration<String> engineAliases() { 396 return Collections.enumeration(getUniqueAliases()); 397 } 398 399 @Override 400 public boolean engineContainsAlias(String alias) { 401 if (alias == null) { 402 throw new NullPointerException("alias == null"); 403 } 404 405 return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias) 406 || mKeyStore.contains(Credentials.USER_CERTIFICATE + alias) 407 || mKeyStore.contains(Credentials.CA_CERTIFICATE + alias); 408 } 409 410 @Override 411 public int engineSize() { 412 return getUniqueAliases().size(); 413 } 414 415 @Override 416 public boolean engineIsKeyEntry(String alias) { 417 return isKeyEntry(alias); 418 } 419 420 private boolean isKeyEntry(String alias) { 421 if (alias == null) { 422 throw new NullPointerException("alias == null"); 423 } 424 425 return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias); 426 } 427 428 private boolean isCertificateEntry(String alias) { 429 if (alias == null) { 430 throw new NullPointerException("alias == null"); 431 } 432 433 return mKeyStore.contains(Credentials.CA_CERTIFICATE + alias); 434 } 435 436 @Override 437 public boolean engineIsCertificateEntry(String alias) { 438 return !isKeyEntry(alias) && isCertificateEntry(alias); 439 } 440 441 @Override 442 public String engineGetCertificateAlias(Certificate cert) { 443 if (cert == null) { 444 return null; 445 } 446 447 final Set<String> nonCaEntries = new HashSet<String>(); 448 449 /* 450 * First scan the PrivateKeyEntry types. The KeyStoreSpi documentation 451 * says to only compare the first certificate in the chain which is 452 * equivalent to the USER_CERTIFICATE prefix for the Android keystore 453 * convention. 454 */ 455 final String[] certAliases = mKeyStore.saw(Credentials.USER_CERTIFICATE); 456 for (String alias : certAliases) { 457 final byte[] certBytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias); 458 if (certBytes == null) { 459 continue; 460 } 461 462 final Certificate c = toCertificate(certBytes); 463 nonCaEntries.add(alias); 464 465 if (cert.equals(c)) { 466 return alias; 467 } 468 } 469 470 /* 471 * Look at all the TrustedCertificateEntry types. Skip all the 472 * PrivateKeyEntry we looked at above. 473 */ 474 final String[] caAliases = mKeyStore.saw(Credentials.CA_CERTIFICATE); 475 for (String alias : caAliases) { 476 if (nonCaEntries.contains(alias)) { 477 continue; 478 } 479 480 final byte[] certBytes = mKeyStore.get(Credentials.CA_CERTIFICATE + alias); 481 if (certBytes == null) { 482 continue; 483 } 484 485 final Certificate c = toCertificate(mKeyStore.get(Credentials.CA_CERTIFICATE + alias)); 486 if (cert.equals(c)) { 487 return alias; 488 } 489 } 490 491 return null; 492 } 493 494 @Override 495 public void engineStore(OutputStream stream, char[] password) throws IOException, 496 NoSuchAlgorithmException, CertificateException { 497 throw new UnsupportedOperationException("Can not serialize AndroidKeyStore to OutputStream"); 498 } 499 500 @Override 501 public void engineLoad(InputStream stream, char[] password) throws IOException, 502 NoSuchAlgorithmException, CertificateException { 503 if (stream != null) { 504 throw new IllegalArgumentException("InputStream not supported"); 505 } 506 507 if (password != null) { 508 throw new IllegalArgumentException("password not supported"); 509 } 510 511 // Unfortunate name collision. 512 mKeyStore = android.security.KeyStore.getInstance(); 513 } 514 515} 516