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}