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