1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-11-22 03:52:06 +01:00

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.
This commit is contained in:
Florian Schmaus 2014-08-01 10:34:47 +02:00
parent 5a2149718a
commit 89dc3a0e85
39 changed files with 1562 additions and 675 deletions

View file

@ -1 +1,10 @@
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

View file

@ -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 {

View file

@ -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-android',
'smack-java7'

View file

@ -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 }

View file

@ -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.<p>
* <p/>
* 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 <tt>printInterpreted</tt> static variable to <tt>true</tt>.
*
*/
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;
}
}

View file

@ -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")) {

View file

@ -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 {

View file

@ -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

View file

@ -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;
/**
* <p>This class is responsible authenticating the user using SASL, binding the resource
* to the connection and establishing a session with the server.</p>
*
* <p>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.</p>
* register with the server or authenticate using SASL. If the
* server supports SASL then Smack will try to authenticate using SASL..</p>
*
* <p>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}. </p>
*
* <p>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.</p>
*
* <p>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.</p>
* {@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<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();
private static List<String> mechanismsPreferences = new ArrayList<String>();
private static final List<SASLMechanism> REGISTERED_MECHANISMS = new ArrayList<SASLMechanism>();
private XMPPConnection connection;
private static final Set<String> BLACKLISTED_MECHANISMS = new HashSet<String>();
/**
* 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<String, String> getRegisterdSASLMechanisms() {
Map<String, String> answer = new HashMap<String, String>();
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<SASLMechanism> 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<String> getBlacklistedSASLMechanisms() {
synchronized(BLACKLISTED_MECHANISMS) {
return new HashSet<String>(BLACKLISTED_MECHANISMS);
}
}
private final AbstractXMPPConnection connection;
private Collection<String> serverMechanisms = new ArrayList<String>();
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<? extends SASLMechanism> 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 <tt>index</tt> 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<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {
List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();
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<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
Constructor<? extends SASLMechanism> 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<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
try {
Constructor<? extends SASLMechanism> 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.
* Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set
* to <code>false</code>.
*
* @return true if the user was able to authenticate with the server usins SASL.
* @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 <success/> 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 <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a>
*/
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<SASLMechanism> 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;
}
}

View file

@ -432,7 +432,7 @@ public final class SmackConfiguration {
if (SmackInitializer.class.isAssignableFrom(initClass)) {
SmackInitializer initializer = (SmackInitializer) initClass.newInstance();
List<Exception> 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) {

View file

@ -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();
}
}

View file

@ -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 {

View file

@ -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 <tt>auth</tt> 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<String,String> props = new HashMap<String,String>();
props.put(Sasl.SERVER_AUTH,"TRUE");
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh);
authenticate();
}
/**
* Builds and sends the <tt>auth</tt> 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<String,String> props = new HashMap<String, String>();
props.put(Sasl.SERVER_AUTH,"TRUE");
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, this);
authenticate();
}
}

View file

@ -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:
* <li>{@link #authenticate(String, String, String, String)} -- Initiate authentication stanza using the
* deprecated method.</li>
* <li>{@link #authenticate(String, CallbackHandler)} -- Initiate authentication stanza
* <li>{@link #authenticate(String, String, CallbackHandler)} -- Initiate authentication stanza
* using the CallbackHandler method.</li>
* <li>{@link #challengeReceived(String)} -- Handle a challenge from the server.</li>
* <li>{@link #challengeReceived(String, boolean)} -- Handle a challenge from the server.</li>
* </ul>
*
* 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<SASLMechanism> {
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 <tt>auth</tt> 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<String,String> props = new HashMap<String,String>();
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", serviceName, props, this);
authenticateInternal();
authenticate();
}
protected void authenticateInternal() throws SmackException {
}
/**
* Builds and sends the <tt>auth</tt> 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<String,String> props = new HashMap<String,String>();
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
connection.sendPacket(new AuthMechanism(getName(), authenticationText));
}
// Send the authentication to the server
getSASLAuthentication().send(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.
* @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("<auth mechanism=\"").append(name);
stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (authenticationText != null &&
authenticationText.trim().length() > 0) {
stanza.append(authenticationText);
}
stanza.append("</auth>");
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("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (data != null &&
data.trim().length() > 0) {
stanza.append(data);
}
stanza.append("</challenge>");
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("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (authenticationText != null) {
stanza.append(authenticationText);
}
stanza.append("</response>");
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("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (data != null &&
data.trim().length() > 0) {
stanza.append(data);
}
stanza.append("</success>");
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("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
stanza.append("<").append(saslErrorString).append("/>");
stanza.append("</failure>");
return stanza.toString();
}
}
protected abstract SASLMechanism newInstance();
}

View file

@ -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 <code>null</code>
*/
public Success(String data) {
this.data = StringUtils.returnIfNotEmptyTrimmed(data);
}
/**
* Returns additional data for the SASL layer or <code>null</code>.
*
* @return additional data or <code>null</code>
*/
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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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 = "&lt;";
public static final String GT_ENCODE = "&gt;";
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");
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];
}
hex.append(Integer.toString((int) aByte & 0xff, 16));
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,15 +214,12 @@ 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();
byte[] bytes = toBytes(data);
return decodeBase64(bytes);
}
bytes = Base64.decode(bytes, 0, bytes.length, Base64.NO_OPTIONS);
return bytes;
public static byte[] decodeBase64(byte[] data) {
return Base64.decode(data, 0, data.length, Base64.NO_OPTIONS);
}
/**
@ -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;
}
}
}

View file

@ -12,5 +12,7 @@
<className>org.jivesoftware.smack.initializer.extensions.ExtensionsInitializer</className>
<className>org.jivesoftware.smack.initializer.experimental.ExperimentalInitializer</className>
<className>org.jivesoftware.smack.initializer.legacy.LegacyInitializer</className>
<className>org.jivesoftware.smack.sasl.javax.SASLJavaXSmackInitializer</className>
<className>org.jivesoftware.smack.sasl.provided.SASLProvidedSmackInitializer</className>
</optionalStartupClasses>
</smack>

View file

@ -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);
}
}

View file

@ -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<String, String> responsePairs = new HashMap<String, String>();
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<String, String> map) {
assertEquals(map.get(key), value);
}
}

View file

@ -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;

View file

@ -11,6 +11,7 @@ dependencies {
compile project(":smack-extensions")
compile project(":smack-tcp")
compile project(":smack-resolver-javax")
compile project(":smack-sasl-javax")
}
javadoc {

View file

@ -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
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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<String, String> getSaslProps() {
Map<String, String> 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();
}
}

View file

@ -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<String, String> 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<String, String> 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<String,String> getSaslProps() {
return new HashMap<String, String>();
}
protected String getServerName() {
return serviceName;
}
}

View file

@ -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<Exception> 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<Exception> initialize(ClassLoader classLoader) {
return initialize();
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.2.0"
enabled="true" immediate="true" name="Smack Resolver JavaX API">
<implementation
class="org.jivesoftware.smack.sasl.javax.SASLJavaXSmackInitializer" />
</scr:component>

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -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<Exception> initialize() {
SASLAuthentication.registerSASLMechanism(new SASLDigestMD5Mechanism());
return null;
}
@Override
public List<Exception> initialize(ClassLoader classLoader) {
return initialize();
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.2.0"
enabled="true" immediate="true" name="Smack Resolver JavaX API">
<implementation
class="org.jivesoftware.smack.sasl.provided.SASLProvidedSmackInitializer" />
</scr:component>

View file

@ -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();
}
}

View file

@ -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