1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2007 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smack.sasl;
22
23import org.jivesoftware.smack.XMPPException;
24import org.jivesoftware.smack.SASLAuthentication;
25import org.jivesoftware.smack.packet.Packet;
26import org.jivesoftware.smack.util.StringUtils;
27
28import java.io.IOException;
29import java.util.Map;
30import java.util.HashMap;
31import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
32import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
33import org.apache.harmony.javax.security.auth.callback.Callback;
34import org.apache.harmony.javax.security.auth.callback.NameCallback;
35import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
36import org.apache.harmony.javax.security.sasl.RealmCallback;
37import org.apache.harmony.javax.security.sasl.RealmChoiceCallback;
38import de.measite.smack.Sasl;
39import org.apache.harmony.javax.security.sasl.SaslClient;
40import org.apache.harmony.javax.security.sasl.SaslException;
41
42/**
43 * Base class for SASL mechanisms. Subclasses must implement these methods:
44 * <ul>
45 *  <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li>
46 * </ul>
47 * Subclasses will likely want to implement their own versions of these mthods:
48 *  <li>{@link #authenticate(String, String, String)} -- Initiate authentication stanza using the
49 *  deprecated method.</li>
50 *  <li>{@link #authenticate(String, String, CallbackHandler)} -- Initiate authentication stanza
51 *  using the CallbackHandler method.</li>
52 *  <li>{@link #challengeReceived(String)} -- Handle a challenge from the server.</li>
53 * </ul>
54 *
55 * @author Jay Kline
56 */
57public abstract class SASLMechanism implements CallbackHandler {
58
59    private SASLAuthentication saslAuthentication;
60    protected SaslClient sc;
61    protected String authenticationId;
62    protected String password;
63    protected String hostname;
64
65
66    public SASLMechanism(SASLAuthentication saslAuthentication) {
67        this.saslAuthentication = saslAuthentication;
68    }
69
70    /**
71     * Builds and sends the <tt>auth</tt> stanza to the server. Note that this method of
72     * authentication is not recommended, since it is very inflexable.  Use
73     * {@link #authenticate(String, String, CallbackHandler)} whenever possible.
74     *
75     * @param username the username of the user being authenticated.
76     * @param host     the hostname where the user account resides.
77     * @param password the password for this account.
78     * @throws IOException If a network error occurs while authenticating.
79     * @throws XMPPException If a protocol error occurs or the user is not authenticated.
80     */
81    public void authenticate(String username, String host, String password) throws IOException, XMPPException {
82        //Since we were not provided with a CallbackHandler, we will use our own with the given
83        //information
84
85        //Set the authenticationID as the username, since they must be the same in this case.
86        this.authenticationId = username;
87        this.password = password;
88        this.hostname = host;
89
90        String[] mechanisms = { getName() };
91        Map<String,String> props = new HashMap<String,String>();
92        sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, this);
93        authenticate();
94    }
95
96    /**
97     * Builds and sends the <tt>auth</tt> stanza to the server. The callback handler will handle
98     * any additional information, such as the authentication ID or realm, if it is needed.
99     *
100     * @param username the username of the user being authenticated.
101     * @param host     the hostname where the user account resides.
102     * @param cbh      the CallbackHandler to obtain user information.
103     * @throws IOException If a network error occures while authenticating.
104     * @throws XMPPException If a protocol error occurs or the user is not authenticated.
105     */
106    public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, XMPPException {
107        String[] mechanisms = { getName() };
108        Map<String,String> props = new HashMap<String,String>();
109        sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, cbh);
110        authenticate();
111    }
112
113    protected void authenticate() throws IOException, XMPPException {
114        String authenticationText = null;
115        try {
116            if(sc.hasInitialResponse()) {
117                byte[] response = sc.evaluateChallenge(new byte[0]);
118                authenticationText = StringUtils.encodeBase64(response, false);
119            }
120        } catch (SaslException e) {
121            throw new XMPPException("SASL authentication failed", e);
122        }
123
124        // Send the authentication to the server
125        getSASLAuthentication().send(new AuthMechanism(getName(), authenticationText));
126    }
127
128
129    /**
130     * The server is challenging the SASL mechanism for the stanza he just sent. Send a
131     * response to the server's challenge.
132     *
133     * @param challenge a base64 encoded string representing the challenge.
134     * @throws IOException if an exception sending the response occurs.
135     */
136    public void challengeReceived(String challenge) throws IOException {
137        byte response[];
138        if(challenge != null) {
139            response = sc.evaluateChallenge(StringUtils.decodeBase64(challenge));
140        } else {
141            response = sc.evaluateChallenge(new byte[0]);
142        }
143
144        Packet responseStanza;
145        if (response == null) {
146            responseStanza = new Response();
147        }
148        else {
149            responseStanza = new Response(StringUtils.encodeBase64(response, false));
150        }
151
152        // Send the authentication to the server
153        getSASLAuthentication().send(responseStanza);
154    }
155
156    /**
157     * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI.
158     *
159     * @return the common name of the SASL mechanism.
160     */
161    protected abstract String getName();
162
163
164    protected SASLAuthentication getSASLAuthentication() {
165        return saslAuthentication;
166    }
167
168    /**
169     *
170     */
171    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
172        for (int i = 0; i < callbacks.length; i++) {
173            if (callbacks[i] instanceof NameCallback) {
174                NameCallback ncb = (NameCallback)callbacks[i];
175                ncb.setName(authenticationId);
176            } else if(callbacks[i] instanceof PasswordCallback) {
177                PasswordCallback pcb = (PasswordCallback)callbacks[i];
178                pcb.setPassword(password.toCharArray());
179            } else if(callbacks[i] instanceof RealmCallback) {
180                RealmCallback rcb = (RealmCallback)callbacks[i];
181                rcb.setText(hostname);
182            } else if(callbacks[i] instanceof RealmChoiceCallback){
183                //unused
184                //RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i];
185            } else {
186               throw new UnsupportedCallbackException(callbacks[i]);
187            }
188         }
189    }
190
191    /**
192     * Initiating SASL authentication by select a mechanism.
193     */
194    public class AuthMechanism extends Packet {
195        final private String name;
196        final private String authenticationText;
197
198        public AuthMechanism(String name, String authenticationText) {
199            if (name == null) {
200                throw new NullPointerException("SASL mechanism name shouldn't be null.");
201            }
202            this.name = name;
203            this.authenticationText = authenticationText;
204        }
205
206        public String toXML() {
207            StringBuilder stanza = new StringBuilder();
208            stanza.append("<auth mechanism=\"").append(name);
209            stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
210            if (authenticationText != null &&
211                    authenticationText.trim().length() > 0) {
212                stanza.append(authenticationText);
213            }
214            stanza.append("</auth>");
215            return stanza.toString();
216        }
217    }
218
219    /**
220     * A SASL challenge stanza.
221     */
222    public static class Challenge extends Packet {
223        final private String data;
224
225        public Challenge(String data) {
226            this.data = data;
227        }
228
229        public String toXML() {
230            StringBuilder stanza = new StringBuilder();
231            stanza.append("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
232            if (data != null &&
233                    data.trim().length() > 0) {
234                stanza.append(data);
235            }
236            stanza.append("</challenge>");
237            return stanza.toString();
238        }
239    }
240
241    /**
242     * A SASL response stanza.
243     */
244    public class Response extends Packet {
245        final private String authenticationText;
246
247        public Response() {
248            authenticationText = null;
249        }
250
251        public Response(String authenticationText) {
252            if (authenticationText == null || authenticationText.trim().length() == 0) {
253                this.authenticationText = null;
254            }
255            else {
256                this.authenticationText = authenticationText;
257            }
258        }
259
260        public String toXML() {
261            StringBuilder stanza = new StringBuilder();
262            stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
263            if (authenticationText != null) {
264                stanza.append(authenticationText);
265            }
266            stanza.append("</response>");
267            return stanza.toString();
268        }
269    }
270
271    /**
272     * A SASL success stanza.
273     */
274    public static class Success extends Packet {
275        final private String data;
276
277        public Success(String data) {
278            this.data = data;
279        }
280
281        public String toXML() {
282            StringBuilder stanza = new StringBuilder();
283            stanza.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
284            if (data != null &&
285                    data.trim().length() > 0) {
286                stanza.append(data);
287            }
288            stanza.append("</success>");
289            return stanza.toString();
290        }
291    }
292
293    /**
294     * A SASL failure stanza.
295     */
296    public static class Failure extends Packet {
297        final private String condition;
298
299        public Failure(String condition) {
300            this.condition = condition;
301        }
302
303        /**
304         * Get the SASL related error condition.
305         *
306         * @return the SASL related error condition.
307         */
308        public String getCondition() {
309            return condition;
310        }
311
312        public String toXML() {
313            StringBuilder stanza = new StringBuilder();
314            stanza.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
315            if (condition != null &&
316                    condition.trim().length() > 0) {
317                stanza.append("<").append(condition).append("/>");
318            }
319            stanza.append("</failure>");
320            return stanza.toString();
321        }
322    }
323}
324