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 org.jivesoftware.smack.ConnectionConfiguration;
020import org.jivesoftware.smack.SmackException;
021import org.jivesoftware.smack.SmackException.NotConnectedException;
022import org.jivesoftware.smack.XMPPConnection;
023import org.jivesoftware.smack.sasl.packet.SaslStreamElements.AuthMechanism;
024import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Response;
025import org.jivesoftware.smack.util.StringTransformer;
026import org.jivesoftware.smack.util.StringUtils;
027import org.jivesoftware.smack.util.stringencoder.Base64;
028import org.jxmpp.jid.DomainBareJid;
029import org.jxmpp.jid.EntityBareJid;
030
031import javax.net.ssl.SSLSession;
032import javax.security.auth.callback.CallbackHandler;
033
034/**
035 * Base class for SASL mechanisms.
036 * Subclasses will likely want to implement their own versions of these methods:
037 *  <li>{@link #authenticate(String, String, DomainBareJid, String, EntityBareJid, SSLSession)} -- Initiate authentication stanza using the
038 *  deprecated method.</li>
039 *  <li>{@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} -- Initiate authentication stanza
040 *  using the CallbackHandler method.</li>
041 *  <li>{@link #challengeReceived(String, boolean)} -- Handle a challenge from the server.</li>
042 * </ul>
043 *
044 * @author Jay Kline
045 * @author Florian Schmaus
046 */
047public abstract class SASLMechanism implements Comparable<SASLMechanism> {
048
049    public static final String CRAMMD5 = "CRAM-MD5";
050    public static final String DIGESTMD5 = "DIGEST-MD5";
051    public static final String EXTERNAL = "EXTERNAL";
052    public static final String GSSAPI = "GSSAPI";
053    public static final String PLAIN = "PLAIN";
054
055    // TODO Remove once Smack's min Android API is 9, where java.text.Normalizer is available
056    private static StringTransformer saslPrepTransformer;
057
058    /**
059     * Set the SASLPrep StringTransformer.
060     * <p>
061     * A simple SASLPrep StringTransformer would be for example: <code>java.text.Normalizer.normalize(string, Form.NFKC);</code>
062     * </p>
063     * 
064     * @param stringTransformer set StringTransformer to use for SASLPrep.
065     * @see <a href="http://tools.ietf.org/html/rfc4013">RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords</a>
066     */
067    public static void setSaslPrepTransformer(StringTransformer stringTransformer) {
068        saslPrepTransformer = stringTransformer;
069    }
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 <tt>auth</tt> 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 SmackException If a network error occurs while authenticating.
155     * @throws NotConnectedException 
156     * @throws InterruptedException 
157     */
158    public final void authenticate(String username, String host, DomainBareJid serviceName, String password,
159                    EntityBareJid authzid, SSLSession sslSession)
160                    throws SmackException, 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    /**
173     * @throws SmackException
174     */
175    protected void authenticateInternal() throws SmackException {
176    }
177
178    /**
179     * Builds and sends the <tt>auth</tt> stanza to the server. The callback handler will handle
180     * any additional information, such as the authentication ID or realm, if it is needed.
181     *
182     * @param host     the hostname where the user account resides.
183     * @param serviceName the xmpp service location
184     * @param cbh      the CallbackHandler to obtain user information.
185     * @param authzid the optional authorization identity.
186     * @param sslSession the optional SSL/TLS session (if one was established)
187     * @throws SmackException
188     * @throws NotConnectedException 
189     * @throws InterruptedException 
190     */
191    public void authenticate(String host, DomainBareJid serviceName, CallbackHandler cbh, EntityBareJid authzid, SSLSession sslSession)
192                    throws SmackException, NotConnectedException, InterruptedException {
193        this.host = host;
194        this.serviceName = serviceName;
195        this.authorizationId = authzid;
196        this.sslSession = sslSession;
197        assert(authorizationId == null || authzidSupported());
198        authenticateInternal(cbh);
199        authenticate();
200    }
201
202    protected abstract void authenticateInternal(CallbackHandler cbh) throws SmackException;
203
204    private final void authenticate() throws SmackException, NotConnectedException, InterruptedException {
205        byte[] authenticationBytes = getAuthenticationText();
206        String authenticationText;
207        // Some SASL mechanisms do return an empty array (e.g. EXTERNAL from javax), so check that
208        // the array is not-empty. Mechanisms are allowed to return either 'null' or an empty array
209        // if there is no authentication text.
210        if (authenticationBytes != null && authenticationBytes.length > 0) {
211            authenticationText = Base64.encodeToString(authenticationBytes);
212        } else {
213            // RFC6120 6.4.2 "If the initiating entity needs to send a zero-length initial response,
214            // it MUST transmit the response as a single equals sign character ("="), which
215            // indicates that the response is present but contains no data."
216            authenticationText = "=";
217        }
218        // Send the authentication to the server
219        connection.sendNonza(new AuthMechanism(getName(), authenticationText));
220    }
221
222    /**
223     * Should return the initial response of the SASL mechanism. The returned byte array will be
224     * send base64 encoded to the server. SASL mechanism are free to return <code>null</code> or an
225     * empty array here.
226     * 
227     * @return the initial response or null
228     * @throws SmackException
229     */
230    protected abstract byte[] getAuthenticationText() throws SmackException;
231
232    /**
233     * The server is challenging the SASL mechanism for the stanza he just sent. Send a
234     * response to the server's challenge.
235     *
236     * @param challengeString a base64 encoded string representing the challenge.
237     * @param finalChallenge true if this is the last challenge send by the server within the success stanza
238     * @throws NotConnectedException
239     * @throws SmackException
240     * @throws InterruptedException 
241     */
242    public final void challengeReceived(String challengeString, boolean finalChallenge) throws SmackException, NotConnectedException, InterruptedException {
243        byte[] challenge = Base64.decode((challengeString != null && challengeString.equals("=")) ? "" : challengeString);
244        byte[] response = evaluateChallenge(challenge);
245        if (finalChallenge) {
246            return;
247        }
248
249        Response responseStanza;
250        if (response == null) {
251            responseStanza = new Response();
252        }
253        else {
254            responseStanza = new Response(Base64.encodeToString(response));
255        }
256
257        // Send the authentication to the server
258        connection.sendNonza(responseStanza);
259    }
260
261    /**
262     * @throws SmackException
263     */
264    protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
265        return null;
266    }
267
268    @Override
269    public final int compareTo(SASLMechanism other) {
270        // Switch to Integer.compare(int, int) once Smack is on Android 19 or higher.
271        Integer ourPriority = getPriority();
272        return ourPriority.compareTo(other.getPriority());
273    }
274
275    /**
276     * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI.
277     *
278     * @return the common name of the SASL mechanism.
279     */
280    public abstract String getName();
281
282    public abstract int getPriority();
283
284    public abstract void checkIfSuccessfulOrThrow() throws SmackException;
285
286    public SASLMechanism instanceForAuthentication(XMPPConnection connection, ConnectionConfiguration connectionConfiguration) {
287        SASLMechanism saslMechansim = newInstance();
288        saslMechansim.connection = connection;
289        saslMechansim.connectionConfiguration = connectionConfiguration;
290        return saslMechansim;
291    }
292
293    public boolean authzidSupported() {
294        return false;
295    }
296
297    protected abstract SASLMechanism newInstance();
298
299    protected static byte[] toBytes(String string) {
300        return StringUtils.toBytes(string);
301    }
302
303    /**
304     * SASLprep the given String. The resulting String is in UTF-8.
305     * 
306     * @param string the String to sasl prep.
307     * @return the given String SASL preped
308     * @see <a href="http://tools.ietf.org/html/rfc4013">RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords</a>
309     */
310    protected static String saslPrep(String string) {
311        StringTransformer stringTransformer = saslPrepTransformer;
312        if (stringTransformer != null) {
313            return stringTransformer.transform(string);
314        }
315        return string;
316    }
317}