From 89dc3a0e8515b38f4a4df1d19bc03db9d000f929 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 1 Aug 2014 10:34:47 +0200 Subject: [PATCH] Add smack-android and redesign SASL authentication This commit marks an important milestone with the addition of the smack-android subproject. Smack is now able to run native on Android without requiring any modifications, which makes the aSmack build environment obsolete. It was necessary to redesign the code for SASL authentication to achieve this. Smack now comes with smack-sasl-provided for SASL implementations that do not rely on additional APIs like javax for platforms where those APIs are not available like Android. --- .travis.yml | 11 +- build.gradle | 2 +- settings.gradle | 5 +- smack-android/build.gradle | 50 +++ .../debugger/android/AndroidDebugger.java | 174 ++++++++ .../smack/bosh/BOSHPacketReader.java | 11 +- smack-core/build.gradle | 15 + .../smack/AbstractXMPPConnection.java | 14 +- .../smack/SASLAuthentication.java | 394 ++++++++---------- .../smack/SmackConfiguration.java | 2 +- .../smack/sasl/SASLAnonymous.java | 37 +- .../smack/sasl/SASLErrorException.java | 2 +- .../smack/sasl/SASLGSSAPIMechanism.java | 89 ---- .../smack/sasl/SASLMechanism.java | 323 ++++---------- .../smack/sasl/packet/SaslStanzas.java | 186 +++++++++ .../jivesoftware/smack/util/ByteUtils.java | 48 +++ .../smack/util/PacketParserUtils.java | 2 +- .../jivesoftware/smack/util/StringUtils.java | 68 +-- .../org.jivesoftware.smack/smack-config.xml | 2 + .../smack/sasl/AbstractSaslTest.java | 31 ++ .../smack/sasl/DigestMd5SaslTest.java | 61 +++ .../smackx}/debugger/LiteDebugger.java | 3 +- smack-java7/build.gradle | 1 + smack-sasl-javax/build.gradle | 8 + .../sasl/javax}/SASLCramMD5Mechanism.java | 22 +- .../sasl/javax}/SASLDigestMD5Mechanism.java | 22 +- .../sasl/javax}/SASLExternalMechanism.java | 23 +- .../smack/sasl/javax/SASLGSSAPIMechanism.java | 69 +++ .../smack/sasl/javax/SASLJavaXMechanism.java | 147 +++++++ .../sasl/javax/SASLJavaXSmackInitializer.java | 42 ++ .../smack/sasl/javax}/SASLPlainMechanism.java | 22 +- .../smack-sasl-javax-components.xml | 6 + .../smack/sasl/javax/SASLDigestMD5Test.java | 34 ++ smack-sasl-provided/build.gradle | 8 + .../sasl/provided/SASLDigestMD5Mechanism.java | 206 +++++++++ .../SASLProvidedSmackInitializer.java | 37 ++ .../smack-sasl-provided-components.xml | 6 + .../sasl/provided/SASLDigestMD5Test.java | 34 ++ .../smack/tcp/XMPPTCPConnection.java | 20 +- 39 files changed, 1562 insertions(+), 675 deletions(-) create mode 100644 smack-android/build.gradle create mode 100644 smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStanzas.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/ByteUtils.java create mode 100644 smack-core/src/test/java/org/jivesoftware/smack/sasl/AbstractSaslTest.java create mode 100644 smack-core/src/test/java/org/jivesoftware/smack/sasl/DigestMd5SaslTest.java rename {smack-core/src/main/java/org/jivesoftware/smack => smack-debug/src/main/java/org/jivesoftware/smackx}/debugger/LiteDebugger.java (99%) create mode 100644 smack-sasl-javax/build.gradle rename {smack-core/src/main/java/org/jivesoftware/smack/sasl => smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax}/SASLCramMD5Mechanism.java (65%) rename {smack-core/src/main/java/org/jivesoftware/smack/sasl => smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax}/SASLDigestMD5Mechanism.java (64%) rename {smack-core/src/main/java/org/jivesoftware/smack/sasl => smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax}/SASLExternalMechanism.java (81%) create mode 100644 smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLGSSAPIMechanism.java create mode 100644 smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXMechanism.java create mode 100644 smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXSmackInitializer.java rename {smack-core/src/main/java/org/jivesoftware/smack/sasl => smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax}/SASLPlainMechanism.java (65%) create mode 100644 smack-sasl-javax/src/main/resources/org.jivesoftware.smack/smack-sasl-javax-components.xml create mode 100644 smack-sasl-javax/src/test/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Test.java create mode 100644 smack-sasl-provided/build.gradle create mode 100644 smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java create mode 100644 smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLProvidedSmackInitializer.java create mode 100644 smack-sasl-provided/src/main/resources/org.jivesoftware.smack/smack-sasl-provided-components.xml create mode 100644 smack-sasl-provided/src/test/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Test.java diff --git a/.travis.yml b/.travis.yml index f5c99a7f6..382050bb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,10 @@ -language: java \ No newline at end of file +language: java + +before_install: + - sudo apt-get update -qq + - if [ `uname -m` = x86_64 ]; then sudo apt-get install -qq --force-yes libgd2-xpm ia32-libs ia32-libs-multiarch; fi + - wget http://dl.google.com/android/android-sdk_r23.0.2-linux.tgz + - tar xzf android-* + - export ANDROID_HOME=$PWD/android-sdk-linux + - export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools + - echo "y" | android update sdk --filter platform-tools,android-8 --no-ui --force diff --git a/build.gradle b/build.gradle index 2d43e6f45..b0e231db1 100644 --- a/build.gradle +++ b/build.gradle @@ -237,7 +237,7 @@ subprojects { } } -['smack-resolver-javax'].each { name -> +['smack-resolver-javax', 'smack-sasl-javax', 'smack-sasl-provided'].each { name -> project(":$name") { jar { manifest { diff --git a/settings.gradle b/settings.gradle index 21ac3fcea..f12514295 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,8 +6,11 @@ include 'smack-core', 'smack-resolver-dnsjava', 'smack-resolver-minidns', 'smack-resolver-javax', + 'smack-sasl-javax', + 'smack-sasl-provided', 'smack-compression-jzlib', 'smack-legacy', 'smack-jingle', 'smack-bosh', - 'smack-java7' \ No newline at end of file + 'smack-android', + 'smack-java7' diff --git a/smack-android/build.gradle b/smack-android/build.gradle new file mode 100644 index 000000000..92eb8ee5b --- /dev/null +++ b/smack-android/build.gradle @@ -0,0 +1,50 @@ +description = """\ +Smack for Android. +All the required dependencies to run Smack on Android""" + +ext { + smackMinAndroidSdk = 8 + androidProjects = [':smack-tcp',':smack-core', ':smack-resolver-minidns', ':smack-sasl-provided', ':smack-extensions', ':smack-experimental'].collect{ project(it) } +} + +// Note that the test dependencies (junit, …) are inferred from the +// sourceSet.test of the core subproject +dependencies { + androidProjects.each { project -> + compile project + } +} + +def getAndroidRuntimeJar() { + def androidHome = new File("$System.env.ANDROID_HOME") + if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set") + new File("$androidHome/platforms/android-$smackMinAndroidSdk/android.jar") +} + +def getAndroidJavadocOffline() { + def androidHome = new File("$System.env.ANDROID_HOME") + if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set") + return "$System.env.ANDROID_HOME" + "/docs/reference" +} + +compileJava { + options.bootClasspath = getAndroidRuntimeJar() +} + +// See http://stackoverflow.com/a/2823592/194894 +// TODO this doesn't seem to work right now. But on the other hand it +// is not really required, just to avoid a javadoc compiler warning +javadoc { + options.linksOffline "http://developer.android.com/reference", getAndroidJavadocOffline() +} + +configure (androidProjects) { + task compileAndroid(type: JavaCompile) { + source = compileJava.source + classpath = compileJava.classpath + destinationDir = new File(buildDir, 'android') + options.bootClasspath = getAndroidRuntimeJar() + } +} + +test { dependsOn androidProjects*.compileAndroid } \ No newline at end of file diff --git a/smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java b/smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java new file mode 100644 index 000000000..efdcc6469 --- /dev/null +++ b/smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java @@ -0,0 +1,174 @@ +/** + * + * 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.smackx.debugger.android; + +import org.jivesoftware.smack.debugger.SmackDebugger; +import org.jivesoftware.smack.AbstractConnectionListener; +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.*; + +import android.util.Log; + +import java.io.Reader; +import java.io.Writer; + +/** + * Very simple debugger that prints to the android log the sent and received stanzas. Use + * this debugger with caution since printing to the console is an expensive operation that may + * even block the thread since only one thread may print at a time.

+ *

+ * It is possible to not only print the raw sent and received stanzas but also the interpreted + * packets by Smack. By default interpreted packets won't be printed. To enable this feature + * just change the printInterpreted static variable to true. + * + */ +public class AndroidDebugger implements SmackDebugger { + + public static boolean printInterpreted = false; + + private XMPPConnection connection = null; + + private PacketListener listener = null; + private ConnectionListener connListener = null; + + private Writer writer; + private Reader reader; + private ReaderListener readerListener; + private WriterListener writerListener; + + public AndroidDebugger(XMPPConnection connection, Writer writer, Reader reader) { + this.connection = connection; + this.writer = writer; + this.reader = reader; + createDebug(); + } + + /** + * Creates the listeners that will print in the console when new activity is detected. + */ + private void createDebug() { + // Create a special Reader that wraps the main Reader and logs data to the GUI. + ObservableReader debugReader = new ObservableReader(reader); + readerListener = new ReaderListener() { + public void read(String str) { + Log.d("SMACK", "RCV (" + connection.getConnectionCounter() + + "): " + str); + } + }; + debugReader.addReaderListener(readerListener); + + // Create a special Writer that wraps the main Writer and logs data to the GUI. + ObservableWriter debugWriter = new ObservableWriter(writer); + writerListener = new WriterListener() { + public void write(String str) { + Log.d("SMACK", "SENT (" + connection.getConnectionCounter() + + "): " + str); + } + }; + debugWriter.addWriterListener(writerListener); + + // Assign the reader/writer objects to use the debug versions. The packet reader + // and writer will use the debug versions when they are created. + reader = debugReader; + writer = debugWriter; + + // Create a thread that will listen for all incoming packets and write them to + // the GUI. This is what we call "interpreted" packet data, since it's the packet + // data as Smack sees it and not as it's coming in as raw XML. + listener = new PacketListener() { + public void processPacket(Packet packet) { + if (printInterpreted) { + Log.d("SMACK", "RCV PKT (" + connection.getConnectionCounter() + + "): " + packet.toXML()); + } + } + }; + + connListener = new AbstractConnectionListener() { + public void connectionClosed() { + Log.d("SMACK", "Connection closed (" + connection.getConnectionCounter() + ")"); + } + + public void connectionClosedOnError(Exception e) { + Log.d("SMACK", "Connection closed due to an exception (" + + connection.getConnectionCounter() + ")"); + } + public void reconnectionFailed(Exception e) { + Log.d("SMACK", "Reconnection failed due to an exception (" + + connection.getConnectionCounter() + ")"); + } + public void reconnectionSuccessful() { + Log.d("SMACK", "Connection reconnected (" + + connection.getConnectionCounter() + ")"); + } + public void reconnectingIn(int seconds) { + Log.d("SMACK", "Connection (" + connection.getConnectionCounter() + + ") will reconnect in " + seconds); + } + }; + } + + public Reader newConnectionReader(Reader newReader) { + ((ObservableReader)reader).removeReaderListener(readerListener); + ObservableReader debugReader = new ObservableReader(newReader); + debugReader.addReaderListener(readerListener); + reader = debugReader; + return reader; + } + + public Writer newConnectionWriter(Writer newWriter) { + ((ObservableWriter)writer).removeWriterListener(writerListener); + ObservableWriter debugWriter = new ObservableWriter(newWriter); + debugWriter.addWriterListener(writerListener); + writer = debugWriter; + return writer; + } + + public void userHasLogged(String user) { + String title = + "User logged (" + connection.getConnectionCounter() + "): " + + user + + "@" + + connection.getServiceName() + + ":" + + connection.getPort(); + Log.d("SMACK", title); + // Add the connection listener to the connection so that the debugger can be notified + // whenever the connection is closed. + connection.addConnectionListener(connListener); + } + + public Reader getReader() { + return reader; + } + + public Writer getWriter() { + return writer; + } + + public PacketListener getReaderListener() { + return listener; + } + + public PacketListener getWriterListener() { + return null; + } +} + diff --git a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java index 91a557c68..7a456a742 100644 --- a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java +++ b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java @@ -20,9 +20,9 @@ package org.jivesoftware.smack.bosh; import java.io.StringReader; import org.jivesoftware.smack.packet.Packet; -import org.jivesoftware.smack.sasl.SASLMechanism.Challenge; -import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure; -import org.jivesoftware.smack.sasl.SASLMechanism.Success; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.Challenge; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.XMPPException.StreamErrorException; import org.xmlpull.v1.XmlPullParserFactory; @@ -97,8 +97,9 @@ public class BOSHPacketReader implements BOSHClientResponseListener { BodyQName.create(XMPPBOSHConnection.BOSH_URI, "to"), connection.getServiceName()) .build()); - connection.getSASLAuthentication().authenticated(); - connection.processPacket(new Success(parser.nextText())); + Success success = new Success(parser.nextText()); + connection.getSASLAuthentication().authenticated(success); + connection.processPacket(success); } else if (parser.getName().equals("features")) { parseFeatures(parser); } else if (parser.getName().equals("failure")) { diff --git a/smack-core/build.gradle b/smack-core/build.gradle index b0d4a8547..847876ae3 100644 --- a/smack-core/build.gradle +++ b/smack-core/build.gradle @@ -4,6 +4,7 @@ Smack core components.""" configurations { compression dns + sasl } dependencies { @@ -34,9 +35,23 @@ task dnsJar(type: Jar) { include('org/jivesoftware/smack/initializer/**') } +task saslJar(type: Jar) { + appendix = 'sasl' + dependsOn classes + from sourceSets.main.output + include('org/jivesoftware/smack/SASLAuthentication.class') + include('org/jivesoftware/smack/SmackException.class') + include('org/jivesoftware/smack/XMPPConnection.class') + include('org/jivesoftware/smack/sasl/**') + include('org/jivesoftware/smack/initializer/**') + include('org/jivesoftware/smack/util/StringUtils.class') + include('org/jivesoftware/smack/util/ByteUtils.class') +} + artifacts { compression compressionJar dns dnsJar + sasl saslJar } class CreateFileTask extends DefaultTask { 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 dea05c0a7..358d65778 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -33,8 +33,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -import javax.security.sasl.SaslException; - import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.ConnectionException; @@ -60,7 +58,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { private static final String[] DEBUGGERS = new String[] { "org.jivesoftware.smackx.debugger.EnhancedDebugger", "org.jivesoftware.smackx.debugger.android.AndroidDebugger", - "org.jivesoftware.smack.debugger.LiteDebugger" }; + "org.jivesoftware.smack.debugger.LiteDebugger", + "org.jivesoftware.smack.debugger.ConsoleDebugger" }; /** * Counter to uniquely identify connections that are created. @@ -347,9 +346,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * @throws XMPPException if an error occurs on the XMPP protocol level. * @throws SmackException if an error occurs somehwere else besides XMPP protocol level. * @throws IOException - * @throws SaslException */ - public void login(String username, String password) throws XMPPException, SmackException, SaslException, IOException { + public void login(String username, String password) throws XMPPException, SmackException, IOException { login(username, password, "Smack"); } @@ -378,9 +376,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * @throws XMPPException if an error occurs on the XMPP protocol level. * @throws SmackException if an error occurs somehwere else besides XMPP protocol level. * @throws IOException - * @throws SaslException */ - public abstract void login(String username, String password, String resource) throws XMPPException, SmackException, SaslException, IOException; + public abstract void login(String username, String password, String resource) throws XMPPException, SmackException, IOException; /** * Logs in to the server anonymously. Very few servers are configured to support anonymous @@ -391,9 +388,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * @throws XMPPException if an error occurs on the XMPP protocol level. * @throws SmackException if an error occurs somehwere else besides XMPP protocol level. * @throws IOException - * @throws SaslException */ - public abstract void loginAnonymously() throws XMPPException, SmackException, SaslException, IOException; + public abstract void loginAnonymously() throws XMPPException, SmackException, IOException; /** * Notification message saying that the server requires the client to bind a diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java index 263e40e89..70e7d2085 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java @@ -18,54 +18,37 @@ package org.jivesoftware.smack; import org.jivesoftware.smack.SmackException.NoResponseException; -import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.SmackException.ResourceBindingNotOfferedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.sasl.SASLAnonymous; -import org.jivesoftware.smack.sasl.SASLCramMD5Mechanism; -import org.jivesoftware.smack.sasl.SASLDigestMD5Mechanism; import org.jivesoftware.smack.sasl.SASLErrorException; -import org.jivesoftware.smack.sasl.SASLExternalMechanism; -import org.jivesoftware.smack.sasl.SASLGSSAPIMechanism; import org.jivesoftware.smack.sasl.SASLMechanism; -import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure; -import org.jivesoftware.smack.sasl.SASLPlainMechanism; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success; import javax.security.auth.callback.CallbackHandler; -import javax.security.sasl.SaslException; import java.io.IOException; -import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; /** *

This class is responsible authenticating the user using SASL, binding the resource * to the connection and establishing a session with the server.

* *

Once TLS has been negotiated (i.e. the connection has been secured) it is possible to - * register with the server, authenticate using Non-SASL or authenticate using SASL. If the - * server supports SASL then Smack will first try to authenticate using SASL. But if that - * fails then Non-SASL will be tried.

+ * register with the server or authenticate using SASL. If the + * server supports SASL then Smack will try to authenticate using SASL..

* *

The server may support many SASL mechanisms to use for authenticating. Out of the box * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use - * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered - * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default, - * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}.

- * - *

Once the user has been authenticated with SASL, it is necessary to bind a resource for - * the connection. If no resource is passed in {@link #authenticate(String, String, String)} - * then the server will assign a resource for the connection. In case a resource is passed - * then the server will receive the desired resource but may assign a modified resource for - * the connection.

- * - *

Once a resource has been binded and if the server supports sessions then Smack will establish - * a session so that instant messaging and presence functionalities may be used.

+ * {@link #registerSASLMechanism(SASLMechanism)} to register a new mechanisms. * * @see org.jivesoftware.smack.sasl.SASLMechanism * @@ -74,114 +57,91 @@ import java.util.Map; */ public class SASLAuthentication { - private static Map> implementedMechanisms = new HashMap>(); - private static List mechanismsPreferences = new ArrayList(); + private static final List REGISTERED_MECHANISMS = new ArrayList(); - private XMPPConnection connection; + private static final Set BLACKLISTED_MECHANISMS = new HashSet(); + + /** + * Registers a new SASL mechanism + * + * @param mechanism a SASLMechanism subclass. + */ + public static void registerSASLMechanism(SASLMechanism mechanism) { + synchronized (REGISTERED_MECHANISMS) { + REGISTERED_MECHANISMS.add(mechanism); + Collections.sort(REGISTERED_MECHANISMS); + } + } + + /** + * Returns the registered SASLMechanism sorted by the level of preference. + * + * @return the registered SASLMechanism sorted by the level of preference. + */ + public static Map getRegisterdSASLMechanisms() { + Map answer = new HashMap(); + synchronized (REGISTERED_MECHANISMS) { + for (SASLMechanism mechanism : REGISTERED_MECHANISMS) { + answer.put(mechanism.getClass().getName(), mechanism.getName()); + } + } + return answer; + } + + /** + * Unregister a SASLMechanism by it's full class name. For example + * "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism". + * + * @param clazz the SASLMechanism class's name + * @return true if the given SASLMechanism was removed, false otherwise + */ + public static boolean unregisterSASLMechanism(String clazz) { + synchronized (REGISTERED_MECHANISMS) { + Iterator it = REGISTERED_MECHANISMS.iterator(); + while (it.hasNext()) { + SASLMechanism mechanism = it.next(); + if (mechanism.getClass().getName().equals(clazz)) { + it.remove(); + return true; + } + } + } + return false; + } + + public static boolean blacklistSASLMechanism(String mechansim) { + synchronized(BLACKLISTED_MECHANISMS) { + return BLACKLISTED_MECHANISMS.add(mechansim); + } + } + + public static boolean unBlacklistSASLMechanism(String mechanism) { + synchronized(BLACKLISTED_MECHANISMS) { + return BLACKLISTED_MECHANISMS.remove(mechanism); + } + } + + public static Set getBlacklistedSASLMechanisms() { + synchronized(BLACKLISTED_MECHANISMS) { + return new HashSet(BLACKLISTED_MECHANISMS); + } + } + + private final AbstractXMPPConnection connection; private Collection serverMechanisms = new ArrayList(); private SASLMechanism currentMechanism = null; + /** * Boolean indicating if SASL negotiation has finished and was successful. */ private boolean saslNegotiated; /** - * The SASL related error condition if there was one provided by the server. + * Either of type {@link SmackException} or {@link SASLErrorException} */ - private SASLFailure saslFailure; + private Exception saslException; - static { - - // Register SASL mechanisms supported by Smack - registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class); - registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class); - registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class); - registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class); - registerSASLMechanism("PLAIN", SASLPlainMechanism.class); - registerSASLMechanism("ANONYMOUS", SASLAnonymous.class); - - supportSASLMechanism("GSSAPI",0); - supportSASLMechanism("DIGEST-MD5",1); - supportSASLMechanism("CRAM-MD5",2); - supportSASLMechanism("PLAIN",3); - supportSASLMechanism("ANONYMOUS",4); - - } - - /** - * Registers a new SASL mechanism - * - * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. - * @param mClass a SASLMechanism subclass. - */ - public static void registerSASLMechanism(String name, Class mClass) { - implementedMechanisms.put(name, mClass); - } - - /** - * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't - * be possible to authenticate users using the removed SASL mechanism. It also removes the - * mechanism from the supported list. - * - * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. - */ - public static void unregisterSASLMechanism(String name) { - implementedMechanisms.remove(name); - mechanismsPreferences.remove(name); - } - - - /** - * Registers a new SASL mechanism in the specified preference position. The client will try - * to authenticate using the most prefered SASL mechanism that is also supported by the server. - * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)} - * - * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. - */ - public static void supportSASLMechanism(String name) { - mechanismsPreferences.add(0, name); - } - - /** - * Registers a new SASL mechanism in the specified preference position. The client will try - * to authenticate using the most prefered SASL mechanism that is also supported by the server. - * Use the index parameter to set the level of preference of the new SASL mechanism. - * A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be - * registered via {@link #registerSASLMechanism(String, Class)} - * - * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. - * @param index preference position amongst all the implemented SASL mechanism. Starts with 0. - */ - public static void supportSASLMechanism(String name, int index) { - mechanismsPreferences.add(index, name); - } - - /** - * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't - * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism - * is still registered, but will just not be used. - * - * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. - */ - public static void unsupportSASLMechanism(String name) { - mechanismsPreferences.remove(name); - } - - /** - * Returns the registerd SASLMechanism classes sorted by the level of preference. - * - * @return the registerd SASLMechanism classes sorted by the level of preference. - */ - public static List> getRegisterSASLMechanisms() { - List> answer = new ArrayList>(); - for (String mechanismsPreference : mechanismsPreferences) { - answer.add(implementedMechanisms.get(mechanismsPreference)); - } - return answer; - } - - SASLAuthentication(XMPPConnection connection) { - super(); + SASLAuthentication(AbstractXMPPConnection connection) { this.connection = connection; this.init(); } @@ -216,42 +176,16 @@ public class SASLAuthentication { * @param cbh the CallbackHandler used to get information from the user * @throws IOException * @throws XMPPErrorException - * @throws NoResponseException * @throws SASLErrorException - * @throws ResourceBindingNotOfferedException - * @throws NotConnectedException + * @throws SmackException */ public void authenticate(String resource, CallbackHandler cbh) throws IOException, - NoResponseException, XMPPErrorException, SASLErrorException, ResourceBindingNotOfferedException, NotConnectedException { - // Locate the SASLMechanism to use - String selectedMechanism = null; - for (String mechanism : mechanismsPreferences) { - if (implementedMechanisms.containsKey(mechanism) - && serverMechanisms.contains(mechanism)) { - selectedMechanism = mechanism; - break; - } - } + XMPPErrorException, SASLErrorException, SmackException { + SASLMechanism selectedMechanism = selectMechanism(); if (selectedMechanism != null) { - // A SASL mechanism was found. Authenticate using the selected mechanism and then - // proceed to bind a resource - Class mechanismClass = implementedMechanisms.get(selectedMechanism); - - Constructor constructor; - try { - constructor = mechanismClass.getConstructor(SASLAuthentication.class); - currentMechanism = constructor.newInstance(this); - } - catch (Exception e) { - throw new SaslException("Exception when creating the SASLAuthentication instance", - e); - } - + currentMechanism = selectedMechanism; synchronized (this) { - // Trigger SASL authentication with the selected mechanism. We use - // connection.getHost() since GSAPI requires the FQDN of the server, which - // may not match the XMPP domain. - currentMechanism.authenticate(connection.getHost(), cbh); + currentMechanism.authenticate(connection.getHost(), connection.getServiceName(), cbh); try { // Wait until SASL negotiation finishes wait(connection.getPacketReplyTimeout()); @@ -261,18 +195,14 @@ public class SASLAuthentication { } } - if (saslFailure != null) { - // SASL authentication failed and the server may have closed the connection - // so throw an exception - throw new SASLErrorException(selectedMechanism, saslFailure); - } + maybeThrowException(); if (!saslNegotiated) { throw new NoResponseException(); } } else { - throw new SaslException( + throw new SmackException( "SASL Authentication failed. No known authentication mechanisims."); } } @@ -291,47 +221,18 @@ public class SASLAuthentication { * @throws XMPPErrorException * @throws SASLErrorException * @throws IOException - * @throws SaslException * @throws SmackException */ public void authenticate(String username, String password, String resource) - throws XMPPErrorException, SASLErrorException, SaslException, IOException, + throws XMPPErrorException, SASLErrorException, IOException, SmackException { - // Locate the SASLMechanism to use - String selectedMechanism = null; - for (String mechanism : mechanismsPreferences) { - if (implementedMechanisms.containsKey(mechanism) - && serverMechanisms.contains(mechanism)) { - selectedMechanism = mechanism; - break; - } - } + SASLMechanism selectedMechanism = selectMechanism(); if (selectedMechanism != null) { - // A SASL mechanism was found. Authenticate using the selected mechanism and then - // proceed to bind a resource - Class mechanismClass = implementedMechanisms.get(selectedMechanism); - try { - Constructor constructor = mechanismClass.getConstructor(SASLAuthentication.class); - currentMechanism = constructor.newInstance(this); - } - catch (Exception e) { - throw new SaslException("Exception when creating the SASLAuthentication instance", - e); - } + currentMechanism = selectedMechanism; synchronized (this) { - // Trigger SASL authentication with the selected mechanism. We use - // connection.getHost() since GSAPI requires the FQDN of the server, which - // may not match the XMPP domain. - - // The serviceName is basically the value that XMPP server sends to the client as - // being - // the location of the XMPP service we are trying to connect to. This should have - // the - // format: host ["/" serv-name ] as per RFC-2831 guidelines - String serviceName = connection.getServiceName(); - currentMechanism.authenticate(username, connection.getHost(), serviceName, password); - + currentMechanism.authenticate(username, connection.getHost(), + connection.getServiceName(), password); try { // Wait until SASL negotiation finishes wait(connection.getPacketReplyTimeout()); @@ -339,21 +240,16 @@ public class SASLAuthentication { catch (InterruptedException e) { // Ignore } - } - if (saslFailure != null) { - // SASL authentication failed and the server may have closed the connection - // so throw an exception - throw new SASLErrorException(selectedMechanism, saslFailure); - } + maybeThrowException(); if (!saslNegotiated) { throw new NoResponseException(); } } else { - throw new SaslException( + throw new SmackException( "SASL Authentication failed. No known authentication mechanisims."); } } @@ -367,14 +263,12 @@ public class SASLAuthentication { * no username. * * @throws SASLErrorException - * @throws IOException - * @throws SaslException * @throws XMPPErrorException if an error occures while authenticating. * @throws SmackException if there was no response from the server. */ - public void authenticateAnonymously() throws SASLErrorException, SaslException, IOException, + public void authenticateAnonymously() throws SASLErrorException, SmackException, XMPPErrorException { - currentMechanism = new SASLAnonymous(this); + currentMechanism = (new SASLAnonymous()).instanceForAuthentication(connection); // Wait until SASL negotiation finishes synchronized (this) { @@ -387,17 +281,24 @@ public class SASLAuthentication { } } - if (saslFailure != null) { - // SASL authentication failed and the server may have closed the connection - // so throw an exception - throw new SASLErrorException(currentMechanism.toString(), saslFailure); - } + maybeThrowException(); if (!saslNegotiated) { throw new NoResponseException(); } } + private void maybeThrowException() throws SmackException, SASLErrorException { + if (saslException != null){ + if (saslException instanceof SmackException) { + throw (SmackException) saslException; + } else if (saslException instanceof SASLErrorException) { + throw (SASLErrorException) saslException; + } else { + throw new IllegalStateException("Unexpected exception type" , saslException); + } + } + } /** * Sets the available SASL mechanism reported by the server. The server will report the * available SASL mechanism once the TLS negotiation was successful. This information is @@ -411,33 +312,49 @@ public class SASLAuthentication { } /** - * Returns true if the user was able to authenticate with the server usins SASL. - * - * @return true if the user was able to authenticate with the server usins SASL. + * Wrapper for {@link #challengeReceived(String, boolean)}, with finalChallenge set + * to false. + * + * @param challenge a base64 encoded string representing the challenge. + * @throws SmackException */ - public boolean isAuthenticated() { - return saslNegotiated; + public void challengeReceived(String challenge) throws SmackException { + challengeReceived(challenge, false); } /** * The server is challenging the SASL authentication we just sent. Forward the challenge - * to the current SASLMechanism we are using. The SASLMechanism will send a response to + * to the current SASLMechanism we are using. The SASLMechanism will eventually send a response to * the server. The length of the challenge-response sequence varies according to the * SASLMechanism in use. * * @param challenge a base64 encoded string representing the challenge. - * @throws IOException If a network error occures while authenticating. - * @throws NotConnectedException + * @param finalChallenge true if this is the last challenge send by the server within the success stanza + * @throws SmackException */ - public void challengeReceived(String challenge) throws IOException, NotConnectedException { - currentMechanism.challengeReceived(challenge); + public void challengeReceived(String challenge, boolean finalChallenge) throws SmackException { + try { + currentMechanism.challengeReceived(challenge, finalChallenge); + } catch (SmackException e) { + authenticationFailed(e); + throw e; + } } /** * Notification message saying that SASL authentication was successful. The next step * would be to bind the resource. + * @throws SmackException */ - public void authenticated() { + public void authenticated(Success success) throws SmackException { + // RFC6120 6.3.10 "At the end of the authentication exchange, the SASL server (the XMPP + // "receiving entity") can include "additional data with success" if appropriate for the + // SASL mechanism in use. In XMPP, this is done by including the additional data as the XML + // character data of the element." The used SASL mechanism should be able to + // verify the data send by the server in the success stanza, if any. + if (success.getData() != null) { + challengeReceived(success.getData(), true); + } saslNegotiated = true; // Wake up the thread that is waiting in the #authenticate method synchronized (this) { @@ -453,18 +370,17 @@ public class SASLAuthentication { * @see RFC6120 6.5 */ public void authenticationFailed(SASLFailure saslFailure) { - this.saslFailure = saslFailure; + authenticationFailed(new SASLErrorException(currentMechanism.getName(), saslFailure)); + } + + public void authenticationFailed(Exception exception) { + saslException = exception; // Wake up the thread that is waiting in the #authenticate method synchronized (this) { notify(); } } - public void send(Packet stanza) throws NotConnectedException { - connection.sendPacket(stanza); - } - - /** * Initializes the internal state in order to be able to be reused. The authentication * is used by the connection at the first login and then reused after the connection @@ -472,6 +388,28 @@ public class SASLAuthentication { */ protected void init() { saslNegotiated = false; - saslFailure = null; + saslException = null; + } + + private SASLMechanism selectMechanism() { + // Locate the SASLMechanism to use + SASLMechanism selectedMechanism = null; + Iterator it = REGISTERED_MECHANISMS.iterator(); + // Iterate in SASL Priority order over registered mechanisms + while (it.hasNext()) { + SASLMechanism mechanism = it.next(); + String mechanismName = mechanism.getName(); + synchronized (BLACKLISTED_MECHANISMS) { + if (BLACKLISTED_MECHANISMS.contains(mechanismName)) { + continue; + } + } + if (serverMechanisms.contains(mechanismName)) { + // Create a new instance of the SASLMechanism for every authentication attempt. + selectedMechanism = mechanism.instanceForAuthentication(connection); + break; + } + } + return selectedMechanism; } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java index 11f225b04..1a20f6e96 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java @@ -432,7 +432,7 @@ public final class SmackConfiguration { if (SmackInitializer.class.isAssignableFrom(initClass)) { SmackInitializer initializer = (SmackInitializer) initClass.newInstance(); List exceptions = initializer.initialize(); - if (exceptions.size() == 0) { + if (exceptions == null || exceptions.size() == 0) { LOGGER.log(Level.FINE, "Loaded SmackInitializer " + className); } else { for (Exception e : exceptions) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java index 9234293e7..d22a9942c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java @@ -16,10 +16,7 @@ */ package org.jivesoftware.smack.sasl; -import org.jivesoftware.smack.SASLAuthentication; -import org.jivesoftware.smack.SmackException.NotConnectedException; - -import java.io.IOException; +import org.jivesoftware.smack.SmackException; import javax.security.auth.callback.CallbackHandler; @@ -30,32 +27,30 @@ import javax.security.auth.callback.CallbackHandler; */ public class SASLAnonymous extends SASLMechanism { - public SASLAnonymous(SASLAuthentication saslAuthentication) { - super(saslAuthentication); - } - - protected String getName() { + public String getName() { return "ANONYMOUS"; } - public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, NotConnectedException { - authenticate(); + @Override + public int getPriority() { + return 500; } - public void authenticate(String username, String host, String password) throws IOException, NotConnectedException { - authenticate(); + @Override + protected void authenticateInternal(CallbackHandler cbh) + throws SmackException { + // Nothing to do here } - protected void authenticate() throws IOException, NotConnectedException { - // Send the authentication to the server - getSASLAuthentication().send(new AuthMechanism(getName(), null)); + @Override + protected String getAuthenticationText() throws SmackException { + // TODO Auto-generated method stub + return null; } - public void challengeReceived(String challenge) throws IOException, NotConnectedException { - // Build the challenge response stanza encoding the response text - // and send the authentication to the server - getSASLAuthentication().send(new Response()); + @Override + public SASLAnonymous newInstance() { + return new SASLAnonymous(); } - } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java index 460f9ff13..756a1cfa5 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java @@ -20,7 +20,7 @@ import java.util.HashMap; import java.util.Map; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; public class SASLErrorException extends XMPPException { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java deleted file mode 100644 index 21bb01ad9..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java +++ /dev/null @@ -1,89 +0,0 @@ -/** - * - * Copyright the original author or authors - * - * 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.sasl; - -import org.jivesoftware.smack.SASLAuthentication; -import org.jivesoftware.smack.SmackException.NotConnectedException; - -import java.io.IOException; -import java.util.Map; -import java.util.HashMap; - -import javax.security.sasl.Sasl; -import javax.security.sasl.SaslException; -import javax.security.auth.callback.CallbackHandler; - -/** - * Implementation of the SASL GSSAPI mechanism - * - * @author Jay Kline - */ -public class SASLGSSAPIMechanism extends SASLMechanism { - - public SASLGSSAPIMechanism(SASLAuthentication saslAuthentication) { - super(saslAuthentication); - - System.setProperty("javax.security.auth.useSubjectCredsOnly","false"); - System.setProperty("java.security.auth.login.config","gss.conf"); - - } - - protected String getName() { - return "GSSAPI"; - } - - /** - * Builds and sends the auth stanza to the server. - * This overrides from the abstract class because the initial token - * needed for GSSAPI is binary, and not safe to put in a string, thus - * getAuthenticationText() cannot be used. - * - * @param username the username of the user being authenticated. - * @param host the hostname where the user account resides. - * @param cbh the CallbackHandler (not used with GSSAPI) - * @throws IOException If a network error occures while authenticating. - * @throws NotConnectedException - */ - public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, SaslException, NotConnectedException { - String[] mechanisms = { getName() }; - Map props = new HashMap(); - props.put(Sasl.SERVER_AUTH,"TRUE"); - sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh); - authenticate(); - } - - /** - * Builds and sends the auth stanza to the server. - * This overrides from the abstract class because the initial token - * needed for GSSAPI is binary, and not safe to put in a string, thus - * getAuthenticationText() cannot be used. - * - * @param username the username of the user being authenticated. - * @param host the hostname where the user account resides. - * @param password the password of the user (ignored for GSSAPI) - * @throws IOException If a network error occures while authenticating. - * @throws NotConnectedException - */ - public void authenticate(String username, String host, String password) throws IOException, SaslException, NotConnectedException { - String[] mechanisms = { getName() }; - Map props = new HashMap(); - props.put(Sasl.SERVER_AUTH,"TRUE"); - sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, this); - authenticate(); - } - -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java index fc896f72b..3ed4b3ed2 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software. + * Copyright 2003-2007 Jive Software, 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. @@ -16,25 +16,14 @@ */ package org.jivesoftware.smack.sasl; -import org.jivesoftware.smack.SASLAuthentication; +import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.AuthMechanism; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.Response; import org.jivesoftware.smack.util.StringUtils; -import java.io.IOException; -import java.util.Map; -import java.util.HashMap; - import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.sasl.RealmCallback; -import javax.security.sasl.RealmChoiceCallback; -import javax.security.sasl.Sasl; -import javax.security.sasl.SaslClient; -import javax.security.sasl.SaslException; /** * Base class for SASL mechanisms. Subclasses must implement these methods: @@ -44,9 +33,9 @@ import javax.security.sasl.SaslException; * Subclasses will likely want to implement their own versions of these methods: *
  • {@link #authenticate(String, String, String, String)} -- Initiate authentication stanza using the * deprecated method.
  • - *
  • {@link #authenticate(String, CallbackHandler)} -- Initiate authentication stanza + *
  • {@link #authenticate(String, String, CallbackHandler)} -- Initiate authentication stanza * using the CallbackHandler method.
  • - *
  • {@link #challengeReceived(String)} -- Handle a challenge from the server.
  • + *
  • {@link #challengeReceived(String, boolean)} -- Handle a challenge from the server.
  • * * * Basic XMPP SASL authentication steps: @@ -73,26 +62,40 @@ import javax.security.sasl.SaslException; * * @author Jay Kline */ -public abstract class SASLMechanism implements CallbackHandler { +public abstract class SASLMechanism implements Comparable { - private SASLAuthentication saslAuthentication; - protected SaslClient sc; + public static final String CRAMMD5 = "CRAM-MD5"; + public static final String DIGESTMD5 = "DIGEST-MD5"; + public static final String EXTERNAL = "EXTERNAL"; + public static final String GSSAPI = "GSSAPI"; + public static final String PLAIN = "PLAIN"; + + protected XMPPConnection connection; + + /** + * authcid + */ protected String authenticationId; - protected String password; - protected String hostname; - public SASLMechanism(SASLAuthentication saslAuthentication) { - this.saslAuthentication = saslAuthentication; - } + /** + * The name of the XMPP service + */ + protected String serviceName; + + /** + * The users password + */ + protected String password; + protected String host; /** * Builds and sends the auth stanza to the server. Note that this method of - * authentication is not recommended, since it is very inflexable. Use - * {@link #authenticate(String, CallbackHandler)} whenever possible. + * authentication is not recommended, since it is very inflexible. Use + * {@link #authenticate(String, String, CallbackHandler)} whenever possible. * * Explanation of auth stanza: * - * The client authentication stanza needs to include the digest-uri of the form: xmpp/serverName + * The client authentication stanza needs to include the digest-uri of the form: xmpp/serviceName * From RFC-2831: * digest-uri = "digest-uri" "=" digest-uri-value * digest-uri-value = serv-type "/" host [ "/" serv-name ] @@ -129,70 +132,67 @@ public abstract class SASLMechanism implements CallbackHandler { * @param serviceName the xmpp service location - used by the SASL client in digest-uri creation * serviceName format is: host [ "/" serv-name ] as per RFC-2831 * @param password the password for this account. - * @throws IOException If a network error occurs while authenticating. - * @throws SaslException + * @throws SmackException If a network error occurs while authenticating. * @throws NotConnectedException */ - public void authenticate(String username, String host, String serviceName, String password) throws IOException, SaslException, NotConnectedException { - //Since we were not provided with a CallbackHandler, we will use our own with the given - //information - - //Set the authenticationID as the username, since they must be the same in this case. + public final void authenticate(String username, String host, String serviceName, String password) + throws SmackException, NotConnectedException { this.authenticationId = username; + this.host = host; + this.serviceName = serviceName; this.password = password; - this.hostname = host; - - String[] mechanisms = { getName() }; - Map props = new HashMap(); - sc = Sasl.createSaslClient(mechanisms, null, "xmpp", serviceName, props, this); + authenticateInternal(); authenticate(); } + protected void authenticateInternal() throws SmackException { + } + /** * Builds and sends the auth stanza to the server. The callback handler will handle * any additional information, such as the authentication ID or realm, if it is needed. * * @param host the hostname where the user account resides. + * @param serviceName the xmpp service location * @param cbh the CallbackHandler to obtain user information. - * @throws IOException If a network error occures while authenticating. - * @throws SaslException If a protocol error occurs or the user is not authenticated. + * @throws SmackException * @throws NotConnectedException */ - public void authenticate(String host, CallbackHandler cbh) throws IOException, SaslException, NotConnectedException { - String[] mechanisms = { getName() }; - Map props = new HashMap(); - sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh); + public void authenticate(String host,String serviceName, CallbackHandler cbh) + throws SmackException, NotConnectedException { + this.host = host; + this.serviceName = serviceName; + authenticateInternal(cbh); authenticate(); } - protected void authenticate() throws IOException, SaslException, NotConnectedException { - String authenticationText = null; - if (sc.hasInitialResponse()) { - byte[] response = sc.evaluateChallenge(new byte[0]); - authenticationText = StringUtils.encodeBase64(response, false); - } + protected abstract void authenticateInternal(CallbackHandler cbh) throws SmackException; + private final void authenticate() throws SmackException, NotConnectedException { + String authenticationText = getAuthenticationText(); // Send the authentication to the server - getSASLAuthentication().send(new AuthMechanism(getName(), authenticationText)); + connection.sendPacket(new AuthMechanism(getName(), authenticationText)); } + protected abstract String getAuthenticationText() throws SmackException; + /** * The server is challenging the SASL mechanism for the stanza he just sent. Send a * response to the server's challenge. * - * @param challenge a base64 encoded string representing the challenge. - * @throws IOException if an exception sending the response occurs. - * @throws NotConnectedException + * @param challengeString a base64 encoded string representing the challenge. + * @param finalChallenge true if this is the last challenge send by the server within the success stanza + * @throws NotConnectedException + * @throws SmackException */ - public void challengeReceived(String challenge) throws IOException, NotConnectedException { - byte response[]; - if(challenge != null) { - response = sc.evaluateChallenge(StringUtils.decodeBase64(challenge)); - } else { - response = sc.evaluateChallenge(new byte[0]); + public final void challengeReceived(String challengeString, boolean finalChallenge) throws SmackException, NotConnectedException { + byte[] challenge = StringUtils.decodeBase64(challengeString); + byte response[] = evaluateChallenge(challenge); + if (finalChallenge) { + return; } - Packet responseStanza; + Response responseStanza; if (response == null) { responseStanza = new Response(); } @@ -201,7 +201,15 @@ public abstract class SASLMechanism implements CallbackHandler { } // Send the authentication to the server - getSASLAuthentication().send(responseStanza); + connection.sendPacket(responseStanza); + } + + protected byte[] evaluateChallenge(byte[] challenge) throws SmackException { + return null; + } + + public final int compareTo(SASLMechanism other) { + return getPriority() - other.getPriority(); } /** @@ -209,184 +217,15 @@ public abstract class SASLMechanism implements CallbackHandler { * * @return the common name of the SASL mechanism. */ - protected abstract String getName(); + public abstract String getName(); - protected SASLAuthentication getSASLAuthentication() { - return saslAuthentication; + public abstract int getPriority(); + + public SASLMechanism instanceForAuthentication(XMPPConnection connection) { + SASLMechanism saslMechansim = newInstance(); + saslMechansim.connection = connection; + return saslMechansim; } - /** - * - */ - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - for (int i = 0; i < callbacks.length; i++) { - if (callbacks[i] instanceof NameCallback) { - NameCallback ncb = (NameCallback)callbacks[i]; - ncb.setName(authenticationId); - } else if(callbacks[i] instanceof PasswordCallback) { - PasswordCallback pcb = (PasswordCallback)callbacks[i]; - pcb.setPassword(password.toCharArray()); - } else if(callbacks[i] instanceof RealmCallback) { - RealmCallback rcb = (RealmCallback)callbacks[i]; - //Retrieve the REALM from the challenge response that the server returned when the client initiated the authentication - //exchange. If this value is not null or empty, *this value* has to be sent back to the server in the client's response - //to the server's challenge - String text = rcb.getDefaultText(); - //The SASL client (sc) created in smack uses rcb.getText when creating the negotiatedRealm to send it back to the server - //Make sure that this value matches the server's realm - rcb.setText(text); - } else if(callbacks[i] instanceof RealmChoiceCallback){ - //unused - //RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i]; - } else { - throw new UnsupportedCallbackException(callbacks[i]); - } - } - } - - /** - * Initiating SASL authentication by select a mechanism. - */ - public static class AuthMechanism extends Packet { - final private String name; - final private String authenticationText; - - public AuthMechanism(String name, String authenticationText) { - if (name == null) { - throw new NullPointerException("SASL mechanism name shouldn't be null."); - } - this.name = name; - this.authenticationText = authenticationText; - } - - public String toXML() { - StringBuilder stanza = new StringBuilder(); - stanza.append(""); - if (authenticationText != null && - authenticationText.trim().length() > 0) { - stanza.append(authenticationText); - } - stanza.append(""); - return stanza.toString(); - } - } - - /** - * A SASL challenge stanza. - */ - public static class Challenge extends Packet { - final private String data; - - public Challenge(String data) { - this.data = data; - } - - public String toXML() { - StringBuilder stanza = new StringBuilder(); - stanza.append(""); - if (data != null && - data.trim().length() > 0) { - stanza.append(data); - } - stanza.append(""); - return stanza.toString(); - } - } - - /** - * A SASL response stanza. - */ - public static class Response extends Packet { - final private String authenticationText; - - public Response() { - authenticationText = null; - } - - public Response(String authenticationText) { - if (authenticationText == null || authenticationText.trim().length() == 0) { - this.authenticationText = null; - } - else { - this.authenticationText = authenticationText; - } - } - - public String toXML() { - StringBuilder stanza = new StringBuilder(); - stanza.append(""); - if (authenticationText != null) { - stanza.append(authenticationText); - } - stanza.append(""); - return stanza.toString(); - } - } - - /** - * A SASL success stanza. - */ - public static class Success extends Packet { - final private String data; - - public Success(String data) { - this.data = data; - } - - public String toXML() { - StringBuilder stanza = new StringBuilder(); - stanza.append(""); - if (data != null && - data.trim().length() > 0) { - stanza.append(data); - } - stanza.append(""); - return stanza.toString(); - } - } - - /** - * A SASL failure stanza. - */ - public static class SASLFailure extends Packet { - private final SASLError saslError; - private final String saslErrorString; - - public SASLFailure(String saslError) { - SASLError error = SASLError.fromString(saslError); - if (error == null) { - // RFC6120 6.5 states that unknown condition must be treat as generic authentication failure. - this.saslError = SASLError.not_authorized; - } else { - this.saslError = error; - } - this.saslErrorString = saslError; - } - - /** - * Get the SASL related error condition. - * - * @return the SASL related error condition. - */ - public SASLError getSASLError() { - return saslError; - } - - /** - * - * @return the SASL error as String - */ - public String getSASLErrorString() { - return saslErrorString; - } - - public String toXML() { - StringBuilder stanza = new StringBuilder(); - stanza.append(""); - stanza.append("<").append(saslErrorString).append("/>"); - stanza.append(""); - return stanza.toString(); - } - } + protected abstract SASLMechanism newInstance(); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStanzas.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStanzas.java new file mode 100644 index 000000000..689fa4741 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStanzas.java @@ -0,0 +1,186 @@ +/** + * + * 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.sasl.packet; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.sasl.SASLError; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class SaslStanzas { + public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl"; + + /** + * Initiating SASL authentication by select a mechanism. + */ + public static class AuthMechanism extends Packet { + public static final String ELEMENT = "auth"; + + private final String mechanism; + private final String authenticationText; + + public AuthMechanism(String mechanism, String authenticationText) { + if (mechanism == null) { + throw new NullPointerException("SASL mechanism shouldn't be null."); + } + this.mechanism = mechanism; + this.authenticationText = StringUtils.returnIfNotEmptyTrimmed(authenticationText); + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(ELEMENT).xmlnsAttribute(NAMESPACE).attribute("mechanism", mechanism).rightAngelBracket(); + xml.optAppend(authenticationText); + xml.closeElement(ELEMENT); + return xml; + } + } + + /** + * A SASL challenge stanza. + */ + public static class Challenge extends Packet { + public static final String ELEMENT = "challenge"; + + private final String data; + + public Challenge(String data) { + this.data = StringUtils.returnIfNotEmptyTrimmed(data); + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder().halfOpenElement(ELEMENT).xmlnsAttribute( + NAMESPACE).rightAngelBracket(); + xml.optAppend(data); + xml.closeElement(ELEMENT); + return xml; + } + } + + /** + * A SASL response stanza. + */ + public static class Response extends Packet { + public static final String ELEMENT = "response"; + + private final String authenticationText; + + public Response() { + authenticationText = null; + } + + public Response(String authenticationText) { + this.authenticationText = StringUtils.returnIfNotEmptyTrimmed(authenticationText); + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(ELEMENT).xmlnsAttribute(NAMESPACE).rightAngelBracket(); + xml.optAppend(authenticationText); + xml.closeElement(ELEMENT); + return xml; + } + } + + /** + * A SASL success stanza. + */ + public static class Success extends Packet { + public static final String ELEMENT = "success"; + + final private String data; + + /** + * Construct a new SASL success stanza with optional additional data for the SASL layer + * (RFC6120 6.3.10) + * + * @param data additional data for the SASL layer or null + */ + public Success(String data) { + this.data = StringUtils.returnIfNotEmptyTrimmed(data); + } + + /** + * Returns additional data for the SASL layer or null. + * + * @return additional data or null + */ + public String getData() { + return data; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(ELEMENT).xmlnsAttribute(NAMESPACE).rightAngelBracket(); + xml.optAppend(data); + xml.closeElement(ELEMENT); + return xml; + } + } + + /** + * A SASL failure stanza. + */ + public static class SASLFailure extends Packet { + public static final String ELEMENT = "failure"; + + private final SASLError saslError; + private final String saslErrorString; + + public SASLFailure(String saslError) { + SASLError error = SASLError.fromString(saslError); + if (error == null) { + // RFC6120 6.5 states that unknown condition must be treat as generic authentication + // failure. + this.saslError = SASLError.not_authorized; + } + else { + this.saslError = error; + } + this.saslErrorString = saslError; + } + + /** + * Get the SASL related error condition. + * + * @return the SASL related error condition. + */ + public SASLError getSASLError() { + return saslError; + } + + /** + * @return the SASL error as String + */ + public String getSASLErrorString() { + return saslErrorString; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(ELEMENT).xmlnsAttribute(ELEMENT).rightAngelBracket(); + xml.emptyElement(saslErrorString); + xml.closeElement(ELEMENT); + return xml; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ByteUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ByteUtils.java new file mode 100644 index 000000000..c9f70f5c6 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ByteUtils.java @@ -0,0 +1,48 @@ +/** + * + * 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.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class ByteUtils { + + private static MessageDigest md5Digest; + + public synchronized static byte[] md5(byte[] data) { + if (md5Digest == null) { + try { + md5Digest = MessageDigest.getInstance(StringUtils.MD5); + } + catch (NoSuchAlgorithmException nsae) { + // Smack wont be able to function normally if this exception is thrown, wrap it into + // an ISE and make the user aware of the problem. + throw new IllegalStateException(nsae); + } + } + md5Digest.update(data); + return md5Digest.digest(); + } + + public static byte[] concact(byte[] arrayOne, byte[] arrayTwo) { + int combinedLength = arrayOne.length + arrayTwo.length; + byte[] res = new byte[combinedLength]; + System.arraycopy(arrayOne, 0, res, 0, arrayOne.length); + System.arraycopy(arrayTwo, 0, res, arrayOne.length, arrayTwo.length); + return res; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java index cd2926585..882292650 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java @@ -44,7 +44,7 @@ import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.provider.IQProvider; import org.jivesoftware.smack.provider.PacketExtensionProvider; import org.jivesoftware.smack.provider.ProviderManager; -import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; 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 7044e4b4c..ac5059848 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 @@ -28,6 +28,7 @@ import java.util.Random; */ public class StringUtils { + public static final String MD5 = "MD5"; public static final String SHA1 = "SHA-1"; public static final String UTF8 = "UTF-8"; @@ -37,6 +38,8 @@ public class StringUtils { public static final String LT_ENCODE = "<"; public static final String GT_ENCODE = ">"; + public static final char[] HEX_CHARS = "0123456789abcdef".toCharArray(); + /** * Escapes all necessary characters in the String so that it can be used * in an XML doc. @@ -129,14 +132,7 @@ public class StringUtils { } } // Now, compute hash. - try { - digest.update(data.getBytes(UTF8)); - } - catch (UnsupportedEncodingException e) { - // Smack wont be able to function normally if this exception is thrown, wrap it into an - // ISE and make the user aware of the problem. - throw new IllegalStateException(e); - } + digest.update(toBytes(data)); return encodeHex(digest.digest()); } @@ -147,16 +143,22 @@ public class StringUtils { * @return generated hex string. */ public static String encodeHex(byte[] bytes) { - StringBuilder hex = new StringBuilder(bytes.length * 2); - - for (byte aByte : bytes) { - if (((int) aByte & 0xff) < 0x10) { - hex.append("0"); - } - hex.append(Integer.toString((int) aByte & 0xff, 16)); + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_CHARS[v >>> 4]; + hexChars[j * 2 + 1] = HEX_CHARS[v & 0x0F]; } + return new String(hexChars); + } - return hex.toString(); + public static byte[] toBytes(String string) { + try { + return string.getBytes(StringUtils.UTF8); + } + catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 encoding not supported by platform", e); + } } /** @@ -166,13 +168,7 @@ public class StringUtils { * @return a base64 encoded String. */ public static String encodeBase64(String data) { - byte [] bytes = null; - try { - bytes = data.getBytes("ISO-8859-1"); - } - catch (UnsupportedEncodingException uee) { - throw new IllegalStateException(uee); - } + byte [] bytes = toBytes(data); return encodeBase64(bytes); } @@ -218,17 +214,14 @@ public class StringUtils { * @return the decoded String. */ public static byte[] decodeBase64(String data) { - byte[] bytes; - try { - bytes = data.getBytes("UTF-8"); - } catch (java.io.UnsupportedEncodingException uee) { - bytes = data.getBytes(); - } - - bytes = Base64.decode(bytes, 0, bytes.length, Base64.NO_OPTIONS); - return bytes; + byte[] bytes = toBytes(data); + return decodeBase64(bytes); } + public static byte[] decodeBase64(byte[] data) { + return Base64.decode(data, 0, data.length, Base64.NO_OPTIONS); + } + /** * Pseudo-random number generator object for use with randomString(). * The Random class is not considered to be cryptographically secure, so @@ -306,4 +299,15 @@ public class StringUtils { res = res.substring(0, res.length() - 1); return res; } + + public static String returnIfNotEmptyTrimmed(String string) { + if (string == null) + return null; + String trimmedString = string.trim(); + if (trimmedString.length() > 0) { + return trimmedString; + } else { + return null; + } + } } diff --git a/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml b/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml index 500b048ff..e10003e36 100644 --- a/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml +++ b/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml @@ -12,5 +12,7 @@ org.jivesoftware.smack.initializer.extensions.ExtensionsInitializer org.jivesoftware.smack.initializer.experimental.ExperimentalInitializer org.jivesoftware.smack.initializer.legacy.LegacyInitializer + org.jivesoftware.smack.sasl.javax.SASLJavaXSmackInitializer + org.jivesoftware.smack.sasl.provided.SASLProvidedSmackInitializer diff --git a/smack-core/src/test/java/org/jivesoftware/smack/sasl/AbstractSaslTest.java b/smack-core/src/test/java/org/jivesoftware/smack/sasl/AbstractSaslTest.java new file mode 100644 index 000000000..b22a858bb --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/sasl/AbstractSaslTest.java @@ -0,0 +1,31 @@ +/** + * + * 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.sasl; + +import org.jivesoftware.smack.DummyConnection; +import org.jivesoftware.smack.XMPPConnection; + +public class AbstractSaslTest { + + protected final XMPPConnection xmppConnection = new DummyConnection(); + protected final SASLMechanism saslMechanism; + + protected AbstractSaslTest(SASLMechanism saslMechanism) { + this.saslMechanism = saslMechanism.instanceForAuthentication(xmppConnection); + } + +} diff --git a/smack-core/src/test/java/org/jivesoftware/smack/sasl/DigestMd5SaslTest.java b/smack-core/src/test/java/org/jivesoftware/smack/sasl/DigestMd5SaslTest.java new file mode 100644 index 000000000..e8eb6bad9 --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/sasl/DigestMd5SaslTest.java @@ -0,0 +1,61 @@ +/** + * + * 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.sasl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.util.StringUtils; + +public class DigestMd5SaslTest extends AbstractSaslTest { + + protected static final String challenge = "realm=\"xmpp.org\",nonce=\"aTUr3GXqUtyy2B7HVDW6C+gQs+j+0EhWWjoBKkkg\",qop=\"auth\",charset=utf-8,algorithm=md5-sess"; + protected static final byte[] challengeBytes = StringUtils.toBytes(challenge); + + public DigestMd5SaslTest(SASLMechanism saslMechanism) { + super(saslMechanism); + } + + protected void runTest() throws NotConnectedException, SmackException { + saslMechanism.authenticate("florian", "irrelevant", "xmpp.org", "secret"); + + byte[] response = saslMechanism.evaluateChallenge(challengeBytes); + String responseString = new String(response); + String[] responseParts = responseString.split(","); + Map responsePairs = new HashMap(); + for (String part : responseParts) { + String[] keyValue = part.split("="); + assertTrue(keyValue.length == 2); + String key = keyValue[0]; + String value = keyValue[1].replace("\"", ""); + responsePairs.put(key, value); + } + assertMapValue("username", "florian", responsePairs); + assertMapValue("realm", "xmpp.org", responsePairs); + assertMapValue("digest-uri", "xmpp/xmpp.org", responsePairs); + assertMapValue("qop", "auth", responsePairs); + } + + private static void assertMapValue(String key, String value, Map map) { + assertEquals(map.get(key), value); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/LiteDebugger.java b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/LiteDebugger.java similarity index 99% rename from smack-core/src/main/java/org/jivesoftware/smack/debugger/LiteDebugger.java rename to smack-debug/src/main/java/org/jivesoftware/smackx/debugger/LiteDebugger.java index f17621fb8..f0fd82b4c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/LiteDebugger.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/LiteDebugger.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.jivesoftware.smack.debugger; +package org.jivesoftware.smackx.debugger; import java.awt.*; import java.awt.datatransfer.*; @@ -25,6 +25,7 @@ import java.io.*; import javax.swing.*; import org.jivesoftware.smack.*; +import org.jivesoftware.smack.debugger.SmackDebugger; import org.jivesoftware.smack.packet.*; import org.jivesoftware.smack.util.*; import org.jxmpp.util.XmppStringUtils; diff --git a/smack-java7/build.gradle b/smack-java7/build.gradle index 16efa8d77..6a53eb668 100644 --- a/smack-java7/build.gradle +++ b/smack-java7/build.gradle @@ -11,6 +11,7 @@ dependencies { compile project(":smack-extensions") compile project(":smack-tcp") compile project(":smack-resolver-javax") + compile project(":smack-sasl-javax") } javadoc { diff --git a/smack-sasl-javax/build.gradle b/smack-sasl-javax/build.gradle new file mode 100644 index 000000000..94d1a9e2a --- /dev/null +++ b/smack-sasl-javax/build.gradle @@ -0,0 +1,8 @@ +description = """\ +SASL with javax.security.sasl +Use javax.security.sasl for SASL.""" + +dependencies { + compile project(path: ':smack-core', configuration: 'sasl') + testCompile project(':smack-core').sourceSets.test.runtimeClasspath +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLCramMD5Mechanism.java similarity index 65% rename from smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java rename to smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLCramMD5Mechanism.java index 1e75be788..f6822e30a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java +++ b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLCramMD5Mechanism.java @@ -14,22 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smack.sasl; - -import org.jivesoftware.smack.SASLAuthentication; +package org.jivesoftware.smack.sasl.javax; /** * Implementation of the SASL CRAM-MD5 mechanism * * @author Jay Kline */ -public class SASLCramMD5Mechanism extends SASLMechanism { +public class SASLCramMD5Mechanism extends SASLJavaXMechanism { - public SASLCramMD5Mechanism(SASLAuthentication saslAuthentication) { - super(saslAuthentication); + public static final String NAME = CRAMMD5; + + public String getName() { + return NAME; } - protected String getName() { - return "CRAM-MD5"; + @Override + public int getPriority() { + return 300; + } + + @Override + public SASLCramMD5Mechanism newInstance() { + return new SASLCramMD5Mechanism(); } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Mechanism.java similarity index 64% rename from smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java rename to smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Mechanism.java index 9c7bb115a..c4959807b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java +++ b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Mechanism.java @@ -14,22 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smack.sasl; - -import org.jivesoftware.smack.SASLAuthentication; +package org.jivesoftware.smack.sasl.javax; /** * Implementation of the SASL DIGEST-MD5 mechanism * * @author Jay Kline */ -public class SASLDigestMD5Mechanism extends SASLMechanism { +public class SASLDigestMD5Mechanism extends SASLJavaXMechanism { - public SASLDigestMD5Mechanism(SASLAuthentication saslAuthentication) { - super(saslAuthentication); + public static final String NAME = DIGESTMD5; + + public String getName() { + return NAME; } - protected String getName() { - return "DIGEST-MD5"; + @Override + public int getPriority() { + return 200; + } + + @Override + public SASLDigestMD5Mechanism newInstance() { + return new SASLDigestMD5Mechanism(); } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLExternalMechanism.java b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLExternalMechanism.java similarity index 81% rename from smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLExternalMechanism.java rename to smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLExternalMechanism.java index f45ce5203..eff4697c4 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLExternalMechanism.java +++ b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLExternalMechanism.java @@ -14,9 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smack.sasl; - -import org.jivesoftware.smack.SASLAuthentication; +package org.jivesoftware.smack.sasl.javax; /** * Implementation of the SASL EXTERNAL mechanism. @@ -44,13 +42,22 @@ import org.jivesoftware.smack.SASLAuthentication; * * @author Jay Kline */ -public class SASLExternalMechanism extends SASLMechanism { +public class SASLExternalMechanism extends SASLJavaXMechanism { - public SASLExternalMechanism(SASLAuthentication saslAuthentication) { - super(saslAuthentication); + public static final String NAME = EXTERNAL; + + @Override + public String getName() { + return EXTERNAL; } - protected String getName() { - return "EXTERNAL"; + @Override + public int getPriority() { + return 500; + } + + @Override + public SASLExternalMechanism newInstance() { + return new SASLExternalMechanism(); } } diff --git a/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLGSSAPIMechanism.java b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLGSSAPIMechanism.java new file mode 100644 index 000000000..ae45cbd47 --- /dev/null +++ b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLGSSAPIMechanism.java @@ -0,0 +1,69 @@ +/** + * + * Copyright the original author or authors + * + * 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.sasl.javax; + +import java.util.Map; + +import javax.security.sasl.Sasl; + +/** + * Implementation of the SASL GSSAPI mechanism + * + * @author Jay Kline + */ +public class SASLGSSAPIMechanism extends SASLJavaXMechanism { + + public static final String NAME = GSSAPI; + + static { + System.setProperty("javax.security.auth.useSubjectCredsOnly","false"); + System.setProperty("java.security.auth.login.config","gss.conf"); + } + + @Override + public String getName() { + return NAME; + } + + @Override + protected Map getSaslProps() { + Map props = super.getSaslProps(); + props.put(Sasl.SERVER_AUTH,"TRUE"); + return props; + } + + /** + * GSSAPI differs from all other SASL mechanism such that it required the FQDN host name as + * server name and not the serviceName (At least that is what old code comments of Smack tell + * us). + */ + @Override + protected String getServerName() { + return host; + } + + @Override + public int getPriority() { + return 100; + } + + @Override + public SASLGSSAPIMechanism newInstance() { + return new SASLGSSAPIMechanism(); + } + +} diff --git a/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXMechanism.java b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXMechanism.java new file mode 100644 index 000000000..c19289bdd --- /dev/null +++ b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXMechanism.java @@ -0,0 +1,147 @@ +/** + * + * 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.sasl.javax; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.RealmCallback; +import javax.security.sasl.RealmChoiceCallback; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.sasl.SASLMechanism; +import org.jivesoftware.smack.util.StringUtils; + +public abstract class SASLJavaXMechanism extends SASLMechanism { + + protected SaslClient sc; + + @Override + public abstract String getName(); + + @Override + protected void authenticateInternal() + throws SmackException { + String[] mechanisms = { getName() }; + Map props = getSaslProps(); + try { + sc = Sasl.createSaslClient(mechanisms, null, "xmpp", getServerName(), props, + new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, + UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback) callbacks[i]; + ncb.setName(authenticationId); + } + else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback) callbacks[i]; + pcb.setPassword(password.toCharArray()); + } + else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback) callbacks[i]; + // Retrieve the REALM from the challenge response that + // the server returned when the client initiated the + // authentication exchange. If this value is not null or + // empty, *this value* has to be sent back to the server + // in the client's response to the server's challenge + String text = rcb.getDefaultText(); + // The SASL client (sc) created in smack uses + // rcb.getText when creating the negotiatedRealm to send + // it back to the server. Make sure that this value + // matches the server's realm + rcb.setText(text); + } + else if (callbacks[i] instanceof RealmChoiceCallback) { + // unused, prevents UnsupportedCallbackException + // RealmChoiceCallback rccb = + // (RealmChoiceCallback)callbacks[i]; + } + else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + } + + }); + } + catch (SaslException e) { + throw new SmackException(e); + } + } + + @Override + protected void authenticateInternal(CallbackHandler cbh) + throws SmackException { + String[] mechanisms = { getName() }; + Map props = getSaslProps(); + try { + sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh); + } + catch (SaslException e) { + throw new SmackException(e); + } + } + + protected String getAuthenticationText() throws SmackException { + String authenticationText = null; + if (sc.hasInitialResponse()) { + byte[] response; + try { + response = sc.evaluateChallenge(new byte[0]); + } + catch (SaslException e) { + throw new SmackException(e); + } + authenticationText = StringUtils.encodeBase64(response, false); + } + return authenticationText; + } + + @Override + protected byte[] evaluateChallenge(byte[] challenge) throws SmackException { + try { + if (challenge != null) { + return sc.evaluateChallenge(challenge); + } + else { + return sc.evaluateChallenge(new byte[0]); + } + } + catch (SaslException e) { + throw new SmackException(e); + } + } + + protected Map getSaslProps() { + return new HashMap(); + } + + protected String getServerName() { + return serviceName; + } +} diff --git a/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXSmackInitializer.java b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXSmackInitializer.java new file mode 100644 index 000000000..735fe214e --- /dev/null +++ b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXSmackInitializer.java @@ -0,0 +1,42 @@ +/** + * + * 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.sasl.javax; + +import java.util.List; + +import org.jivesoftware.smack.SASLAuthentication; +import org.jivesoftware.smack.initializer.SmackAndOsgiInitializer; + +public class SASLJavaXSmackInitializer extends SmackAndOsgiInitializer { + + @Override + public List initialize() { + SASLAuthentication.registerSASLMechanism(new SASLExternalMechanism()); + SASLAuthentication.registerSASLMechanism(new SASLGSSAPIMechanism()); + SASLAuthentication.registerSASLMechanism(new SASLDigestMD5Mechanism()); + SASLAuthentication.registerSASLMechanism(new SASLCramMD5Mechanism()); + SASLAuthentication.registerSASLMechanism(new SASLPlainMechanism()); + + return null; + } + + @Override + public List initialize(ClassLoader classLoader) { + return initialize(); + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLPlainMechanism.java b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLPlainMechanism.java similarity index 65% rename from smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLPlainMechanism.java rename to smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLPlainMechanism.java index e80f2069f..4f02de9c0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLPlainMechanism.java +++ b/smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLPlainMechanism.java @@ -14,22 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smack.sasl; - -import org.jivesoftware.smack.SASLAuthentication; +package org.jivesoftware.smack.sasl.javax; /** * Implementation of the SASL PLAIN mechanism * * @author Jay Kline */ -public class SASLPlainMechanism extends SASLMechanism { +public class SASLPlainMechanism extends SASLJavaXMechanism { - public SASLPlainMechanism(SASLAuthentication saslAuthentication) { - super(saslAuthentication); + public static final String NAME = PLAIN; + + public String getName() { + return NAME; } - protected String getName() { - return "PLAIN"; + @Override + public int getPriority() { + return 400; + } + + @Override + public SASLPlainMechanism newInstance() { + return new SASLPlainMechanism(); } } diff --git a/smack-sasl-javax/src/main/resources/org.jivesoftware.smack/smack-sasl-javax-components.xml b/smack-sasl-javax/src/main/resources/org.jivesoftware.smack/smack-sasl-javax-components.xml new file mode 100644 index 000000000..ebbd5403e --- /dev/null +++ b/smack-sasl-javax/src/main/resources/org.jivesoftware.smack/smack-sasl-javax-components.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/smack-sasl-javax/src/test/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Test.java b/smack-sasl-javax/src/test/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Test.java new file mode 100644 index 000000000..3b1659bf2 --- /dev/null +++ b/smack-sasl-javax/src/test/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Test.java @@ -0,0 +1,34 @@ +/** + * + * 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.sasl.javax; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.sasl.DigestMd5SaslTest; +import org.junit.Test; + +public class SASLDigestMD5Test extends DigestMd5SaslTest { + + public SASLDigestMD5Test() { + super(new SASLDigestMD5Mechanism()); + } + + @Test + public void testDigestMD5() throws NotConnectedException, SmackException { + runTest(); + } +} diff --git a/smack-sasl-provided/build.gradle b/smack-sasl-provided/build.gradle new file mode 100644 index 000000000..7b3e33824 --- /dev/null +++ b/smack-sasl-provided/build.gradle @@ -0,0 +1,8 @@ +description = """\ +SASL with Smack provided code +Use Smack provided code for SASL.""" + +dependencies { + compile project(path: ':smack-core', configuration: 'sasl') + testCompile project(':smack-core').sourceSets.test.runtimeClasspath +} diff --git a/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java b/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java new file mode 100644 index 000000000..72bc8dc7a --- /dev/null +++ b/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java @@ -0,0 +1,206 @@ +/** + * + * 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.sasl.provided; + +import javax.security.auth.callback.CallbackHandler; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.sasl.SASLMechanism; +import org.jivesoftware.smack.util.ByteUtils; +import org.jivesoftware.smack.util.StringUtils; + +public class SASLDigestMD5Mechanism extends SASLMechanism { + + public static final String NAME = DIGESTMD5; + + private static final String INITAL_NONCE = "00000001"; + + /** + * The only 'qop' value supported by this implementation + */ + private static final String QOP_VALUE = "auth"; + + private enum State { + INITIAL, + RESPONSE_SENT, + VALID_SERVER_RESPONSE, + } + + private static boolean verifyServerResponse = true; + + public static void setVerifyServerResponse(boolean verifyServerResponse) { + SASLDigestMD5Mechanism.verifyServerResponse = verifyServerResponse; + } + + /** + * The state of the this instance of SASL DIGEST-MD5 authentication. + */ + private State state = State.INITIAL; + + private String nonce; + private String cnonce; + private String digestUri; + private String hex_hashed_a1; + + @Override + protected void authenticateInternal(CallbackHandler cbh) throws SmackException { + throw new UnsupportedOperationException("CallbackHandler not (yet) supported"); + } + + @Override + protected String getAuthenticationText() throws SmackException { + // DIGEST-MD5 has no initial response, return null + return null; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public int getPriority() { + return 210; + } + + @Override + public SASLDigestMD5Mechanism newInstance() { + return new SASLDigestMD5Mechanism(); + } + + @Override + protected byte[] evaluateChallenge(byte[] challenge) throws SmackException { + if (challenge.length == 0) { + throw new SmackException("Initial challenge has zero length"); + } + String[] challengeParts = (new String(challenge)).split(","); + byte[] response = null; + switch (state) { + case INITIAL: + for (String part : challengeParts) { + String[] keyValue = part.split("="); + assert (keyValue.length == 2); + String key = keyValue[0]; + String value = keyValue[1]; + if ("nonce".equals(key)) { + if (nonce != null) { + throw new SmackException("Nonce value present multiple times"); + } + nonce = value.replace("\"", ""); + } + else if ("qop".equals(key)) { + value = value.replace("\"", ""); + if (!value.equals("auth")) { + throw new SmackException("Unsupported qop operation: " + value); + } + } + } + if (nonce == null) { + // RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly + // once; if not present, or if multiple instances are present, the client should + // abort the authentication exchange." + throw new SmackException("nonce value not present in initial challenge"); + } + // RFC 2831 2.1.2.1 defines A1, A2, KD and response-value + byte[] a1FirstPart = ByteUtils.md5(toBytes(authenticationId + ':' + serviceName + ':' + + password)); + cnonce = StringUtils.randomString(32); + byte[] a1 = ByteUtils.concact(a1FirstPart, toBytes(':' + nonce + ':' + cnonce)); + digestUri = "xmpp/" + serviceName; + hex_hashed_a1 = StringUtils.encodeHex(ByteUtils.md5(a1)); + String responseValue = calcResponse(DigestType.ClientResponse); + // @formatter:off + // See RFC 2831 2.1.2 digest-response + String saslString = "username=\"" + authenticationId + '"' + + ",realm=\"" + serviceName + '"' + + ",nonce=\"" + nonce + '"' + + ",cnonce=\"" + cnonce + '"' + + ",nc=" + INITAL_NONCE + + ",qop=auth" + + ",digest-uri=\"" + digestUri + '"' + + ",response=" + responseValue + + ",charset=utf-8"; + // @formatter:on + response = toBytes(saslString); + state = State.RESPONSE_SENT; + break; + case RESPONSE_SENT: + if (verifyServerResponse) { + String serverResponse = null; + for (String part : challengeParts) { + String[] keyValue = part.split("="); + assert (keyValue.length == 2); + String key = keyValue[0]; + String value = keyValue[1]; + if ("rspauth".equals(key)) { + serverResponse = value; + break; + } + } + if (serverResponse == null) { + throw new SmackException("No server response received while performing " + NAME + + " authentication"); + } + String expectedServerResponse = calcResponse(DigestType.ServerResponse); + if (!serverResponse.equals(expectedServerResponse)) { + throw new SmackException("Invalid server response while performing " + NAME + + " authentication"); + } + } + state = State.VALID_SERVER_RESPONSE; + break; + default: + throw new IllegalStateException(); + } + return response; + } + + private enum DigestType { + ClientResponse, + ServerResponse + } + + private String calcResponse(DigestType digestType) { + StringBuilder a2 = new StringBuilder(); + if (digestType == DigestType.ClientResponse) { + a2.append("AUTHENTICATE"); + } + a2.append(':'); + a2.append(digestUri); + String hex_hashed_a2 = StringUtils.encodeHex(ByteUtils.md5(toBytes(a2.toString()))); + + StringBuilder kd_argument = new StringBuilder(); + kd_argument.append(hex_hashed_a1); + kd_argument.append(':'); + kd_argument.append(nonce); + kd_argument.append(':'); + kd_argument.append(INITAL_NONCE); + kd_argument.append(':'); + kd_argument.append(cnonce); + kd_argument.append(':'); + kd_argument.append(QOP_VALUE); + kd_argument.append(':'); + kd_argument.append(hex_hashed_a2); + byte[] kd = ByteUtils.md5(toBytes(kd_argument.toString())); + String responseValue = StringUtils.encodeHex(kd); + return responseValue; + } + + private static byte[] toBytes(String string) { + return StringUtils.toBytes(string); + } +} diff --git a/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLProvidedSmackInitializer.java b/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLProvidedSmackInitializer.java new file mode 100644 index 000000000..eb7eb920e --- /dev/null +++ b/smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLProvidedSmackInitializer.java @@ -0,0 +1,37 @@ +/** + * + * 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.sasl.provided; + +import java.util.List; + +import org.jivesoftware.smack.SASLAuthentication; +import org.jivesoftware.smack.initializer.SmackAndOsgiInitializer; + +public class SASLProvidedSmackInitializer extends SmackAndOsgiInitializer { + + @Override + public List initialize() { + SASLAuthentication.registerSASLMechanism(new SASLDigestMD5Mechanism()); + return null; + } + + @Override + public List initialize(ClassLoader classLoader) { + return initialize(); + } + +} diff --git a/smack-sasl-provided/src/main/resources/org.jivesoftware.smack/smack-sasl-provided-components.xml b/smack-sasl-provided/src/main/resources/org.jivesoftware.smack/smack-sasl-provided-components.xml new file mode 100644 index 000000000..ee995e18b --- /dev/null +++ b/smack-sasl-provided/src/main/resources/org.jivesoftware.smack/smack-sasl-provided-components.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/smack-sasl-provided/src/test/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Test.java b/smack-sasl-provided/src/test/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Test.java new file mode 100644 index 000000000..6684ac248 --- /dev/null +++ b/smack-sasl-provided/src/test/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Test.java @@ -0,0 +1,34 @@ +/** + * + * 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.sasl.provided; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.sasl.DigestMd5SaslTest; +import org.jivesoftware.smack.sasl.provided.SASLDigestMD5Mechanism; +import org.junit.Test; + +public class SASLDigestMD5Test extends DigestMd5SaslTest { + public SASLDigestMD5Test() { + super(new SASLDigestMD5Mechanism()); + } + + @Test + public void testDigestMD5() throws NotConnectedException, SmackException { + runTest(); + } +} 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 d59a53c3c..f29c8bcf1 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 @@ -35,9 +35,9 @@ import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.parsing.ParsingExceptionCallback; import org.jivesoftware.smack.parsing.UnparsablePacket; -import org.jivesoftware.smack.sasl.SASLMechanism.Challenge; -import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure; -import org.jivesoftware.smack.sasl.SASLMechanism.Success; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.Challenge; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success; import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.TLSUtils; @@ -54,7 +54,6 @@ import javax.net.ssl.SSLSocket; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.PasswordCallback; -import javax.security.sasl.SaslException; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -247,7 +246,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } @Override - public synchronized void login(String username, String password, String resource) throws XMPPException, SmackException, SaslException, IOException { + public synchronized void login(String username, String password, String resource) throws XMPPException, SmackException, IOException { if (!isConnected()) { throw new NotConnectedException(); } @@ -266,7 +265,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { saslAuthentication.authenticate(resource, config.getCallbackHandler()); } } else { - throw new SaslException("No non-anonymous SASL authentication mechanism available"); + throw new SmackException("No non-anonymous SASL authentication mechanism available"); } // If compression is enabled then request the server to use stream compression. XEP-170 @@ -312,7 +311,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } @Override - public synchronized void loginAnonymously() throws XMPPException, SmackException, SaslException, IOException { + public synchronized void loginAnonymously() throws XMPPException, SmackException, IOException { if (!isConnected()) { throw new NotConnectedException(); } @@ -324,7 +323,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { saslAuthentication.authenticateAnonymously(); } else { - throw new SaslException("No anonymous SASL authentication mechanism available"); + throw new SmackException("No anonymous SASL authentication mechanism available"); } String response = bindResourceAndEstablishSession(null); @@ -1063,7 +1062,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { getSASLAuthentication().challengeReceived(challengeData); } else if (name.equals("success")) { - processPacket(new Success(parser.nextText())); + Success success = new Success(parser.nextText()); + processPacket(success); // We now need to bind a resource for the connection // Open a new stream and wait for the response packetWriter.openStream(); @@ -1072,7 +1072,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { resetParser(); // The SASL authentication with the server was successful. The next step // will be to bind the resource - getSASLAuthentication().authenticated(); + getSASLAuthentication().authenticated(success); } else if (name.equals("compressed")) { // Server confirmed that it's possible to use stream compression. Start