mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-09-27 18:19:33 +02:00
373 lines
15 KiB
Java
373 lines
15 KiB
Java
/**
|
|
*
|
|
* Copyright 2003-2007 Jive Software, 2014-2021 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 java.text.Normalizer;
|
|
import java.text.Normalizer.Form;
|
|
|
|
import javax.net.ssl.SSLSession;
|
|
import javax.security.auth.callback.CallbackHandler;
|
|
|
|
import org.jivesoftware.smack.ConnectionConfiguration;
|
|
import org.jivesoftware.smack.SmackException.NoResponseException;
|
|
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
|
import org.jivesoftware.smack.SmackException.SmackSaslException;
|
|
import org.jivesoftware.smack.XMPPConnection;
|
|
import org.jivesoftware.smack.sasl.packet.SaslNonza.AuthMechanism;
|
|
import org.jivesoftware.smack.sasl.packet.SaslNonza.Response;
|
|
import org.jivesoftware.smack.util.StringUtils;
|
|
import org.jivesoftware.smack.util.stringencoder.Base64;
|
|
|
|
import org.jxmpp.jid.DomainBareJid;
|
|
import org.jxmpp.jid.EntityBareJid;
|
|
|
|
/**
|
|
* Base class for SASL mechanisms.
|
|
* Subclasses will likely want to implement their own versions of these methods:
|
|
* <ul>
|
|
* <li>{@link #authenticate(String, String, DomainBareJid, String, EntityBareJid, SSLSession)} -- Initiate authentication stanza using the
|
|
* deprecated method.</li>
|
|
* <li>{@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} -- Initiate authentication stanza
|
|
* using the CallbackHandler method.</li>
|
|
* <li>{@link #challengeReceived(String, boolean)} -- Handle a challenge from the server.</li>
|
|
* </ul>
|
|
*
|
|
* @author Jay Kline
|
|
* @author Florian Schmaus
|
|
*/
|
|
public abstract class SASLMechanism implements Comparable<SASLMechanism> {
|
|
|
|
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";
|
|
|
|
/**
|
|
* Boolean indicating if SASL negotiation has finished and was successful.
|
|
*/
|
|
private boolean authenticationSuccessful;
|
|
|
|
/**
|
|
* Either of type {@link SmackSaslException},{@link SASLErrorException}, {@link NotConnectedException} or
|
|
* {@link InterruptedException}.
|
|
*/
|
|
private Exception exception;
|
|
|
|
protected XMPPConnection connection;
|
|
|
|
protected ConnectionConfiguration connectionConfiguration;
|
|
|
|
/**
|
|
* Then authentication identity (authcid). RFC 6120 § 6.3.7 informs us that some SASL mechanisms use this as a
|
|
* "simple user name". But the exact form is a matter of the mechanism and that it does not necessarily map to an
|
|
* localpart. But it usually is the localpart of the client JID, although sometimes other formats are used (e.g. the
|
|
* full JID).
|
|
* <p>
|
|
* Not to be confused with the authzid (see RFC 6120 § 6.3.8).
|
|
* </p>
|
|
*/
|
|
protected String authenticationId;
|
|
|
|
/**
|
|
* The authorization identifier (authzid).
|
|
* This is always a bare Jid, but can be null.
|
|
*/
|
|
protected EntityBareJid authorizationId;
|
|
|
|
/**
|
|
* The name of the XMPP service
|
|
*/
|
|
protected DomainBareJid serviceName;
|
|
|
|
/**
|
|
* The users password
|
|
*/
|
|
protected String password;
|
|
protected String host;
|
|
|
|
/**
|
|
* The used SSL/TLS session (if any).
|
|
*/
|
|
protected SSLSession sslSession;
|
|
|
|
/**
|
|
* Builds and sends the <code>auth</code> stanza to the server. Note that this method of
|
|
* authentication is not recommended, since it is very inflexible. Use
|
|
* {@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} whenever possible.
|
|
*
|
|
* Explanation of auth stanza:
|
|
*
|
|
* 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 ]
|
|
*
|
|
* digest-uri:
|
|
* Indicates the principal name of the service with which the client
|
|
* wishes to connect, formed from the serv-type, host, and serv-name.
|
|
* For example, the FTP service
|
|
* on "ftp.example.com" would have a "digest-uri" value of "ftp/ftp.example.com"; the SMTP
|
|
* server from the example above would have a "digest-uri" value of
|
|
* "smtp/mail3.example.com/example.com".
|
|
*
|
|
* host:
|
|
* The DNS host name or IP address for the service requested. The DNS host name
|
|
* must be the fully-qualified canonical name of the host. The DNS host name is the
|
|
* preferred form; see notes on server processing of the digest-uri.
|
|
*
|
|
* serv-name:
|
|
* Indicates the name of the service if it is replicated. The service is
|
|
* considered to be replicated if the client's service-location process involves resolution
|
|
* using standard DNS lookup operations, and if these operations involve DNS records (such
|
|
* as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case,
|
|
* the initial name used by the client is the "serv-name", and the final name is the "host"
|
|
* component. For example, the incoming mail service for "example.com" may be replicated
|
|
* through the use of MX records stored in the DNS, one of which points at an SMTP server
|
|
* called "mail3.example.com"; it's "serv-name" would be "example.com", it's "host" would be
|
|
* "mail3.example.com". If the service is not replicated, or the serv-name is identical to
|
|
* the host, then the serv-name component MUST be omitted
|
|
*
|
|
* digest-uri verification is needed for ejabberd 2.0.3 and higher
|
|
*
|
|
* @param username the username of the user being authenticated.
|
|
* @param host the hostname where the user account resides.
|
|
* @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.
|
|
* @param authzid the optional authorization identity.
|
|
* @param sslSession the optional SSL/TLS session (if one was established)
|
|
* @throws SmackSaslException if a SASL related error occurs.
|
|
* @throws NotConnectedException if the XMPP connection is not connected.
|
|
* @throws InterruptedException if the calling thread was interrupted.
|
|
*/
|
|
public final void authenticate(String username, String host, DomainBareJid serviceName, String password,
|
|
EntityBareJid authzid, SSLSession sslSession)
|
|
throws SmackSaslException, NotConnectedException, InterruptedException {
|
|
this.authenticationId = username;
|
|
this.host = host;
|
|
this.serviceName = serviceName;
|
|
this.password = password;
|
|
this.authorizationId = authzid;
|
|
this.sslSession = sslSession;
|
|
assert authorizationId == null || authzidSupported();
|
|
authenticateInternal();
|
|
authenticate();
|
|
}
|
|
|
|
protected void authenticateInternal() throws SmackSaslException {
|
|
}
|
|
|
|
/**
|
|
* Builds and sends the <code>auth</code> 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.
|
|
* @param authzid the optional authorization identity.
|
|
* @param sslSession the optional SSL/TLS session (if one was established)
|
|
* @throws SmackSaslException if a SASL related error occurs.
|
|
* @throws NotConnectedException if the XMPP connection is not connected.
|
|
* @throws InterruptedException if the calling thread was interrupted.
|
|
*/
|
|
public void authenticate(String host, DomainBareJid serviceName, CallbackHandler cbh, EntityBareJid authzid, SSLSession sslSession)
|
|
throws SmackSaslException, NotConnectedException, InterruptedException {
|
|
this.host = host;
|
|
this.serviceName = serviceName;
|
|
this.authorizationId = authzid;
|
|
this.sslSession = sslSession;
|
|
assert authorizationId == null || authzidSupported();
|
|
authenticateInternal(cbh);
|
|
authenticate();
|
|
}
|
|
|
|
protected abstract void authenticateInternal(CallbackHandler cbh) throws SmackSaslException;
|
|
|
|
private void authenticate() throws SmackSaslException, NotConnectedException, InterruptedException {
|
|
byte[] authenticationBytes = getAuthenticationText();
|
|
String authenticationText;
|
|
// Some SASL mechanisms do return an empty array (e.g. EXTERNAL from javax), so check that
|
|
// the array is not-empty. Mechanisms are allowed to return either 'null' or an empty array
|
|
// if there is no authentication text.
|
|
if (authenticationBytes != null && authenticationBytes.length > 0) {
|
|
authenticationText = Base64.encodeToString(authenticationBytes);
|
|
} else {
|
|
// RFC6120 6.4.2 "If the initiating entity needs to send a zero-length initial response,
|
|
// it MUST transmit the response as a single equals sign character ("="), which
|
|
// indicates that the response is present but contains no data."
|
|
authenticationText = "=";
|
|
}
|
|
// Send the authentication to the server
|
|
connection.sendNonza(new AuthMechanism(getName(), authenticationText));
|
|
}
|
|
|
|
/**
|
|
* Should return the initial response of the SASL mechanism. The returned byte array will be
|
|
* send base64 encoded to the server. SASL mechanism are free to return <code>null</code> or an
|
|
* empty array here.
|
|
*
|
|
* @return the initial response or null
|
|
* @throws SmackSaslException if a SASL specific error occurred.
|
|
*/
|
|
protected abstract byte[] getAuthenticationText() throws SmackSaslException;
|
|
|
|
/**
|
|
* The server is challenging the SASL mechanism for the stanza he just sent. Send a
|
|
* response to the server's challenge.
|
|
*
|
|
* @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 SmackSaslException if a SASL related error occurs.
|
|
* @throws InterruptedException if the connection is interrupted
|
|
* @throws NotConnectedException if the XMPP connection is not connected.
|
|
*/
|
|
public final void challengeReceived(String challengeString, boolean finalChallenge) throws SmackSaslException, InterruptedException, NotConnectedException {
|
|
byte[] challenge = Base64.decode((challengeString != null && challengeString.equals("=")) ? "" : challengeString);
|
|
byte[] response = evaluateChallenge(challenge);
|
|
if (finalChallenge) {
|
|
return;
|
|
}
|
|
|
|
Response responseStanza;
|
|
if (response == null) {
|
|
responseStanza = new Response();
|
|
}
|
|
else {
|
|
responseStanza = new Response(Base64.encodeToString(response));
|
|
}
|
|
|
|
// Send the authentication to the server
|
|
connection.sendNonza(responseStanza);
|
|
}
|
|
|
|
/**
|
|
* Evaluate the SASL challenge.
|
|
*
|
|
* @param challenge challenge to evaluate.
|
|
*
|
|
* @return null.
|
|
* @throws SmackSaslException If a SASL related error occurs.
|
|
*/
|
|
protected byte[] evaluateChallenge(byte[] challenge) throws SmackSaslException {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public final int compareTo(SASLMechanism other) {
|
|
Integer ourPriority = getPriority();
|
|
return Integer.compare(ourPriority, other.getPriority());
|
|
}
|
|
|
|
/**
|
|
* Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI.
|
|
*
|
|
* @return the common name of the SASL mechanism.
|
|
*/
|
|
public abstract String getName();
|
|
|
|
/**
|
|
* Get the priority of this SASL mechanism. Lower values mean higher priority.
|
|
*
|
|
* @return the priority of this SASL mechanism.
|
|
*/
|
|
public abstract int getPriority();
|
|
|
|
/**
|
|
* Check if the SASL mechanism was successful and if it was, then mark it so.
|
|
*
|
|
* @throws SmackSaslException in case of an SASL error.
|
|
*/
|
|
public final void afterFinalSaslChallenge() throws SmackSaslException {
|
|
checkIfSuccessfulOrThrow();
|
|
|
|
authenticationSuccessful = true;
|
|
}
|
|
|
|
protected abstract void checkIfSuccessfulOrThrow() throws SmackSaslException;
|
|
|
|
public SASLMechanism instanceForAuthentication(XMPPConnection connection, ConnectionConfiguration connectionConfiguration) {
|
|
SASLMechanism saslMechansim = newInstance();
|
|
saslMechansim.connection = connection;
|
|
saslMechansim.connectionConfiguration = connectionConfiguration;
|
|
return saslMechansim;
|
|
}
|
|
|
|
public boolean authzidSupported() {
|
|
return false;
|
|
}
|
|
|
|
public boolean requiresPassword() {
|
|
return true;
|
|
}
|
|
|
|
public boolean isAuthenticationSuccessful() {
|
|
return authenticationSuccessful;
|
|
}
|
|
|
|
public boolean isFinished() {
|
|
return isAuthenticationSuccessful() || exception != null;
|
|
}
|
|
|
|
public void throwExceptionIfRequired() throws SmackSaslException, SASLErrorException, NotConnectedException,
|
|
InterruptedException, NoResponseException {
|
|
if (exception != null) {
|
|
if (exception instanceof SmackSaslException) {
|
|
throw (SmackSaslException) exception;
|
|
} else if (exception instanceof SASLErrorException) {
|
|
throw (SASLErrorException) exception;
|
|
} else if (exception instanceof NotConnectedException) {
|
|
throw (NotConnectedException) exception;
|
|
} else if (exception instanceof InterruptedException) {
|
|
throw (InterruptedException) exception;
|
|
} else {
|
|
throw new IllegalStateException("Unexpected exception type", exception);
|
|
}
|
|
}
|
|
|
|
if (!authenticationSuccessful) {
|
|
throw NoResponseException.newWith(connection, "successful SASL authentication");
|
|
}
|
|
}
|
|
|
|
public void setException(Exception exception) {
|
|
this.exception = exception;
|
|
}
|
|
|
|
protected abstract SASLMechanism newInstance();
|
|
|
|
protected static byte[] toBytes(String string) {
|
|
return StringUtils.toUtf8Bytes(string);
|
|
}
|
|
|
|
/**
|
|
* SASLprep the given String. The resulting String is in UTF-8.
|
|
*
|
|
* @param string the String to sasl prep.
|
|
* @return the given String SASL preped
|
|
* @see <a href="http://tools.ietf.org/html/rfc4013">RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords</a>
|
|
*/
|
|
protected static String saslPrep(String string) {
|
|
return Normalizer.normalize(string, Form.NFKC);
|
|
}
|
|
|
|
@Override
|
|
public final String toString() {
|
|
return "SASL Mech: " + getName() + ", Prio: " + getPriority();
|
|
}
|
|
}
|