001/**
002 *
003 * Copyright 2003-2007 Jive Software, 2014-2016 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 javax.net.ssl.SSLSession;
020import javax.security.auth.callback.CallbackHandler;
021
022import org.jivesoftware.smack.ConnectionConfiguration;
023import org.jivesoftware.smack.SmackException;
024import org.jivesoftware.smack.SmackException.NotConnectedException;
025import org.jivesoftware.smack.XMPPConnection;
026import org.jivesoftware.smack.sasl.packet.SaslStreamElements.AuthMechanism;
027import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Response;
028import org.jivesoftware.smack.util.StringTransformer;
029import org.jivesoftware.smack.util.StringUtils;
030import org.jivesoftware.smack.util.stringencoder.Base64;
031
032import org.jxmpp.jid.DomainBareJid;
033import org.jxmpp.jid.EntityBareJid;
034
035/**
036 * Base class for SASL mechanisms.
037 * Subclasses will likely want to implement their own versions of these methods:
038 *  <li>{@link #authenticate(String, String, DomainBareJid, String, EntityBareJid, SSLSession)} -- Initiate authentication stanza using the
039 *  deprecated method.</li>
040 *  <li>{@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} -- Initiate authentication stanza
041 *  using the CallbackHandler method.</li>
042 *  <li>{@link #challengeReceived(String, boolean)} -- Handle a challenge from the server.</li>
043 * </ul>
044 *
045 * @author Jay Kline
046 * @author Florian Schmaus
047 */
048public abstract class SASLMechanism implements Comparable<SASLMechanism> {
049
050    public static final String CRAMMD5 = "CRAM-MD5";
051    public static final String DIGESTMD5 = "DIGEST-MD5";
052    public static final String EXTERNAL = "EXTERNAL";
053    public static final String GSSAPI = "GSSAPI";
054    public static final String PLAIN = "PLAIN";
055
056    // TODO Remove once Smack's min Android API is 9, where java.text.Normalizer is available
057    private static StringTransformer saslPrepTransformer;
058
059    /**
060     * Set the SASLPrep StringTransformer.
061     * <p>
062     * A simple SASLPrep StringTransformer would be for example: <code>java.text.Normalizer.normalize(string, Form.NFKC);</code>
063     * </p>
064     * 
065     * @param stringTransformer set StringTransformer to use for SASLPrep.
066     * @see <a href="http://tools.ietf.org/html/rfc4013">RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords</a>
067     */
068    public static void setSaslPrepTransformer(StringTransformer stringTransformer) {
069        saslPrepTransformer = stringTransformer;
070    }
071
072    protected XMPPConnection connection;
073
074    protected ConnectionConfiguration connectionConfiguration;
075
076    /**
077     * Then authentication identity (authcid). RFC 6120 § 6.3.7 informs us that some SASL mechanisms use this as a
078     * "simple user name". But the exact form is a matter of the mechanism and that it does not necessarily map to an
079     * localpart. But it usually is the localpart of the client JID, although sometimes other formats are used (e.g. the
080     * full JID).
081     * <p>
082     * Not to be confused with the authzid (see RFC 6120 § 6.3.8).
083     * </p>
084     */
085    protected String authenticationId;
086
087    /**
088     * The authorization identifier (authzid).
089     * This is always a bare Jid, but can be null.
090     */
091    protected EntityBareJid authorizationId;
092
093    /**
094     * The name of the XMPP service
095     */
096    protected DomainBareJid serviceName;
097
098    /**
099     * The users password
100     */
101    protected String password;
102    protected String host;
103
104    /**
105     * The used SSL/TLS session (if any).
106     */
107    protected SSLSession sslSession;
108
109    /**
110     * Builds and sends the <tt>auth</tt> stanza to the server. Note that this method of
111     * authentication is not recommended, since it is very inflexible. Use
112     * {@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} whenever possible.
113     * 
114     * Explanation of auth stanza:
115     * 
116     * The client authentication stanza needs to include the digest-uri of the form: xmpp/serviceName 
117     * From RFC-2831: 
118     * digest-uri = "digest-uri" "=" digest-uri-value
119     * digest-uri-value = serv-type "/" host [ "/" serv-name ]
120     * 
121     * digest-uri: 
122     * Indicates the principal name of the service with which the client 
123     * wishes to connect, formed from the serv-type, host, and serv-name. 
124     * For example, the FTP service
125     * on "ftp.example.com" would have a "digest-uri" value of "ftp/ftp.example.com"; the SMTP
126     * server from the example above would have a "digest-uri" value of
127     * "smtp/mail3.example.com/example.com".
128     * 
129     * host:
130     * The DNS host name or IP address for the service requested. The DNS host name
131     * must be the fully-qualified canonical name of the host. The DNS host name is the
132     * preferred form; see notes on server processing of the digest-uri.
133     * 
134     * serv-name: 
135     * Indicates the name of the service if it is replicated. The service is
136     * considered to be replicated if the client's service-location process involves resolution
137     * using standard DNS lookup operations, and if these operations involve DNS records (such
138     * as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case,
139     * the initial name used by the client is the "serv-name", and the final name is the "host"
140     * component. For example, the incoming mail service for "example.com" may be replicated
141     * through the use of MX records stored in the DNS, one of which points at an SMTP server
142     * called "mail3.example.com"; it's "serv-name" would be "example.com", it's "host" would be
143     * "mail3.example.com". If the service is not replicated, or the serv-name is identical to
144     * the host, then the serv-name component MUST be omitted
145     *
146     * digest-uri verification is needed for ejabberd 2.0.3 and higher
147     *
148     * @param username the username of the user being authenticated.
149     * @param host the hostname where the user account resides.
150     * @param serviceName the xmpp service location - used by the SASL client in digest-uri creation
151     * serviceName format is: host [ "/" serv-name ] as per RFC-2831
152     * @param password the password for this account.
153     * @param authzid the optional authorization identity.
154     * @param sslSession the optional SSL/TLS session (if one was established)
155     * @throws SmackException If a network error occurs while authenticating.
156     * @throws NotConnectedException 
157     * @throws InterruptedException 
158     */
159    public final void authenticate(String username, String host, DomainBareJid serviceName, String password,
160                    EntityBareJid authzid, SSLSession sslSession)
161                    throws SmackException, NotConnectedException, InterruptedException {
162        this.authenticationId = username;
163        this.host = host;
164        this.serviceName = serviceName;
165        this.password = password;
166        this.authorizationId = authzid;
167        this.sslSession = sslSession;
168        assert (authorizationId == null || authzidSupported());
169        authenticateInternal();
170        authenticate();
171    }
172
173    /**
174     * @throws SmackException
175     */
176    protected void authenticateInternal() throws SmackException {
177    }
178
179    /**
180     * Builds and sends the <tt>auth</tt> stanza to the server. The callback handler will handle
181     * any additional information, such as the authentication ID or realm, if it is needed.
182     *
183     * @param host     the hostname where the user account resides.
184     * @param serviceName the xmpp service location
185     * @param cbh      the CallbackHandler to obtain user information.
186     * @param authzid the optional authorization identity.
187     * @param sslSession the optional SSL/TLS session (if one was established)
188     * @throws SmackException
189     * @throws NotConnectedException 
190     * @throws InterruptedException 
191     */
192    public void authenticate(String host, DomainBareJid serviceName, CallbackHandler cbh, EntityBareJid authzid, SSLSession sslSession)
193                    throws SmackException, NotConnectedException, InterruptedException {
194        this.host = host;
195        this.serviceName = serviceName;
196        this.authorizationId = authzid;
197        this.sslSession = sslSession;
198        assert (authorizationId == null || authzidSupported());
199        authenticateInternal(cbh);
200        authenticate();
201    }
202
203    protected abstract void authenticateInternal(CallbackHandler cbh) throws SmackException;
204
205    private final void authenticate() throws SmackException, NotConnectedException, InterruptedException {
206        byte[] authenticationBytes = getAuthenticationText();
207        String authenticationText;
208        // Some SASL mechanisms do return an empty array (e.g. EXTERNAL from javax), so check that
209        // the array is not-empty. Mechanisms are allowed to return either 'null' or an empty array
210        // if there is no authentication text.
211        if (authenticationBytes != null && authenticationBytes.length > 0) {
212            authenticationText = Base64.encodeToString(authenticationBytes);
213        } else {
214            // RFC6120 6.4.2 "If the initiating entity needs to send a zero-length initial response,
215            // it MUST transmit the response as a single equals sign character ("="), which
216            // indicates that the response is present but contains no data."
217            authenticationText = "=";
218        }
219        // Send the authentication to the server
220        connection.sendNonza(new AuthMechanism(getName(), authenticationText));
221    }
222
223    /**
224     * Should return the initial response of the SASL mechanism. The returned byte array will be
225     * send base64 encoded to the server. SASL mechanism are free to return <code>null</code> or an
226     * empty array here.
227     * 
228     * @return the initial response or null
229     * @throws SmackException
230     */
231    protected abstract byte[] getAuthenticationText() throws SmackException;
232
233    /**
234     * The server is challenging the SASL mechanism for the stanza he just sent. Send a
235     * response to the server's challenge.
236     *
237     * @param challengeString a base64 encoded string representing the challenge.
238     * @param finalChallenge true if this is the last challenge send by the server within the success stanza
239     * @throws NotConnectedException
240     * @throws SmackException
241     * @throws InterruptedException 
242     */
243    public final void challengeReceived(String challengeString, boolean finalChallenge) throws SmackException, NotConnectedException, InterruptedException {
244        byte[] challenge = Base64.decode((challengeString != null && challengeString.equals("=")) ? "" : challengeString);
245        byte[] response = evaluateChallenge(challenge);
246        if (finalChallenge) {
247            return;
248        }
249
250        Response responseStanza;
251        if (response == null) {
252            responseStanza = new Response();
253        }
254        else {
255            responseStanza = new Response(Base64.encodeToString(response));
256        }
257
258        // Send the authentication to the server
259        connection.sendNonza(responseStanza);
260    }
261
262    /**
263     * @throws SmackException
264     */
265    protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
266        return null;
267    }
268
269    @Override
270    public final int compareTo(SASLMechanism other) {
271        // Switch to Integer.compare(int, int) once Smack is on Android 19 or higher.
272        Integer ourPriority = getPriority();
273        return ourPriority.compareTo(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    public abstract void checkIfSuccessfulOrThrow() throws SmackException;
291
292    public SASLMechanism instanceForAuthentication(XMPPConnection connection, ConnectionConfiguration connectionConfiguration) {
293        SASLMechanism saslMechansim = newInstance();
294        saslMechansim.connection = connection;
295        saslMechansim.connectionConfiguration = connectionConfiguration;
296        return saslMechansim;
297    }
298
299    public boolean authzidSupported() {
300        return false;
301    }
302
303    protected abstract SASLMechanism newInstance();
304
305    protected static byte[] toBytes(String string) {
306        return StringUtils.toBytes(string);
307    }
308
309    /**
310     * SASLprep the given String. The resulting String is in UTF-8.
311     * 
312     * @param string the String to sasl prep.
313     * @return the given String SASL preped
314     * @see <a href="http://tools.ietf.org/html/rfc4013">RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords</a>
315     */
316    protected static String saslPrep(String string) {
317        StringTransformer stringTransformer = saslPrepTransformer;
318        if (stringTransformer != null) {
319            return stringTransformer.transform(string);
320        }
321        return string;
322    }
323
324    @Override
325    public final String toString() {
326        return "SASL Mech: " + getName() + ", Prio: " + getPriority();
327    }
328}