From 6f14294164b94d25fa00038a5c2d544ddf195b4e Mon Sep 17 00:00:00 2001 From: Joe Steele Date: Thu, 21 Aug 2014 16:16:19 -0400 Subject: [PATCH 1/3] Remove SslHelper. Don't use SecureRandom. SslHelper has been removed, and its functionality has been transferred into TrustedSocketFactory. The added layer of indirection wasn't really simplifying anything. It's now easier to see what happens when createSocket() is invoked. A new instance of SecureRandom is no longer passed to SSLContext.init(). Instead, null is passed. The (default) provider of the TLS SSLContext used is OpenSSLProvider, which provides an SSLSocket instance of type OpenSSLSocketImpl. The only use of SecureRandom is in OpenSSLSocketImpl.startHandshake(), where it is used to seed the OpenSSL PRNG with additional random data. But if SecureRandom is null, then /dev/urandom is used for seeding instead. Meanwhile, the default provider for the SecureRandom service is OpenSSLRandom, which uses the OpenSSL PRNG as its data source. So we were effectively seeding the OpenSSL PRNG with itself. That's probably okay (we trust that the OpenSSL PRNG was properly initialized with random data before first use), but using /dev/urandom would seem like a better source (or at least as good a source) for the additional seed data added with each new connection. Note that our PRNGFixes class replaces the default SecureRandom service with one whose data source is /dev/urandom for certain vulnerable API levels anyway. (It also makes sure that the OpenSSL PRNG is properly seeded before first use for certain vulnerable API levels.) --- src/com/fsck/k9/mail/store/ImapStore.java | 8 +- src/com/fsck/k9/mail/store/Pop3Store.java | 6 +- .../fsck/k9/mail/transport/SmtpTransport.java | 6 +- src/com/fsck/k9/net/ssl/SslHelper.java | 81 ------------------- .../fsck/k9/net/ssl/TrustedSocketFactory.java | 44 +++++++--- 5 files changed, 42 insertions(+), 103 deletions(-) delete mode 100644 src/com/fsck/k9/net/ssl/SslHelper.java diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 5f9dd1079..f2c002e05 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -96,7 +96,7 @@ import com.fsck.k9.mail.store.ImapResponseParser.ImapList; import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; import com.fsck.k9.mail.store.imap.ImapUtility; import com.fsck.k9.mail.transport.imap.ImapSettings; -import com.fsck.k9.net.ssl.SslHelper; +import com.fsck.k9.net.ssl.TrustedSocketFactory; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; @@ -2435,7 +2435,7 @@ public class ImapStore extends Store { mSettings.getPort()); if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { - mSocket = SslHelper.createSslSocket(mSettings.getHost(), + mSocket = TrustedSocketFactory.createSocket(mSettings.getHost(), mSettings.getPort(), mSettings.getClientCertificateAlias()); } else { mSocket = new Socket(); @@ -2485,8 +2485,8 @@ public class ImapStore extends Store { // STARTTLS executeSimpleCommand("STARTTLS"); - mSocket = SslHelper.createStartTlsSocket(mSocket, - mSettings.getHost(), mSettings.getPort(), true, + mSocket = TrustedSocketFactory.createSocket(mSocket, + mSettings.getHost(), mSettings.getPort(), mSettings.getClientCertificateAlias()); mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); mIn = new PeekableInputStream(new BufferedInputStream(mSocket diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java index a66bca2ac..37c9b8efe 100644 --- a/src/com/fsck/k9/mail/store/Pop3Store.java +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -12,7 +12,7 @@ import com.fsck.k9.mail.*; import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.Hex; import com.fsck.k9.mail.internet.MimeMessage; -import com.fsck.k9.net.ssl.SslHelper; +import com.fsck.k9.net.ssl.TrustedSocketFactory; import javax.net.ssl.SSLException; @@ -314,7 +314,7 @@ public class Pop3Store extends Store { try { SocketAddress socketAddress = new InetSocketAddress(mHost, mPort); if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { - mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias); + mSocket = TrustedSocketFactory.createSocket(mHost, mPort, mClientCertificateAlias); } else { mSocket = new Socket(); } @@ -336,7 +336,7 @@ public class Pop3Store extends Store { if (mCapabilities.stls) { executeSimpleCommand(STLS_COMMAND); - mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true, + mSocket = TrustedSocketFactory.createSocket(mSocket, mHost, mPort, mClientCertificateAlias); mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index cedfaeaf5..cd8423c07 100644 --- a/src/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -15,7 +15,7 @@ import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.filter.SmtpDataStuffing; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.LocalStore.LocalMessage; -import com.fsck.k9.net.ssl.SslHelper; +import com.fsck.k9.net.ssl.TrustedSocketFactory; import javax.net.ssl.SSLException; @@ -224,7 +224,7 @@ public class SmtpTransport extends Transport { try { SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort); if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { - mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias); + mSocket = TrustedSocketFactory.createSocket(mHost, mPort, mClientCertificateAlias); mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); secureConnection = true; } else { @@ -278,7 +278,7 @@ public class SmtpTransport extends Transport { if (extensions.containsKey("STARTTLS")) { executeSimpleCommand("STARTTLS"); - mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true, + mSocket = TrustedSocketFactory.createSocket(mSocket, mHost, mPort, mClientCertificateAlias); mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), diff --git a/src/com/fsck/k9/net/ssl/SslHelper.java b/src/com/fsck/k9/net/ssl/SslHelper.java deleted file mode 100644 index 38e281bf0..000000000 --- a/src/com/fsck/k9/net/ssl/SslHelper.java +++ /dev/null @@ -1,81 +0,0 @@ - -package com.fsck.k9.net.ssl; - -import java.io.IOException; -import java.net.Socket; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; - -import android.util.Log; - -import com.fsck.k9.K9; -import com.fsck.k9.mail.MessagingException; - -/** - * Helper class to create SSL sockets with support for client certificate - * authentication - */ -public class SslHelper { - - private static SSLContext createSslContext(String host, int port, String clientCertificateAlias) - throws NoSuchAlgorithmException, KeyManagementException, MessagingException { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "createSslContext: Client certificate alias: " - + clientCertificateAlias); - - KeyManager[] keyManagers; - if (clientCertificateAlias == null || clientCertificateAlias.isEmpty()) { - keyManagers = null; - } else { - keyManagers = new KeyManager[] { new KeyChainKeyManager(K9.app, clientCertificateAlias) }; - } - - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(keyManagers, - new TrustManager[] { - TrustManagerFactory.get( - host, port) - }, - new SecureRandom()); - - return sslContext; - } - - /** - * Create SSL socket - * - * @param host - * @param port - * @param clientCertificateAlias if not null, uses client certificate - * retrieved by this alias for authentication - */ - public static Socket createSslSocket(String host, int port, String clientCertificateAlias) - throws NoSuchAlgorithmException, KeyManagementException, IOException, - MessagingException { - SSLContext sslContext = createSslContext(host, port, clientCertificateAlias); - return TrustedSocketFactory.createSocket(sslContext); - } - - /** - * Create socket for START_TLS. autoClose = true - * - * @param socket - * @param host - * @param port - * @param secure - * @param clientCertificateAlias if not null, uses client certificate - * retrieved by this alias for authentication - */ - public static Socket createStartTlsSocket(Socket socket, String host, int port, boolean secure, - String clientCertificateAlias) throws NoSuchAlgorithmException, - KeyManagementException, IOException, MessagingException { - SSLContext sslContext = createSslContext(host, port, clientCertificateAlias); - boolean autoClose = true; - return TrustedSocketFactory.createSocket(sslContext, socket, host, port, autoClose); - } -} diff --git a/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java b/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java index 030bf4910..6b7bad9a0 100644 --- a/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java +++ b/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java @@ -3,14 +3,21 @@ package com.fsck.k9.net.ssl; import android.util.Log; import com.fsck.k9.K9; +import com.fsck.k9.mail.MessagingException; +import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + import java.io.IOException; import java.net.Socket; -import java.security.SecureRandom; -import java.util.*; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** @@ -71,7 +78,7 @@ public class TrustedSocketFactory { try { SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, new SecureRandom()); + sslContext.init(null, null, null); SSLSocketFactory sf = sslContext.getSocketFactory(); SSLSocket sock = (SSLSocket) sf.createSocket(); enabledCiphers = sock.getEnabledCipherSuites(); @@ -114,19 +121,32 @@ public class TrustedSocketFactory { return result.toArray(new String[result.size()]); } - public static Socket createSocket(SSLContext sslContext) throws IOException { - SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket(); - hardenSocket(socket); + public static Socket createSocket(String host, int port, String clientCertificateAlias) + throws IOException, MessagingException, KeyManagementException, NoSuchAlgorithmException { - return socket; + return createSocket(null, host, port, clientCertificateAlias); } - public static Socket createSocket(SSLContext sslContext, Socket s, String host, int port, - boolean autoClose) throws IOException { - SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket(s, host, port, autoClose); - hardenSocket(socket); + public static Socket createSocket(Socket socket, String host, int port, String clientCertificateAlias) + throws NoSuchAlgorithmException, KeyManagementException, MessagingException, IOException { - return socket; + TrustManager[] trustManagers = new TrustManager[] { TrustManagerFactory.get(host, port) }; + KeyManager[] keyManagers = null; + if (clientCertificateAlias != null && !clientCertificateAlias.isEmpty()) { + keyManagers = new KeyManager[] { new KeyChainKeyManager(K9.app, clientCertificateAlias) }; + } + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(keyManagers, trustManagers, null); + SSLSocketFactory socketFactory = context.getSocketFactory(); + Socket trustedSocket; + if (socket == null) { + trustedSocket = socketFactory.createSocket(); + } else { + trustedSocket = socketFactory.createSocket(socket, host, port, true); + } + hardenSocket((SSLSocket) trustedSocket); + return trustedSocket; } private static void hardenSocket(SSLSocket sock) { From 7dfbd906c92f280e9cce47389a2280d2064df15c Mon Sep 17 00:00:00 2001 From: Joe Steele Date: Fri, 22 Aug 2014 10:02:45 -0400 Subject: [PATCH 2/3] Eliminate DomainNameChecker There's no need to maintain our own implementation when comparable classes already exist in the Android API. StrictHostnameVerifier is used instead. --- src/com/fsck/k9/helper/DomainNameChecker.java | 280 ------------------ .../fsck/k9/net/ssl/TrustManagerFactory.java | 31 +- 2 files changed, 14 insertions(+), 297 deletions(-) delete mode 100644 src/com/fsck/k9/helper/DomainNameChecker.java diff --git a/src/com/fsck/k9/helper/DomainNameChecker.java b/src/com/fsck/k9/helper/DomainNameChecker.java deleted file mode 100644 index 6731de648..000000000 --- a/src/com/fsck/k9/helper/DomainNameChecker.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (C) 2008 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 com.fsck.k9.helper; - -import android.net.http.SslCertificate; -import android.util.Log; -import com.fsck.k9.K9; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.security.cert.X509Certificate; -import java.security.cert.CertificateParsingException; -import java.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -/** - * Implements basic domain-name validation as specified by RFC2818. - */ -public class DomainNameChecker { - private static Pattern QUICK_IP_PATTERN; - static { - try { - QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$"); - } catch (PatternSyntaxException e) { - } - } - - private static final int ALT_DNS_NAME = 2; - private static final int ALT_IPA_NAME = 7; - - /** - * Checks the site certificate against the domain name of the site being - * visited - * - * @param certificate - * The certificate to check - * @param thisDomain - * The domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - public static boolean match(X509Certificate certificate, String thisDomain) { - if ((certificate == null) || (thisDomain == null) - || thisDomain.isEmpty()) { - return false; - } - - thisDomain = thisDomain.toLowerCase(Locale.US); - if (!isIpAddress(thisDomain)) { - return matchDns(certificate, thisDomain); - } else { - return matchIpAddress(certificate, thisDomain); - } - } - - /** - * @return True iff the domain name is specified as an IP address - */ - private static boolean isIpAddress(String domain) { - if ((domain == null) || domain.isEmpty()) { - return false; - } - - boolean rval; - try { - // do a quick-dirty IP match first to avoid DNS lookup - rval = QUICK_IP_PATTERN.matcher(domain).matches(); - if (rval) { - rval = domain.equals(InetAddress.getByName(domain) - .getHostAddress()); - } - } catch (UnknownHostException e) { - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = "unknown host exception"; - } - - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "DomainNameChecker.isIpAddress(): " - + errorMessage); - } - - rval = false; - } - - return rval; - } - - /** - * Checks the site certificate against the IP domain name of the site being - * visited - * - * @param certificate - * The certificate to check - * @param thisDomain - * The DNS domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) { - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "DomainNameChecker.matchIpAddress(): this domain: " + thisDomain); - } - - try { - Collection> subjectAltNames = certificate.getSubjectAlternativeNames(); - if (subjectAltNames != null) { - for (List altNameEntry : subjectAltNames) { - if ((altNameEntry != null) && (2 <= altNameEntry.size())) { - Integer altNameType = (Integer)(altNameEntry.get(0)); - if (altNameType != null && altNameType.intValue() == ALT_IPA_NAME) { - String altName = (String)(altNameEntry.get(1)); - if (altName != null) { - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "alternative IP: " + altName); - } - if (thisDomain.equalsIgnoreCase(altName)) { - return true; - } - } - } - } - } - } - } catch (CertificateParsingException e) { - } - - return false; - } - - /** - * Checks the site certificate against the DNS domain name of the site being - * visited - * - * @param certificate - * The certificate to check - * @param thisDomain - * The DNS domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - private static boolean matchDns(X509Certificate certificate, String thisDomain) { - boolean hasDns = false; - try { - Collection> subjectAltNames = certificate.getSubjectAlternativeNames(); - if (subjectAltNames != null) { - for (List altNameEntry : subjectAltNames) { - if ((altNameEntry != null) && (2 <= altNameEntry.size())) { - Integer altNameType = (Integer)(altNameEntry.get(0)); - if (altNameType != null && altNameType.intValue() == ALT_DNS_NAME) { - hasDns = true; - String altName = (String)(altNameEntry.get(1)); - if (altName != null && matchDns(thisDomain, altName)) { - return true; - } - } - } - } - } - } catch (CertificateParsingException e) { - // one way we can get here is if an alternative name starts with - // '*' character, which is contrary to one interpretation of the - // spec (a valid DNS name must start with a letter); there is no - // good way around this, and in order to be compatible we proceed - // to check the common name (ie, ignore alternative names) - if (K9.DEBUG) { - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = "failed to parse certificate"; - } - - Log.v(K9.LOG_TAG, "DomainNameChecker.matchDns(): " - + errorMessage); - } - } - - if (!hasDns) { - SslCertificate sslCertificate = new SslCertificate(certificate); - return matchDns(thisDomain, sslCertificate.getIssuedTo().getCName()); - } - - return false; - } - - /** - * @param thisDomain - * The domain name of the site being visited - * @param thatDomain - * The domain name from the certificate - * @return True iff thisDomain matches thatDomain as specified by RFC2818 - */ - private static boolean matchDns(String thisDomain, String thatDomain) { - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "DomainNameChecker.matchDns():" - + " this domain: " + thisDomain + " that domain: " - + thatDomain); - } - - if ((thisDomain == null) || thisDomain.isEmpty() - || (thatDomain == null) || thatDomain.isEmpty()) { - return false; - } - - thatDomain = thatDomain.toLowerCase(Locale.US); - - // (a) domain name strings are equal, ignoring case: X matches X - boolean rval = thisDomain.equals(thatDomain); - if (!rval) { - String[] thisDomainTokens = thisDomain.split("\\."); - String[] thatDomainTokens = thatDomain.split("\\."); - - int thisDomainTokensNum = thisDomainTokens.length; - int thatDomainTokensNum = thatDomainTokens.length; - - // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X - if (thisDomainTokensNum >= thatDomainTokensNum) { - for (int i = thatDomainTokensNum - 1; i >= 0; --i) { - rval = thisDomainTokens[i].equals(thatDomainTokens[i]); - if (!rval) { - // (c) OR we have a special *-match: - // Z.Y.X matches *.Y.X but does not match *.X - rval = ((i == 0) && (thisDomainTokensNum == thatDomainTokensNum)); - if (rval) { - rval = thatDomainTokens[0].equals("*"); - if (!rval) { - // (d) OR we have a *-component match: - // f*.com matches foo.com but not bar.com - rval = domainTokenMatch(thisDomainTokens[0], - thatDomainTokens[0]); - } - } - - break; - } - } - } - } - - return rval; - } - - /** - * @param thisDomainToken - * The domain token from the current domain name - * @param thatDomainToken - * The domain token from the certificate - * @return True iff thisDomainToken matches thatDomainToken, using the - * wildcard match as specified by RFC2818-3.1. For example, f*.com - * must match foo.com but not bar.com - */ - private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) { - if ((thisDomainToken != null) && (thatDomainToken != null)) { - int starIndex = thatDomainToken.indexOf('*'); - if (starIndex >= 0) { - if (thatDomainToken.length() - 1 <= thisDomainToken.length()) { - String prefix = thatDomainToken.substring(0, starIndex); - String suffix = thatDomainToken.substring(starIndex + 1); - - return thisDomainToken.startsWith(prefix) - && thisDomainToken.endsWith(suffix); - } - } - } - - return false; - } -} diff --git a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java b/src/com/fsck/k9/net/ssl/TrustManagerFactory.java index 27b2c70bb..4a2c7206a 100644 --- a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java +++ b/src/com/fsck/k9/net/ssl/TrustManagerFactory.java @@ -3,12 +3,15 @@ package com.fsck.k9.net.ssl; import android.util.Log; -import com.fsck.k9.helper.DomainNameChecker; import com.fsck.k9.mail.CertificateChainException; import com.fsck.k9.security.LocalKeyStore; +import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; + import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -58,31 +61,25 @@ public final class TrustManagerFactory { public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { String message = null; - boolean foundInGlobalKeyStore = false; + X509Certificate certificate = chain[0]; + try { defaultTrustManager.checkServerTrusted(chain, authType); - foundInGlobalKeyStore = true; + new StrictHostnameVerifier().verify(mHost, certificate); + return; } catch (CertificateException e) { + // cert. chain can't be validated + message = e.getMessage(); + } catch (SSLException e) { + // host name doesn't match certificate message = e.getMessage(); } - X509Certificate certificate = chain[0]; - // Check the local key store if we couldn't verify the certificate using the global // key store or if the host name doesn't match the certificate name - if (foundInGlobalKeyStore - && DomainNameChecker.match(certificate, mHost) - || keyStore.isValidCertificate(certificate, mHost, mPort)) { - return; + if (!keyStore.isValidCertificate(certificate, mHost, mPort)) { + throw new CertificateChainException(message, chain); } - - if (message == null) { - message = (foundInGlobalKeyStore) ? - "Certificate domain name does not match " + mHost : - "Couldn't find certificate in local key store"; - } - - throw new CertificateChainException(message, chain); } public X509Certificate[] getAcceptedIssuers() { From 43c38a047feedda4720af5bfbc188a33f8dfaced Mon Sep 17 00:00:00 2001 From: Joe Steele Date: Sat, 6 Sep 2014 16:35:48 -0400 Subject: [PATCH 3/3] Implement SSL file-based session caching Caching is beneficial because it can eliminate redundant cryptographic computations and network traffic when re-establishing a connection to the same server, thus saving time and conserving power. --- .../k9/net/ssl/SslSessionCacheHelper.java | 89 +++++++++++++++++++ .../fsck/k9/net/ssl/TrustedSocketFactory.java | 1 + 2 files changed, 90 insertions(+) create mode 100644 src/com/fsck/k9/net/ssl/SslSessionCacheHelper.java diff --git a/src/com/fsck/k9/net/ssl/SslSessionCacheHelper.java b/src/com/fsck/k9/net/ssl/SslSessionCacheHelper.java new file mode 100644 index 000000000..20439c4ff --- /dev/null +++ b/src/com/fsck/k9/net/ssl/SslSessionCacheHelper.java @@ -0,0 +1,89 @@ +package com.fsck.k9.net.ssl; + +import java.io.File; +import java.lang.reflect.Method; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSessionContext; + +import com.fsck.k9.K9; + +import android.content.Context; +import android.os.Build; +import android.util.Log; + +/** + * A class to help with associating an {@code SSLContext} with a persistent + * file-based cache of SSL sessions. + *

+ * This uses reflection to achieve its task. + *

+ * The alternative to this would be to use {@link SSLCertificateSocketFactory} + * which also provides session caching. The problem with using that occurs when + * using STARTTLS in combination with + * {@code TrustedSocketFactory.hardenSocket(SSLSocket)}. The result is that + * {@code hardenSocket()} fails to change anything because by the time it is + * applied to the socket, the SSL handshake has already been completed. (This is + * because of another feature of {@link SSLCertificateSocketFactory} whereby it + * performs host name verification which necessitates initiating the SSL + * handshake immediately on socket creation.) + *

+ * If eventually the use of hardenSocket() should become unnecessary, then + * switching to using {@link SSLCertificateSocketFactory} would be a better + * solution. + */ +public class SslSessionCacheHelper { + private static Object sSessionCache; + private static Method sSetPersistentCacheMethod; + private static boolean sIsDisabled = false; + + static { + final String packageName; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + packageName = "org.apache.harmony.xnet.provider.jsse"; + } else { + packageName = "com.android.org.conscrypt"; + } + final File cacheDirectory = K9.app.getDir("sslcache", Context.MODE_PRIVATE); + try { + Class fileClientSessionCacheClass = Class.forName(packageName + + ".FileClientSessionCache"); + Method usingDirectoryMethod = fileClientSessionCacheClass + .getMethod("usingDirectory", File.class); + sSessionCache = usingDirectoryMethod.invoke(null, cacheDirectory); + + Class sslClientSessionCacheClass = Class.forName(packageName + + ".SSLClientSessionCache"); + Class clientSessionContextClass = Class.forName(packageName + + ".ClientSessionContext"); + sSetPersistentCacheMethod = clientSessionContextClass.getMethod( + "setPersistentCache", sslClientSessionCacheClass); + } catch (Exception e) { + // Something went wrong. Proceed without a session cache. + Log.e(K9.LOG_TAG, "Failed to initialize SslSessionCacheHelper: " + e); + sIsDisabled = true; + } + } + + /** + * Associate an {@code SSLContext} with a persistent file-based cache of SSL + * sessions which can be used when re-establishing a connection to the same + * server. + *

+ * This is beneficial because it can eliminate redundant cryptographic + * computations and network traffic, thus saving time and conserving power. + */ + public static void setPersistentCache(SSLContext sslContext) { + if (sIsDisabled) { + return; + } + try { + SSLSessionContext sessionContext = sslContext.getClientSessionContext(); + sSetPersistentCacheMethod.invoke(sessionContext, sSessionCache); + } catch (Exception e) { + // Something went wrong. Proceed without a session cache. + Log.e(K9.LOG_TAG, "Failed to initialize persistent SSL cache: " + e); + sIsDisabled = true; + } + } +} diff --git a/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java b/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java index 6b7bad9a0..7d2438cda 100644 --- a/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java +++ b/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java @@ -138,6 +138,7 @@ public class TrustedSocketFactory { SSLContext context = SSLContext.getInstance("TLS"); context.init(keyManagers, trustManagers, null); + SslSessionCacheHelper.setPersistentCache(context); SSLSocketFactory socketFactory = context.getSocketFactory(); Socket trustedSocket; if (socket == null) {