k-9/src/com/fsck/k9/security/KeyChainKeyManager.java

186 lines
6.2 KiB
Java
Raw Normal View History

2014-05-25 16:45:14 -04:00
package com.fsck.k9.security;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509ExtendedKeyManager;
import android.app.Activity;
import android.os.Build;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.ClientCertificateRequiredException;
/**
* For client certificate authentication! Provide private keys and certificates
* during the TLS handshake using the Android 4.0 KeyChain API. If interactive
* selection is requested, we harvest the parameters during the handshake and
* abort with a custom (runtime) ClientCertificateRequiredException.
*/
public class KeyChainKeyManager extends X509ExtendedKeyManager {
private static PrivateKey sClientCertificateReferenceWorkaround;
2014-05-25 16:45:14 -04:00
private String mAlias;
public KeyChainKeyManager() {
mAlias = null;
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager set to interactive prompting required");
}
public KeyChainKeyManager(String alias) {
if (alias == null || "".equals(alias)) {
throw new IllegalArgumentException(
"KeyChainKeyManager: The provided alias is null or empty!");
}
mAlias = alias;
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager set up with for auto-selected alias " + alias);
}
@Override
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
if (mAlias == null) {
throw new ClientCertificateRequiredException(keyTypes, issuers,
socket.getInetAddress().getHostName(), socket.getPort());
}
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager.chooseClientAlias returning preselected alias "
+ mAlias);
return mAlias;
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
try {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager.getCertificateChain for " + alias);
X509Certificate[] chain = KeyChain.getCertificateChain(K9.app, alias);
if (chain == null || chain.length == 0) {
throw new IllegalStateException("No certificate chain found for: " + alias);
}
return chain;
} catch (KeyChainException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
@Override
public PrivateKey getPrivateKey(String alias) {
try {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager.getPrivateKey for " + alias);
PrivateKey key;
2014-05-25 16:45:14 -04:00
/*
* We need to keep reference to the first private key retrieved so
* it won't get garbage collected. If it will then the whole app
* will crash on Android < 4.2 with "Fatal signal 11 code=1". See
2014-05-25 16:45:14 -04:00
* https://code.google.com/p/android/issues/detail?id=62319
*/
if (sClientCertificateReferenceWorkaround == null
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
key = retrieveFirstPrivateKey(alias);
} else {
key = KeyChain.getPrivateKey(K9.app, alias);
}
2014-05-25 16:45:14 -04:00
if (key == null) {
throw new IllegalStateException("No private key found for: " + alias);
}
2014-05-25 16:45:14 -04:00
return key;
} catch (KeyChainException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
private synchronized PrivateKey retrieveFirstPrivateKey(String alias)
throws KeyChainException, InterruptedException {
PrivateKey key = KeyChain.getPrivateKey(K9.app, alias);
if (sClientCertificateReferenceWorkaround == null) {
sClientCertificateReferenceWorkaround = key;
}
return key;
}
2014-05-25 16:45:14 -04:00
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
// not valid for client side
throw new UnsupportedOperationException();
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
// not valid for client side
throw new UnsupportedOperationException();
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
// not valid for client side
throw new UnsupportedOperationException();
}
public static String interactivelyChooseClientCertificateAlias(Activity activity,
String[] keyTypes, Principal[] issuers, String hostName, int port,
String preSelectedAlias) {
// defined as array to be able to set it inside the callback
final String[] selectedAlias = new String[1];
KeyChain.choosePrivateKeyAlias(activity, new KeyChainAliasCallback() {
@Override
public void alias(String alias) {
synchronized (selectedAlias) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "User has selected client certificate alias:" + alias);
// see below. not null is condition for breaking out of loop
if (alias == null) {
alias = "";
}
selectedAlias[0] = alias;
selectedAlias.notifyAll();
}
}
}, keyTypes, issuers, hostName, port, preSelectedAlias);
synchronized (selectedAlias) {
while (selectedAlias[0] == null) {
try {
selectedAlias.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
if ("".equals(selectedAlias[0])) {
selectedAlias[0] = null;
}
}
return selectedAlias[0];
}
}