When I use a certificate, my IMAP setup doesn't need LOGIN and in fact
returns BAD. So I have changed the IMAP login system to accept that we
are authenticated if the server does. Please check that as I'm sure
there must be a prettier way (perhaps only activate if LOGIN returns
BAD: Unknown command).
The main reason this patch is preliminary is that I store the keys and
certificates in with the source code. I appreciate that this will not
work for everybody. I only got my Android phone yesterday and started
hacking on it afterwards, I haven't sorted out how to read in keys and
certificates via the preferences.
The keys and certificates are blanked out with xxxs in the patch. Please
review.
The main idea is to create all SSL connexions via the
TrustManagerFactory, which will do the boilerplate to connect with the
keystore.
I haven't converted POP3 as I don't use it.
I notice with horror that emacs has gone and munged the indentation. Any
help with the correct settings much appreciated.
Thanks for the preliminary patch. It definitely looks like a great
start. Don't worry too much about indent style. I beat the source with
astyle periodically ;)
You are, of course, correct that we can't really hardcode client certs
into the .apk. A cursory glance over the SDK documentation and related
forums shows me only other developers bewildered by how to make use of
android's "Secure Credentials Storage". :/
> --
> You received this message because you are subscribed to the Google Groups "K-9 Mail" group.
> To post to this group, send email to k-9-...@googlegroups.com.
> To unsubscribe from this group, send email to k-9-mail+u...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/k-9-mail?hl=en.
>
>
Jesse Vincent <jes...@gmail.com> writes:
[...]
> You are, of course, correct that we can't really hardcode client certs
> into the .apk. A cursory glance over the SDK documentation and related
> forums shows me only other developers bewildered by how to make use of
> android's "Secure Credentials Storage". :/
I don't know what that is :-)
As my patch shows it's easy enough to store keys in an application
specific keystore. You could read the key data from a file on the
SD-card, or take them from an email attachment from another account --
whatever.
I guess the question is how to integrate it into the preferences UI. I'd
like some help with that.
Maybe people would like to protect the keystore with a password, and
we should add a password popup for that.
When the key is needed I'm not sure how best to pop up a keystore
password request -- perhaps by creating a new X509KeyManager wrapper
class? On my old Symbian phone, it would pop up a keystore password
request then let you choose the certificate when the server end requests
one. This seems quite sensible.
In the patch I sent there is only one global keystore; it might make
more sense to have a keystore per account.
To be clear, the reason I embedded the keys and certificates in the
source is that it was easier for me to quickly get it working and the
stumbling block to tidying it up is implementing the UI for key
selection. IIUC once a key has been added to the keystore once, it can
be saved there permanently (of course we needn't save the keystore).
There was no file-selection example in the current preferences UI so I
didn't have anything to copy, and it would have been a bit more annoying
to test as I didn't know transfer files to my device -- now I do (the
first file I transferred was my hacked K9 binary ;-).
From the preferences UI, if we had certificate and key selection screen,
it would be easy to add the file data to the keystore. I'd be happy to
adapt the patch to do that given some help with the UI.
[...]
It adds client certificates and means that if you don't need a password
to login, you don't have to.
While the fact that the certificates are inline is rather rough, there
are bits that it would be nice to have applied.
Please consider!
I can't really see applying this without some way to use a keystore
rather than compiling your certificate into the binary. Can you use
Android APIs to use the platform's keystore?
> I can't really see applying this without some way to use a keystore
> rather than compiling your certificate into the binary. Can you use
> Android APIs to use the platform's keystore?
Sorry, I didn't check this thread.
Thanks for replying. I actually updated the patch again for my new
phone a month or so ago.
This time I put the key and certificates in as resources the project.
R.raw.key has the key (base64 encoded PKCS8)
R.raw.cert has the client certificate (public key, PEM)
R.raw.ca has the certificate authority certificate to verify the server
(public key, PEM)
This is less ideal than having a keystore file and so on. As K9 already
has a keystore, we could use that -- I'm not sure how to use the android
keystore.
The other part of the patch allows an IMAP connection to be used if
LOGIN is unnecessary.
Is there any chance of getting onto an inclusion track for these changes
as it's a little irritating to keep maintaining my fork? I'll try to
keep track of emails, please CC me!
Index: src/com/fsck/k9/mail/store/ImapStore.java
===================================================================
--- src/com/fsck/k9/mail/store/ImapStore.java (revision 3275)
+++ src/com/fsck/k9/mail/store/ImapStore.java (working copy)
@@ -92,6 +92,8 @@
import com.jcraft.jzlib.ZInputStream;
import com.jcraft.jzlib.ZOutputStream;
+import org.apache.commons.codec.binary.Base64;
+
/**
* <pre>
* TODO Need to start keeping track of UIDVALIDITY
@@ -2329,13 +2331,7 @@
if (mSettings.getConnectionSecurity() == CONNECTION_SECURITY_SSL_REQUIRED ||
mSettings.getConnectionSecurity() == CONNECTION_SECURITY_SSL_OPTIONAL)
{
- SSLContext sslContext = SSLContext.getInstance("TLS");
- final boolean secure = mSettings.getConnectionSecurity() == CONNECTION_SECURITY_SSL_REQUIRED;
- sslContext.init(null, new TrustManager[]
- {
- TrustManagerFactory.get(mSettings.getHost(), secure)
- }, new SecureRandom());
- mSocket = sslContext.getSocketFactory().createSocket();
+ mSocket = TrustManagerFactory.getSocketFactory(mSettings.getHost(), mSettings.getConnectionSecurity() == CONNECTION_SECURITY_SSL_REQUIRED).createSocket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
}
else
@@ -2382,11 +2378,7 @@
SSLContext sslContext = SSLContext.getInstance("TLS");
boolean secure = mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED;
- sslContext.init(null, new TrustManager[]
- {
- TrustManagerFactory.get(mSettings.getHost(), secure)
- }, new SecureRandom());
- mSocket = sslContext.getSocketFactory().createSocket(mSocket, mSettings.getHost(), mSettings.getPort(),
+ mSocket = TrustManagerFactory.getSocketFactory(mSettings.getHost(),secure).createSocket(mSocket, mSettings.getHost(), mSettings.getPort(),
true);
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket
@@ -2426,11 +2418,23 @@
}
}
- else if (mSettings.getAuthType() == AuthType.PLAIN)
- {
- receiveCapabilities(executeSimpleCommand(String.format("LOGIN %s %s", ImapStore.encodeString(mSettings.getUsername()), ImapStore.encodeString(mSettings.getPassword())), true));
- }
- authSuccess = true;
+ else if (mSettings.getAuthType() == AuthType.PLAIN)
+ {
+ try {
+ receiveCapabilities(executeSimpleCommand("LOGIN \"" + encodeString(mSettings.getUsername()) + "\" \"" + encodeString(mSettings.getPassword()) + "\"", true));
+ }catch(ImapException ie){
+ try {
+ if (K9.DEBUG_SENSITIVE)
+ Log.d(K9.LOG_TAG, "Received error from LOGIN, maybe already logged in? ",ie);
+ executeSimpleCommand("STATUS INBOX ()");
+ }catch(ImapException nie){
+ if (K9.DEBUG_SENSITIVE)
+ Log.d(K9.LOG_TAG, "Not logged in ",nie);
+ throw ie;
+ }
+ }
+ }
+ authSuccess = true;
}
catch (ImapException ie)
{
@@ -2561,6 +2565,7 @@
}
catch (SSLException e)
{
+ Log.e(K9.LOG_TAG, "SSL error while opening connection.", e);
throw new CertificateValidationException(e.getMessage(), e);
}
catch (GeneralSecurityException gse)
Index: src/com/fsck/k9/mail/store/TrustManagerFactory.java
===================================================================
--- src/com/fsck/k9/mail/store/TrustManagerFactory.java (revision 3275)
+++ src/com/fsck/k9/mail/store/TrustManagerFactory.java (working copy)
@@ -6,12 +6,14 @@
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.helper.DomainNameChecker;
+import com.fsck.k9.R;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -19,6 +21,22 @@
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
+import java.security.KeyStore;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import java.security.cert.Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.cert.CertificateFactory;
+import java.security.KeyFactory;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.io.ByteArrayInputStream;
+import java.security.Key;
+import org.apache.commons.codec.binary.Base64;
+import java.security.KeyStore.TrustedCertificateEntry;
public final class TrustManagerFactory
{
@@ -83,6 +101,7 @@
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
+ if(K9.DEBUG)return;
defaultTrustManager.checkClientTrusted(chain, authType);
}
@@ -90,6 +109,7 @@
throws CertificateException
{
TrustManagerFactory.setLastCertChain(chain);
+ if(K9.DEBUG)return;
try
{
defaultTrustManager.checkServerTrusted(chain, authType);
@@ -124,15 +144,33 @@
}
- static
+ static String resourceToString(int id)
{
- try
- {
- javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
- Application app = K9.app;
- keyStoreFile = new File(app.getDir("KeyStore", Context.MODE_PRIVATE) + File.separator + "KeyStore.bks");
- keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ InputStream i = K9.app.getResources().openRawResource(id);
+ byte[] buf = null;
+ try {
+ buf = new byte[i.available()];
+ i.read(buf);
+ i.close();
+ } catch(IOException e) {
+ if (K9.DEBUG)
+ Log.v(K9.LOG_TAG,"error retrieving resource " + id + ": " + e.toString());
+ }
+ return new String(buf);
+ }
+
+ static void setupKeyStore()
+ {
+ Application app = K9.app;
+ String key = resourceToString(R.raw.key);
+ String cert = resourceToString(R.raw.cert);
+ String ca = resourceToString(R.raw.ca);
+ try{
+
+ keyStoreFile = new File(app.getDir("KeyStore", Context.MODE_PRIVATE) + File.separator + "KeyStore.");
+ keyStore = KeyStore.getInstance("BKS");
java.io.FileInputStream fis;
+
try
{
fis = new java.io.FileInputStream(keyStoreFile);
@@ -158,6 +196,64 @@
Log.e(LOG_TAG, "KeyStore CertificateException while initializing TrustManagerFactory ", e);
keyStore = null;
}
+
+ Key privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(key.getBytes())));
+ CertificateFactory certFact = CertificateFactory.getInstance("X509");
+
+ keyStore.setEntry("CA",new TrustedCertificateEntry(certFact.generateCertificate(new ByteArrayInputStream(ca.getBytes()))),null);
+ assert(keyStore.isCertificateEntry("CA"));
+ keyStore.setKeyEntry("private", privateKey, "".toCharArray(),
+ new Certificate[] {
+ certFact.generateCertificate(new ByteArrayInputStream(cert.getBytes()))
+ });
+ assert(null != keyStore.getKey("private","".toCharArray()));
+ assert(null != keyStore.getCertificateChain("private"));
+
+ }catch(GeneralSecurityException e){
+ Log.e(LOG_TAG, "failed to initialize keyStore ", e);
+ }
+ }
+
+ public static SSLSocketFactory getSocketFactory(String host,boolean check_host_certificate) throws GeneralSecurityException {
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ KeyStore keyStore = TrustManagerFactory.getKeyStore();
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+ kmf.init(keyStore, "".toCharArray());
+ if (K9.DEBUG)
+ Log.v(K9.LOG_TAG, "created KeyManagerFactory with " + KeyManagerFactory.getDefaultAlgorithm() + " for keystore type " + keyStore.getType());
+
+ KeyManager[] keyManagers = kmf.getKeyManagers();
+ if (K9.DEBUG){
+ Log.v(K9.LOG_TAG,"KeyManagers " + keyManagers);
+ }
+
+ for(KeyManager km : keyManagers){
+ if (K9.DEBUG){
+ if (km instanceof X509KeyManager) {
+ Log.v(K9.LOG_TAG, "Key Manager has RSA aliases " + ((X509KeyManager)km).getClientAliases("RSA",null));
+ }
+ Log.v(K9.LOG_TAG,"NOT X509KEYMANAGER " + km + " class " + km.getClass());
+ }
+ }
+
+ sslContext.init(keyManagers, new TrustManager[]
+ {
+ get(host, check_host_certificate)
+ }, new SecureRandom());
+ return sslContext.getSocketFactory();
+ }
+
+
+
+ static
+ {
+ try
+ {
+ javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
+
+ setupKeyStore();
+
tmf.init(keyStore);
TrustManager[] tms = tmf.getTrustManagers();
if (tms != null)
@@ -195,6 +291,7 @@
{
Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
}
+
unsecureTrustManager = new SimpleX509TrustManager();
}
Index: src/com/fsck/k9/mail/transport/SmtpTransport.java
===================================================================
--- src/com/fsck/k9/mail/transport/SmtpTransport.java (revision 3275)
+++ src/com/fsck/k9/mail/transport/SmtpTransport.java (working copy)
@@ -13,7 +13,6 @@
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.TrustManagerFactory;
-import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import java.io.BufferedInputStream;
@@ -25,7 +24,6 @@
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
import org.apache.commons.codec.binary.Hex;
import java.util.ArrayList;
import java.util.Arrays;
@@ -161,13 +159,8 @@
if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL)
{
- SSLContext sslContext = SSLContext.getInstance("TLS");
boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
- sslContext.init(null, new TrustManager[]
- {
- TrustManagerFactory.get(mHost, secure)
- }, new SecureRandom());
- mSocket = sslContext.getSocketFactory().createSocket();
+ mSocket = TrustManagerFactory.getSocketFactory(mHost, secure).createSocket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
mSecure = true;
}
@@ -243,13 +236,8 @@
{
executeSimpleCommand("STARTTLS");
- SSLContext sslContext = SSLContext.getInstance("TLS");
- boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED;
- sslContext.init(null, new TrustManager[]
- {
- TrustManagerFactory.get(mHost, secure)
- }, new SecureRandom());
- mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort,
+ boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
+ mSocket = TrustManagerFactory.getSocketFactory(mHost, secure).createSocket(mSocket, mHost, mPort,
true);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
1024));
On Wed 2.Mar'11 at 21:41:38 +0000, John Fremlin wrote:
> Jesse Vincent <jes...@gmail.com> writes:
>
> Sorry, I didn't check this thread.
>
> Thanks for replying. I actually updated the patch again for my new
> phone a month or so ago.
>
> This time I put the key and certificates in as resources the project.
> R.raw.key has the key (base64 encoded PKCS8)
> R.raw.cert has the client certificate (public key, PEM)
> R.raw.ca has the certificate authority certificate to verify the server
> (public key, PEM)
>
> This is less ideal than having a keystore file and so on. As K9 already
> has a keystore, we could use that -- I'm not sure how to use the android
> keystore.
>
> The other part of the patch allows an IMAP connection to be used if
> LOGIN is unnecessary.
>
> Is there any chance of getting onto an inclusion track for these changes
> as it's a little irritating to keep maintaining my fork? I'll try to
> keep track of emails, please CC me!
If you're up for building support for users to manage their keys through
K-9's interface rather than as resources, I'd love to have it in core.
Best,
Jesse
I guess the GUI elements needed are little boxes to load in a client
certificate and a key from files. These boxes should be related on a per
connection basis. It would be nice to have a way to load in a
certificate authority to authenticate the server too, but that's not
necessary.
I had a look at adding these GUI elements but there seems to be all
sorts of weird reduplication necessary to eventually shove them into
Java getProperty() methods.
I'd be happy to patch up the path stuff to deal with the GUI if someone
could sort that end out . . .
As this seems to be unlikely to happen, could we come to some sort of
compromise where the parts of the patch that uniformly factor out the
cryptographic handling into helper routines, and handle the case where
no LOGIN is required on IMAP, are merged, but the use of built-in
certificates is disabled?
As this seems to be unlikely to happen, could we come to some sort of
compromise where the parts of the patch that uniformly factor out the
cryptographic handling into helper routines, and handle the case where
no LOGIN is required on IMAP, are merged, but the use of built-in
certificates is disabled?
The whole patch for adding the unified crypto handling with certificates
is very small and I'm not sure that splitting it up is sensible, but to
show willingness, here is the simplest part, a way of dealing with
servers that don't need LOGIN at all.
diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java
index 9ecc582..10caa83 100644
--- a/src/com/fsck/k9/mail/store/ImapStore.java
+++ b/src/com/fsck/k9/mail/store/ImapStore.java
@@ -1958,8 +1958,20 @@ public class ImapStore extends Store {
}
} else if (mSettings.getAuthType() == AuthType.PLAIN) {
- receiveCapabilities(executeSimpleCommand(String.format("LOGIN %s %s", ImapStore.encodeString(mSettin
- }
+ try {
+ receiveCapabilities(executeSimpleCommand("LOGIN \"" + encodeString(mSettings.getUsername())
+ }catch(ImapException ie){
+ try {
+ if (K9.DEBUG_SENSITIVE)
+ Log.d(K9.LOG_TAG, "Received error from LOGIN, maybe already logged in? ",ie);
+ executeSimpleCommand("STATUS INBOX ()");
+ }catch(ImapException nie){
+ if (K9.DEBUG_SENSITIVE)
+ Log.d(K9.LOG_TAG, "Not logged in ",nie);
+ throw ie;
+ }
+ }
+ }
authSuccess = true;
} catch (ImapException ie) {
throw new AuthenticationFailedException(ie.getAlertText(), ie);