diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index 13c7d14b9..ffc7f984a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -209,7 +209,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { /** * */ - private IOException connectionException; + private Exception connectionException; /** * Flag that indicates if the user is currently authenticated with the server. @@ -435,13 +435,19 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return userJID; } - protected void setConnectionException(IOException exception) { - connectionException = exception; + protected void setConnectionException(Exception e) { + connectionException = e; } - protected void throwConnectionExceptionOrNoResponse() throws IOException, NoResponseException { + protected void throwConnectionExceptionOrNoResponse() throws IOException, NoResponseException, SmackException { if (connectionException != null) { - throw connectionException; + if (connectionException instanceof IOException) { + throw (IOException) connectionException; + } else if (connectionException instanceof SmackException) { + throw (SmackException) connectionException; + } else { + throw new SmackException(connectionException); + } } else { throw new NoResponseException(); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java index b2ea581c2..51f18b8f4 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java @@ -80,6 +80,16 @@ public class ConnectionConfiguration implements Cloneable { private boolean useDnsSrvRr = true; private SecurityMode securityMode = SecurityMode.enabled; + /** + * + */ + private String[] enabledSSLProtocols; + + /** + * + */ + private String[] enabledSSLCiphers; + /** * Permanent store for the Roster, needed for roster versioning */ @@ -310,6 +320,42 @@ public class ConnectionConfiguration implements Cloneable { this.customSSLContext = context; } + /** + * Set the enabled SSL/TLS protocols. + * + * @param enabledSSLProtocols + */ + public void setEnabledSSLProtocols(String[] enabledSSLProtocols) { + this.enabledSSLProtocols = enabledSSLProtocols; + } + + /** + * Return the enabled SSL/TLS protocols. + * + * @return the enabled SSL/TLS protocols + */ + public String[] getEnabledSSLProtocols() { + return enabledSSLProtocols; + } + + /** + * Set the enabled SSL/TLS ciphers. + * + * @param enabledSSLCiphers the enabled SSL/TLS ciphers + */ + public void setEnabledSSLCiphers(String[] enabledSSLCiphers) { + this.enabledSSLCiphers = enabledSSLCiphers; + } + + /** + * Return the enabled SSL/TLS ciphers. + * + * @return the enabled SSL/TLS ciphers + */ + public String[] getEnabledSSLCiphers() { + return enabledSSLCiphers; + } + /** * Returns true if the connection is going to use stream compression. Stream compression * will be requested after TLS was established (if TLS was enabled) and only if the server diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java index 33f89e927..756ee3b52 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java @@ -122,6 +122,18 @@ public class SmackException extends Exception { } } + public static class SecurityNotPossibleException extends SmackException { + + /** + * + */ + private static final long serialVersionUID = -6836090872690331336L; + + public SecurityNotPossibleException(String message) { + super(message); + } + } + public static class ConnectionException extends SmackException { /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java index 132e1d340..58f2069bd 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java @@ -20,6 +20,7 @@ package org.jivesoftware.smack.util; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Collection; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; @@ -536,4 +537,16 @@ public class StringUtils { public static boolean isNullOrEmpty(CharSequence cs) { return cs == null || cs.length() == 0; } + + public static String collectionToString(Collection collection) { + StringBuilder sb = new StringBuilder(); + for (String s : collection) { + sb.append(s); + sb.append(" "); + } + String res = sb.toString(); + // Remove the trailing whitespace + res = res.substring(0, res.length() - 1); + return res; + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java new file mode 100644 index 000000000..fe48ea03a --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java @@ -0,0 +1,156 @@ +/** + * + * Copyright 2014 Florian Schmaus + * + * 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 org.jivesoftware.smack.util; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.SmackException.SecurityNotPossibleException; + + +public class TLSUtils { + + public static final String SSL = "SSL"; + public static final String TLS = "TLS"; + public static final String PROTO_SSL3 = SSL + "v3"; + public static final String PROTO_TLSV1 = TLS + "v1"; + public static final String PROTO_TLSV1_1 = TLS + "v1.1"; + public static final String PROTO_TLSV1_2 = TLS + "v1.2"; + + /** + * Enable only TLS. Connections created with the given ConnectionConfiguration will only support TLS. + *

+ * According to the Encrypted + * XMPP Manifesto, TLSv1.2 shall be deployed, providing fallback support for SSLv3 and + * TLSv1.1. This method goes one step boyond and upgrades the handshake to use TLSv1 or better. + * This method requires the underlying OS to support all of TLSv1.2 , 1.1 and 1.0. + *

+ * + * @param conf the configuration to apply this setting to + */ + public static void setTLSOnly(ConnectionConfiguration conf) { + conf.setEnabledSSLProtocols(new String[] { PROTO_TLSV1_2, PROTO_TLSV1_1, PROTO_TLSV1 }); + } + + /** + * Enable only TLS and SSLv3. Connections created with the given ConnectionConfiguration will + * only support TLS and SSLv3. + *

+ * According to the Encrypted + * XMPP Manifesto, TLSv1.2 shall be deployed, providing fallback support for SSLv3 and + * TLSv1.1. + *

+ * + * @param conf the configuration to apply this setting to + */ + public static void setSSLv3AndTLSOnly(ConnectionConfiguration conf) { + conf.setEnabledSSLProtocols(new String[] { PROTO_TLSV1_2, PROTO_TLSV1_1, PROTO_TLSV1, PROTO_SSL3 }); + } + + /** + * Accept all SSL/TLS certificates. + *

+ * Warning Use with care. Only use this method if you understand the implications. + *

+ * + * @param conf + * @throws NoSuchAlgorithmException + * @throws KeyManagementException + */ + public static void acceptAllCertificates(ConnectionConfiguration conf) throws NoSuchAlgorithmException, KeyManagementException { + SSLContext context = SSLContext.getInstance(TLS); + context.init(null, new TrustManager[] { new AcceptAllTrustManager() }, new SecureRandom()); + conf.setCustomSSLContext(context); + } + + public static void setEnabledProtocolsAndCiphers(final SSLSocket sslSocket, + String[] enabledProtocols, String[] enabledCiphers) + throws SecurityNotPossibleException { + if (enabledProtocols != null) { + Set enabledProtocolsSet = new HashSet(Arrays.asList(enabledProtocols)); + Set supportedProtocolsSet = new HashSet( + Arrays.asList(sslSocket.getSupportedProtocols())); + Set protocolsIntersection = new HashSet(supportedProtocolsSet); + protocolsIntersection.retainAll(enabledProtocolsSet); + if (protocolsIntersection.isEmpty()) { + throw new SecurityNotPossibleException("Request to enable SSL/TLS protocols '" + + StringUtils.collectionToString(enabledProtocolsSet) + + "', but only '" + + StringUtils.collectionToString(supportedProtocolsSet) + + "' are supported."); + } + + // Set the enabled protocols + enabledProtocols = new String[protocolsIntersection.size()]; + enabledProtocols = protocolsIntersection.toArray(enabledProtocols); + sslSocket.setEnabledProtocols(enabledProtocols); + } + + if (enabledCiphers != null) { + Set enabledCiphersSet = new HashSet(Arrays.asList(enabledCiphers)); + Set supportedCiphersSet = new HashSet( + Arrays.asList(sslSocket.getEnabledCipherSuites())); + Set ciphersIntersection = new HashSet(supportedCiphersSet); + ciphersIntersection.retainAll(enabledCiphersSet); + if (ciphersIntersection.isEmpty()) { + throw new SecurityNotPossibleException("Request to enable SSL/TLS ciphers '" + + StringUtils.collectionToString(enabledCiphersSet) + + "', but only '" + + StringUtils.collectionToString(supportedCiphersSet) + + "' are supported."); + } + + enabledCiphers = new String[ciphersIntersection.size()]; + enabledCiphers = ciphersIntersection.toArray(enabledCiphers); + sslSocket.setEnabledCipherSuites(enabledCiphers); + } + } + + public static class AcceptAllTrustManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) + throws CertificateException { + // Nothing to do here + } + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) + throws CertificateException { + // Nothing to do here + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index 181331239..98bd8cfb2 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -23,9 +23,9 @@ import org.jivesoftware.smack.ConnectionListener; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.AlreadyLoggedInException; -import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.ConnectionException; +import org.jivesoftware.smack.SmackException.SecurityNotPossibleException; import org.jivesoftware.smack.SmackException.SecurityRequiredException; import org.jivesoftware.smack.XMPPException.StreamErrorException; import org.jivesoftware.smack.XMPPConnection; @@ -41,6 +41,7 @@ import org.jivesoftware.smack.sasl.SASLMechanism.Success; import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.TLSUtils; import org.jivesoftware.smack.util.dns.HostAddress; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -68,9 +69,15 @@ import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.reflect.Constructor; import java.net.Socket; +import java.security.KeyManagementException; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.Provider; import java.security.Security; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; @@ -578,10 +585,18 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * The server has indicated that TLS negotiation can start. We now need to secure the * existing plain connection and perform a handshake. This method won't return until the * connection has finished the handshake or an error occurred while securing the connection. + * @throws IOException + * @throws CertificateException + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws KeyStoreException + * @throws UnrecoverableKeyException + * @throws KeyManagementException + * @throws SecurityNotPossibleException * * @throws Exception if an exception occurs. */ - private void proceedTLSReceived() throws Exception { + private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException, UnrecoverableKeyException, KeyManagementException, SecurityNotPossibleException { SSLContext context = this.config.getCustomSSLContext(); KeyStore ks = null; KeyManager[] kms = null; @@ -655,14 +670,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // Initialize the reader and writer with the new secured version initReaderAndWriter(); - try { - // Proceed to do the handshake - ((SSLSocket) socket).startHandshake(); - } - catch (IOException e) { - setConnectionException(e); - throw e; - } + final SSLSocket sslSocket = (SSLSocket) socket; + TLSUtils.setEnabledProtocolsAndCiphers(sslSocket, config.getEnabledSSLProtocols(), config.getEnabledSSLCiphers()); + + // Proceed to do the handshake + sslSocket.startHandshake(); + //if (((SSLSocket) socket).getWantClientAuth()) { // System.err.println("XMPPConnection wants client auth"); //} @@ -907,11 +920,10 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * has been established or if the server's features could not be parsed within * the connection's PacketReplyTimeout. * - * @throws NoResponseException if the server fails to send an opening stream back - * within packetReplyTimeout. * @throws IOException + * @throws SmackException */ - synchronized void startup() throws NoResponseException, IOException { + synchronized void startup() throws IOException, SmackException { readerThread.start(); try { @@ -1006,11 +1018,17 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { parseFeatures(parser); } else if (name.equals("proceed")) { - // Secure the connection by negotiating TLS - proceedTLSReceived(); - // Reset the state of the parser since a new stream element is going - // to be sent by the server - resetParser(); + try { + // Secure the connection by negotiating TLS + proceedTLSReceived(); + // Reset the state of the parser since a new stream element is going + // to be sent by the server + resetParser(); + } + catch (Exception e) { + setConnectionException(e); + throw e; + } } else if (name.equals("failure")) { String namespace = parser.getNamespace(null);