001/**
002 *
003 * Copyright 2003-2007 Jive Software, 2014-2021 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack.sasl;
018
019import java.text.Normalizer;
020import java.text.Normalizer.Form;
021
022import javax.net.ssl.SSLSession;
023import javax.security.auth.callback.CallbackHandler;
024
025import org.jivesoftware.smack.ConnectionConfiguration;
026import org.jivesoftware.smack.SmackException.NoResponseException;
027import org.jivesoftware.smack.SmackException.NotConnectedException;
028import org.jivesoftware.smack.SmackException.SmackSaslException;
029import org.jivesoftware.smack.XMPPConnection;
030import org.jivesoftware.smack.sasl.packet.SaslNonza.AuthMechanism;
031import org.jivesoftware.smack.sasl.packet.SaslNonza.Response;
032import org.jivesoftware.smack.util.StringUtils;
033import org.jivesoftware.smack.util.stringencoder.Base64;
034
035import org.jxmpp.jid.DomainBareJid;
036import org.jxmpp.jid.EntityBareJid;
037
038/**
039 * Base class for SASL mechanisms.
040 * Subclasses will likely want to implement their own versions of these methods:
041 * <ul>
042 *  <li>{@link #authenticate(String, String, DomainBareJid, String, EntityBareJid, SSLSession)} -- Initiate authentication stanza using the
043 *  deprecated method.</li>
044 *  <li>{@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} -- Initiate authentication stanza
045 *  using the CallbackHandler method.</li>
046 *  <li>{@link #challengeReceived(String, boolean)} -- Handle a challenge from the server.</li>
047 * </ul>
048 *
049 * @author Jay Kline
050 * @author Florian Schmaus
051 */
052public abstract class SASLMechanism implements Comparable<SASLMechanism> {
053
054    public static final String CRAMMD5 = "CRAM-MD5";
055    public static final String DIGESTMD5 = "DIGEST-MD5";
056    public static final String EXTERNAL = "EXTERNAL";
057    public static final String GSSAPI = "GSSAPI";
058    public static final String PLAIN = "PLAIN";
059
060    /**
061     * Boolean indicating if SASL negotiation has finished and was successful.
062     */
063    private boolean authenticationSuccessful;
064
065    /**
066     * Either of type {@link SmackSaslException},{@link SASLErrorException}, {@link NotConnectedException} or
067     * {@link InterruptedException}.
068     */
069    private Exception exception;
070
071    protected XMPPConnection connection;
072
073    protected ConnectionConfiguration connectionConfiguration;
074
075    /**
076     * Then authentication identity (authcid). RFC 6120 § 6.3.7 informs us that some SASL mechanisms use this as a
077     * "simple user name". But the exact form is a matter of the mechanism and that it does not necessarily map to an
078     * localpart. But it usually is the localpart of the client JID, although sometimes other formats are used (e.g. the
079     * full JID).
080     * <p>
081     * Not to be confused with the authzid (see RFC 6120 § 6.3.8).
082     * </p>
083     */
084    protected String authenticationId;
085
086    /**
087     * The authorization identifier (authzid).
088     * This is always a bare Jid, but can be null.
089     */
090    protected EntityBareJid authorizationId;
091
092    /**
093     * The name of the XMPP service
094     */
095    protected DomainBareJid serviceName;
096
097    /**
098     * The users password
099     */
100    protected String password;
101    protected String host;
102
103    /**
104     * The used SSL/TLS session (if any).
105     */
106    protected SSLSession sslSession;
107
108    /**
109     * Builds and sends the <code>auth</code> stanza to the server. Note that this method of
110     * authentication is not recommended, since it is very inflexible. Use
111     * {@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} whenever possible.
112     *
113     * Explanation of auth stanza:
114     *
115     * The client authentication stanza needs to include the digest-uri of the form: xmpp/serviceName
116     * From RFC-2831:
117     * digest-uri = "digest-uri" "=" digest-uri-value
118     * digest-uri-value = serv-type "/" host [ "/" serv-name ]
119     *
120     * digest-uri:
121     * Indicates the principal name of the service with which the client
122     * wishes to connect, formed from the serv-type, host, and serv-name.
123     * For example, the FTP service
124     * on "ftp.example.com" would have a "digest-uri" value of "ftp/ftp.example.com"; the SMTP
125     * server from the example above would have a "digest-uri" value of
126     * "smtp/mail3.example.com/example.com".
127     *
128     * host:
129     * The DNS host name or IP address for the service requested. The DNS host name
130     * must be the fully-qualified canonical name of the host. The DNS host name is the
131     * preferred form; see notes on server processing of the digest-uri.
132     *
133     * serv-name:
134     * Indicates the name of the service if it is replicated. The service is
135     * considered to be replicated if the client's service-location process involves resolution
136     * using standard DNS lookup operations, and if these operations involve DNS records (such
137     * as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case,
138     * the initial name used by the client is the "serv-name", and the final name is the "host"
139     * component. For example, the incoming mail service for "example.com" may be replicated
140     * through the use of MX records stored in the DNS, one of which points at an SMTP server
141     * called "mail3.example.com"; it's "serv-name" would be "example.com", it's "host" would be
142     * "mail3.example.com". If the service is not replicated, or the serv-name is identical to
143     * the host, then the serv-name component MUST be omitted
144     *
145     * digest-uri verification is needed for ejabberd 2.0.3 and higher
146     *
147     * @param username the username of the user being authenticated.
148     * @param host the hostname where the user account resides.
149     * @param serviceName the xmpp service location - used by the SASL client in digest-uri creation
150     * serviceName format is: host [ "/" serv-name ] as per RFC-2831
151     * @param password the password for this account.
152     * @param authzid the optional authorization identity.
153     * @param sslSession the optional SSL/TLS session (if one was established)
154     * @throws SmackSaslException if a SASL related error occurs.
155     * @throws NotConnectedException if the XMPP connection is not connected.
156     * @throws InterruptedException if the calling thread was interrupted.
157     */
158    public final void authenticate(String username, String host, DomainBareJid serviceName, String password,
159                    EntityBareJid authzid, SSLSession sslSession)
160                    throws SmackSaslException, NotConnectedException, InterruptedException {
161        this.authenticationId = username;
162        this.host = host;
163        this.serviceName = serviceName;
164        this.password = password;
165        this.authorizationId = authzid;
166        this.sslSession = sslSession;
167        assert authorizationId == null || authzidSupported();
168        authenticateInternal();
169        authenticate();
170    }
171
172    protected void authenticateInternal() throws SmackSaslException {
173    }
174
175    /**
176     * Builds and sends the <code>auth</code> stanza to the server. The callback handler will handle
177     * any additional information, such as the authentication ID or realm, if it is needed.
178     *
179     * @param host     the hostname where the user account resides.
180     * @param serviceName the xmpp service location
181     * @param cbh      the CallbackHandler to obtain user information.
182     * @param authzid the optional authorization identity.
183     * @param sslSession the optional SSL/TLS session (if one was established)
184     * @throws SmackSaslException if a SASL related error occurs.
185     * @throws NotConnectedException if the XMPP connection is not connected.
186     * @throws InterruptedException if the calling thread was interrupted.
187     */
188    public void authenticate(String host, DomainBareJid serviceName, CallbackHandler cbh, EntityBareJid authzid, SSLSession sslSession)
189                    throws SmackSaslException, NotConnectedException, InterruptedException {
190        this.host = host;
191        this.serviceName = serviceName;
192        this.authorizationId = authzid;
193        this.sslSession = sslSession;
194        assert authorizationId == null || authzidSupported();
195        authenticateInternal(cbh);
196        authenticate();
197    }
198
199    protected abstract void authenticateInternal(CallbackHandler cbh) throws SmackSaslException;
200
201    private void authenticate() throws SmackSaslException, NotConnectedException, InterruptedException {
202        byte[] authenticationBytes = getAuthenticationText();
203        String authenticationText;
204        // Some SASL mechanisms do return an empty array (e.g. EXTERNAL from javax), so check that
205        // the array is not-empty. Mechanisms are allowed to return either 'null' or an empty array
206        // if there is no authentication text.
207        if (authenticationBytes != null && authenticationBytes.length > 0) {
208            authenticationText = Base64.encodeToString(authenticationBytes);
209        } else {
210            // RFC6120 6.4.2 "If the initiating entity needs to send a zero-length initial response,
211            // it MUST transmit the response as a single equals sign character ("="), which
212            // indicates that the response is present but contains no data."
213            authenticationText = "=";
214        }
215        // Send the authentication to the server
216        connection.sendNonza(new AuthMechanism(getName(), authenticationText));
217    }
218
219    /**
220     * Should return the initial response of the SASL mechanism. The returned byte array will be
221     * send base64 encoded to the server. SASL mechanism are free to return <code>null</code> or an
222     * empty array here.
223     *
224     * @return the initial response or null
225     * @throws SmackSaslException if a SASL specific error occurred.
226     */
227    protected abstract byte[] getAuthenticationText() throws SmackSaslException;
228
229    /**
230     * The server is challenging the SASL mechanism for the stanza he just sent. Send a
231     * response to the server's challenge.
232     *
233     * @param challengeString a base64 encoded string representing the challenge.
234     * @param finalChallenge true if this is the last challenge send by the server within the success stanza
235     * @throws SmackSaslException if a SASL related error occurs.
236     * @throws InterruptedException if the connection is interrupted
237     * @throws NotConnectedException if the XMPP connection is not connected.
238     */
239    public final void challengeReceived(String challengeString, boolean finalChallenge) throws SmackSaslException, InterruptedException, NotConnectedException {
240        byte[] challenge = Base64.decode((challengeString != null && challengeString.equals("=")) ? "" : challengeString);
241        byte[] response = evaluateChallenge(challenge);
242        if (finalChallenge) {
243            return;
244        }
245
246        Response responseStanza;
247        if (response == null) {
248            responseStanza = new Response();
249        }
250        else {
251            responseStanza = new Response(Base64.encodeToString(response));
252        }
253
254        // Send the authentication to the server
255        connection.sendNonza(responseStanza);
256    }
257
258    /**
259     * Evaluate the SASL challenge.
260     *
261     * @param challenge challenge to evaluate.
262     *
263     * @return null.
264     * @throws SmackSaslException If a SASL related error occurs.
265     */
266    protected byte[] evaluateChallenge(byte[] challenge) throws SmackSaslException {
267        return null;
268    }
269
270    @Override
271    public final int compareTo(SASLMechanism other) {
272        Integer ourPriority = getPriority();
273        return Integer.compare(ourPriority, other.getPriority());
274    }
275
276    /**
277     * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI.
278     *
279     * @return the common name of the SASL mechanism.
280     */
281    public abstract String getName();
282
283    /**
284     * Get the priority of this SASL mechanism. Lower values mean higher priority.
285     *
286     * @return the priority of this SASL mechanism.
287     */
288    public abstract int getPriority();
289
290    /**
291     * Check if the SASL mechanism was successful and if it was, then mark it so.
292     *
293     * @throws SmackSaslException in case of an SASL error.
294     */
295    public final void afterFinalSaslChallenge() throws SmackSaslException {
296        checkIfSuccessfulOrThrow();
297
298        authenticationSuccessful = true;
299    }
300
301    protected abstract void checkIfSuccessfulOrThrow() throws SmackSaslException;
302
303    public SASLMechanism instanceForAuthentication(XMPPConnection connection, ConnectionConfiguration connectionConfiguration) {
304        SASLMechanism saslMechansim = newInstance();
305        saslMechansim.connection = connection;
306        saslMechansim.connectionConfiguration = connectionConfiguration;
307        return saslMechansim;
308    }
309
310    public boolean authzidSupported() {
311        return false;
312    }
313
314    public boolean requiresPassword() {
315        return true;
316    }
317
318    public boolean isAuthenticationSuccessful() {
319        return authenticationSuccessful;
320    }
321
322    public boolean isFinished() {
323        return isAuthenticationSuccessful() || exception != null;
324    }
325
326    public void throwExceptionIfRequired() throws SmackSaslException, SASLErrorException, NotConnectedException,
327                    InterruptedException, NoResponseException {
328        if (exception != null) {
329            if (exception instanceof SmackSaslException) {
330                throw (SmackSaslException) exception;
331            } else if (exception instanceof SASLErrorException) {
332                throw (SASLErrorException) exception;
333            } else if (exception instanceof NotConnectedException) {
334                throw (NotConnectedException) exception;
335            } else if (exception instanceof InterruptedException) {
336                throw (InterruptedException) exception;
337            } else {
338                throw new IllegalStateException("Unexpected exception type", exception);
339            }
340        }
341
342        if (!authenticationSuccessful) {
343            throw NoResponseException.newWith(connection, "successful SASL authentication");
344        }
345    }
346
347    public void setException(Exception exception) {
348        this.exception = exception;
349    }
350
351    protected abstract SASLMechanism newInstance();
352
353    protected static byte[] toBytes(String string) {
354        return StringUtils.toUtf8Bytes(string);
355    }
356
357    /**
358     * SASLprep the given String. The resulting String is in UTF-8.
359     *
360     * @param string the String to sasl prep.
361     * @return the given String SASL preped
362     * @see <a href="http://tools.ietf.org/html/rfc4013">RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords</a>
363     */
364    protected static String saslPrep(String string) {
365        return Normalizer.normalize(string, Form.NFKC);
366    }
367
368    @Override
369    public final String toString() {
370        return "SASL Mech: " + getName() + ", Prio: " + getPriority();
371    }
372}