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 {
|
|
|
|
|
2014-07-14 17:59:07 -04:00
|
|
|
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);
|
|
|
|
|
2014-07-14 17:59:07 -04:00
|
|
|
PrivateKey key;
|
2014-05-25 16:45:14 -04:00
|
|
|
|
|
|
|
/*
|
2014-07-14 17:59:07 -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
|
|
|
|
*/
|
2014-07-14 17:59:07 -04:00
|
|
|
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
|
|
|
|
2014-07-14 17:59:07 -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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-14 17:59:07 -04:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|