Merge "Simplify KeyChain API by removing now unneeded CA certificate lookup (1 of 3)"
This commit is contained in:
committed by
Android (Google) Code Review
commit
eb00a7e43f
@@ -25,10 +25,8 @@ import android.os.Bundle;
|
||||
*/
|
||||
interface IKeyChainService {
|
||||
// APIs used by KeyChain
|
||||
byte[] getPrivate(String alias, String authToken);
|
||||
byte[] getPrivateKey(String alias, String authToken);
|
||||
byte[] getCertificate(String alias, String authToken);
|
||||
byte[] getCaCertificate(String alias, String authToken);
|
||||
String findIssuer(in Bundle cert);
|
||||
|
||||
// APIs used by CertInstaller
|
||||
void installCaCertificate(in byte[] caCertificate);
|
||||
|
||||
@@ -28,24 +28,19 @@ import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import dalvik.system.CloseGuard;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.TrustAnchor;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import org.apache.harmony.xnet.provider.jsse.IndexedPKIXParameters;
|
||||
import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
@@ -59,30 +54,6 @@ public final class KeyChain {
|
||||
*/
|
||||
public static final String ACCOUNT_TYPE = "com.android.keychain";
|
||||
|
||||
/**
|
||||
* @hide Also used by KeyChainService implementation
|
||||
*/
|
||||
// TODO This non-localized CA string to be removed when CAs moved out of keystore
|
||||
public static final String CA_SUFFIX = " CA";
|
||||
|
||||
public static final String KEY_INTENT = "intent";
|
||||
|
||||
/**
|
||||
* Intentionally not public to leave open the future possibility
|
||||
* of hardware based keys. Callers should use {@link #toPrivateKey
|
||||
* toPrivateKey} in order to convert a bundle to a {@code
|
||||
* PrivateKey}
|
||||
*/
|
||||
private static final String KEY_PKCS8 = "pkcs8";
|
||||
|
||||
/**
|
||||
* Intentionally not public to leave open the future possibility
|
||||
* of hardware based certs. Callers should use {@link
|
||||
* #toCertificate toCertificate} in order to convert a bundle to a
|
||||
* {@code PrivateKey}
|
||||
*/
|
||||
private static final String KEY_X509 = "x509";
|
||||
|
||||
/**
|
||||
* Returns an {@code Intent} for use with {@link
|
||||
* android.app.Activity#startActivityForResult
|
||||
@@ -97,160 +68,81 @@ public final class KeyChain {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@code KeyChain} instance. When the caller is
|
||||
* done using the {@code KeyChain}, it must be closed with {@link
|
||||
* #close()} or resource leaks will occur.
|
||||
* Returns a new {@code KeyChainResult} instance.
|
||||
*/
|
||||
public static KeyChain getInstance(Context context) throws InterruptedException {
|
||||
return new KeyChain(context);
|
||||
}
|
||||
|
||||
private final AccountManager mAccountManager;
|
||||
|
||||
private final Object mServiceLock = new Object();
|
||||
private IKeyChainService mService;
|
||||
private boolean mIsBound;
|
||||
|
||||
private Account mAccount;
|
||||
|
||||
private ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
@Override public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
synchronized (mServiceLock) {
|
||||
mService = IKeyChainService.Stub.asInterface(service);
|
||||
mServiceLock.notifyAll();
|
||||
|
||||
// Account is created if necessary during binding of the IKeyChainService
|
||||
mAccount = mAccountManager.getAccountsByType(ACCOUNT_TYPE)[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onServiceDisconnected(ComponentName name) {
|
||||
synchronized (mServiceLock) {
|
||||
mService = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final CloseGuard mGuard = CloseGuard.get();
|
||||
|
||||
private KeyChain(Context context) throws InterruptedException {
|
||||
public static KeyChainResult get(Context context, String alias)
|
||||
throws InterruptedException, RemoteException {
|
||||
if (context == null) {
|
||||
throw new NullPointerException("context == null");
|
||||
}
|
||||
mContext = context;
|
||||
ensureNotOnMainThread();
|
||||
mAccountManager = AccountManager.get(mContext);
|
||||
mIsBound = mContext.bindService(new Intent(IKeyChainService.class.getName()),
|
||||
mServiceConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
if (!mIsBound) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
synchronized (mServiceLock) {
|
||||
// there is a race between binding on this thread and the
|
||||
// callback on the main thread. wait until binding is done
|
||||
// to be sure we have the mAccount initialized.
|
||||
if (mService == null) {
|
||||
mServiceLock.wait();
|
||||
}
|
||||
}
|
||||
mGuard.open("close");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Bundle} will contain {@link #KEY_INTENT} if user needs
|
||||
* to confirm application access to requested key. In the alias
|
||||
* does not exist or there is an error, null is
|
||||
* returned. Otherwise the {@code Bundle} contains information
|
||||
* representing the private key which can be interpreted with
|
||||
* {@link #toPrivateKey toPrivateKey}.
|
||||
*
|
||||
* non-null alias
|
||||
*/
|
||||
public Bundle getPrivate(String alias) {
|
||||
return get(alias, Credentials.USER_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
public Bundle getCertificate(String alias) {
|
||||
return get(alias, Credentials.USER_CERTIFICATE);
|
||||
}
|
||||
|
||||
public Bundle getCaCertificate(String alias) {
|
||||
return get(alias, Credentials.CA_CERTIFICATE);
|
||||
}
|
||||
|
||||
private Bundle get(String alias, String type) {
|
||||
if (alias == null) {
|
||||
throw new NullPointerException("alias == null");
|
||||
}
|
||||
ensureNotOnMainThread();
|
||||
|
||||
String authAlias = (type.equals(Credentials.CA_CERTIFICATE)) ? (alias + CA_SUFFIX) : alias;
|
||||
AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(mAccount,
|
||||
authAlias,
|
||||
false,
|
||||
null,
|
||||
null);
|
||||
Bundle bundle;
|
||||
try {
|
||||
bundle = future.getResult();
|
||||
} catch (OperationCanceledException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (AuthenticatorException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
|
||||
if (intent != null) {
|
||||
Bundle result = new Bundle();
|
||||
// we don't want this Eclair compatability flag,
|
||||
// it will prevent onActivityResult from being called
|
||||
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
result.putParcelable(KEY_INTENT, intent);
|
||||
return result;
|
||||
}
|
||||
String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
|
||||
if (authToken == null) {
|
||||
throw new AssertionError("Invalid authtoken");
|
||||
}
|
||||
|
||||
byte[] bytes;
|
||||
try {
|
||||
if (type.equals(Credentials.USER_PRIVATE_KEY)) {
|
||||
bytes = mService.getPrivate(alias, authToken);
|
||||
} else if (type.equals(Credentials.USER_CERTIFICATE)) {
|
||||
bytes = mService.getCertificate(alias, authToken);
|
||||
} else if (type.equals(Credentials.CA_CERTIFICATE)) {
|
||||
bytes = mService.getCaCertificate(alias, authToken);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
ensureNotOnMainThread(context);
|
||||
final BlockingQueue<IKeyChainService> q = new LinkedBlockingQueue<IKeyChainService>(1);
|
||||
ServiceConnection keyChainServiceConnection = new ServiceConnection() {
|
||||
@Override public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
try {
|
||||
q.put(IKeyChainService.Stub.asInterface(service));
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new AssertionError(e);
|
||||
@Override public void onServiceDisconnected(ComponentName name) {}
|
||||
};
|
||||
boolean isBound = context.bindService(new Intent(IKeyChainService.class.getName()),
|
||||
keyChainServiceConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
if (!isBound) {
|
||||
throw new AssertionError("could not bind to KeyChainService");
|
||||
}
|
||||
if (bytes == null) {
|
||||
throw new AssertionError();
|
||||
IKeyChainService keyChainService;
|
||||
try {
|
||||
keyChainService = q.take();
|
||||
|
||||
// Account is created if necessary during binding of the IKeyChainService
|
||||
AccountManager accountManager = AccountManager.get(context);
|
||||
Account account = accountManager.getAccountsByType(ACCOUNT_TYPE)[0];
|
||||
AccountManagerFuture<Bundle> future = accountManager.getAuthToken(account,
|
||||
alias,
|
||||
false,
|
||||
null,
|
||||
null);
|
||||
Bundle bundle;
|
||||
try {
|
||||
bundle = future.getResult();
|
||||
} catch (OperationCanceledException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (AuthenticatorException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
|
||||
if (intent != null) {
|
||||
Bundle result = new Bundle();
|
||||
// we don't want this Eclair compatability flag,
|
||||
// it will prevent onActivityResult from being called
|
||||
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
return new KeyChainResult(intent);
|
||||
}
|
||||
|
||||
String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
|
||||
if (authToken == null) {
|
||||
throw new AssertionError("Invalid authtoken");
|
||||
}
|
||||
byte[] privateKeyBytes = keyChainService.getPrivateKey(alias, authToken);
|
||||
byte[] certificateBytes = keyChainService.getCertificate(alias, authToken);
|
||||
return new KeyChainResult(toPrivateKey(privateKeyBytes),
|
||||
toCertificate(certificateBytes));
|
||||
} finally {
|
||||
context.unbindService(keyChainServiceConnection);
|
||||
}
|
||||
Bundle result = new Bundle();
|
||||
if (type.equals(Credentials.USER_PRIVATE_KEY)) {
|
||||
result.putByteArray(KEY_PKCS8, bytes);
|
||||
} else if (type.equals(Credentials.USER_CERTIFICATE)) {
|
||||
result.putByteArray(KEY_X509, bytes);
|
||||
} else if (type.equals(Credentials.CA_CERTIFICATE)) {
|
||||
result.putByteArray(KEY_X509, bytes);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static PrivateKey toPrivateKey(Bundle bundle) {
|
||||
byte[] bytes = bundle.getByteArray(KEY_PKCS8);
|
||||
private static PrivateKey toPrivateKey(byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
throw new IllegalArgumentException("not a private key bundle");
|
||||
throw new IllegalArgumentException("bytes == null");
|
||||
}
|
||||
try {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
@@ -262,20 +154,9 @@ public final class KeyChain {
|
||||
}
|
||||
}
|
||||
|
||||
public static Bundle fromPrivateKey(PrivateKey privateKey) {
|
||||
Bundle bundle = new Bundle();
|
||||
String format = privateKey.getFormat();
|
||||
if (!format.equals("PKCS#8")) {
|
||||
throw new IllegalArgumentException("Unsupported private key format " + format);
|
||||
}
|
||||
bundle.putByteArray(KEY_PKCS8, privateKey.getEncoded());
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public static X509Certificate toCertificate(Bundle bundle) {
|
||||
byte[] bytes = bundle.getByteArray(KEY_X509);
|
||||
private static X509Certificate toCertificate(byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
throw new IllegalArgumentException("not a certificate bundle");
|
||||
throw new IllegalArgumentException("bytes == null");
|
||||
}
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
@@ -286,87 +167,11 @@ public final class KeyChain {
|
||||
}
|
||||
}
|
||||
|
||||
public static Bundle fromCertificate(Certificate cert) {
|
||||
Bundle bundle = new Bundle();
|
||||
String type = cert.getType();
|
||||
if (!type.equals("X.509")) {
|
||||
throw new IllegalArgumentException("Unsupported certificate type " + type);
|
||||
}
|
||||
try {
|
||||
bundle.putByteArray(KEY_X509, cert.getEncoded());
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private void ensureNotOnMainThread() {
|
||||
private static void ensureNotOnMainThread(Context context) {
|
||||
Looper looper = Looper.myLooper();
|
||||
if (looper != null && looper == mContext.getMainLooper()) {
|
||||
if (looper != null && looper == context.getMainLooper()) {
|
||||
throw new IllegalStateException(
|
||||
"calling this from your main thread can lead to deadlock");
|
||||
}
|
||||
}
|
||||
|
||||
public Bundle findIssuer(X509Certificate cert) {
|
||||
if (cert == null) {
|
||||
throw new NullPointerException("cert == null");
|
||||
}
|
||||
ensureNotOnMainThread();
|
||||
|
||||
// check and see if the issuer is already known to the default IndexedPKIXParameters
|
||||
IndexedPKIXParameters index = SSLParametersImpl.getDefaultIndexedPKIXParameters();
|
||||
try {
|
||||
TrustAnchor anchor = index.findTrustAnchor(cert);
|
||||
if (anchor != null && anchor.getTrustedCert() != null) {
|
||||
X509Certificate ca = anchor.getTrustedCert();
|
||||
return fromCertificate(ca);
|
||||
}
|
||||
} catch (CertPathValidatorException ignored) {
|
||||
}
|
||||
|
||||
// otherwise, it might be a user installed CA in the keystore
|
||||
String alias;
|
||||
try {
|
||||
alias = mService.findIssuer(fromCertificate(cert));
|
||||
} catch (RemoteException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
if (alias == null) {
|
||||
Log.w(TAG, "Lookup failed for issuer");
|
||||
return null;
|
||||
}
|
||||
|
||||
Bundle bundle = get(alias, Credentials.CA_CERTIFICATE);
|
||||
Intent intent = bundle.getParcelable(KEY_INTENT);
|
||||
if (intent != null) {
|
||||
// permission still required
|
||||
return bundle;
|
||||
}
|
||||
// add the found CA to the index for next time
|
||||
X509Certificate ca = toCertificate(bundle);
|
||||
index.index(new TrustAnchor(ca, null));
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (mIsBound) {
|
||||
mContext.unbindService(mServiceConnection);
|
||||
mIsBound = false;
|
||||
mGuard.close();
|
||||
}
|
||||
}
|
||||
|
||||
protected void finalize() throws Throwable {
|
||||
// note we don't close, we just warn.
|
||||
// shouldn't be doing I/O in a finalizer,
|
||||
// which the unbind would cause.
|
||||
try {
|
||||
if (mGuard != null) {
|
||||
mGuard.warnIfOpen();
|
||||
}
|
||||
} finally {
|
||||
super.finalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
72
keystore/java/android/security/KeyChainResult.java
Normal file
72
keystore/java/android/security/KeyChainResult.java
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package android.security;
|
||||
|
||||
import android.content.Intent;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* The KeyChainResult is the complex result value from {@link
|
||||
* KeyChain#get}. The caller should first inspect {@link #getIntent}
|
||||
* to determine if the user needs to grant the application access to
|
||||
* the protected contents. If {@code getIntent} returns null, access
|
||||
* has been granted and the methods {@link #getPrivateKey} and {@link
|
||||
* #getCertificate} can be used to access the credentials.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public final class KeyChainResult {
|
||||
|
||||
private final Intent intent;
|
||||
private final PrivateKey privateKey;
|
||||
private final X509Certificate certificate;
|
||||
|
||||
KeyChainResult(Intent intent) {
|
||||
this(intent, null, null);
|
||||
}
|
||||
|
||||
KeyChainResult(PrivateKey privateKey, X509Certificate certificate) {
|
||||
this(null, privateKey, certificate);
|
||||
}
|
||||
|
||||
private KeyChainResult(Intent intent, PrivateKey privateKey, X509Certificate certificate) {
|
||||
this.intent = intent;
|
||||
this.privateKey = privateKey;
|
||||
this.certificate = certificate;
|
||||
}
|
||||
|
||||
public Intent getIntent() {
|
||||
return intent;
|
||||
}
|
||||
|
||||
public PrivateKey getPrivateKey() {
|
||||
checkIntent();
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public X509Certificate getCertificate() {
|
||||
checkIntent();
|
||||
return certificate;
|
||||
}
|
||||
|
||||
private void checkIntent() {
|
||||
if (intent != null) {
|
||||
throw new IllegalStateException("non-null Intent, check getIntent()");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user