001/** 002 * 003 * Copyright 2014 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.provided; 018 019import java.io.UnsupportedEncodingException; 020 021import javax.security.auth.callback.CallbackHandler; 022 023import org.jivesoftware.smack.SmackException; 024import org.jivesoftware.smack.sasl.SASLMechanism; 025import org.jivesoftware.smack.util.ByteUtils; 026import org.jivesoftware.smack.util.MD5; 027import org.jivesoftware.smack.util.StringUtils; 028 029public class SASLDigestMD5Mechanism extends SASLMechanism { 030 031 public static final String NAME = DIGESTMD5; 032 033 private static final String INITAL_NONCE = "00000001"; 034 035 /** 036 * The only 'qop' value supported by this implementation 037 */ 038 private static final String QOP_VALUE = "auth"; 039 040 private enum State { 041 INITIAL, 042 RESPONSE_SENT, 043 VALID_SERVER_RESPONSE, 044 } 045 046 private static boolean verifyServerResponse = true; 047 048 public static void setVerifyServerResponse(boolean verifyServerResponse) { 049 SASLDigestMD5Mechanism.verifyServerResponse = verifyServerResponse; 050 } 051 052 /** 053 * The state of the this instance of SASL DIGEST-MD5 authentication. 054 */ 055 private State state = State.INITIAL; 056 057 private String nonce; 058 private String cnonce; 059 private String digestUri; 060 private String hex_hashed_a1; 061 062 @Override 063 protected void authenticateInternal(CallbackHandler cbh) throws SmackException { 064 throw new UnsupportedOperationException("CallbackHandler not (yet) supported"); 065 } 066 067 @Override 068 protected byte[] getAuthenticationText() throws SmackException { 069 // DIGEST-MD5 has no initial response, return null 070 return null; 071 } 072 073 @Override 074 public String getName() { 075 return NAME; 076 } 077 078 @Override 079 public int getPriority() { 080 return 210; 081 } 082 083 @Override 084 public SASLDigestMD5Mechanism newInstance() { 085 return new SASLDigestMD5Mechanism(); 086 } 087 088 @Override 089 public boolean authzidSupported() { 090 return true; 091 } 092 093 094 @Override 095 public void checkIfSuccessfulOrThrow() throws SmackException { 096 if (verifyServerResponse && state != State.VALID_SERVER_RESPONSE) { 097 throw new SmackException(NAME + " no valid server response"); 098 } 099 } 100 101 @Override 102 protected byte[] evaluateChallenge(byte[] challenge) throws SmackException { 103 if (challenge.length == 0) { 104 throw new SmackException("Initial challenge has zero length"); 105 } 106 String challengeString; 107 try { 108 challengeString = new String(challenge, StringUtils.UTF8); 109 } 110 catch (UnsupportedEncodingException e) { 111 throw new AssertionError(e); 112 } 113 String[] challengeParts = challengeString.split(","); 114 byte[] response = null; 115 switch (state) { 116 case INITIAL: 117 for (String part : challengeParts) { 118 String[] keyValue = part.split("="); 119 assert (keyValue.length == 2); 120 String key = keyValue[0]; 121 // RFC 2831 § 7.1 about the formating of the digest-challenge: 122 // "The full form is "<n>#<m>element" indicating at least <n> and 123 // at most <m> elements, each separated by one or more commas 124 // (",") and OPTIONAL linear white space (LWS)." 125 // Which means the key value may be preceded by whitespace, 126 // which is what we remove: *Only the preceding whitespace*. 127 key = key.replaceFirst("^\\s+", ""); 128 String value = keyValue[1]; 129 if ("nonce".equals(key)) { 130 if (nonce != null) { 131 throw new SmackException("Nonce value present multiple times"); 132 } 133 nonce = value.replace("\"", ""); 134 } 135 else if ("qop".equals(key)) { 136 value = value.replace("\"", ""); 137 if (!value.equals("auth")) { 138 throw new SmackException("Unsupported qop operation: " + value); 139 } 140 } 141 } 142 if (nonce == null) { 143 // RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly 144 // once; if not present, or if multiple instances are present, the client should 145 // abort the authentication exchange." 146 throw new SmackException("nonce value not present in initial challenge"); 147 } 148 // RFC 2831 2.1.2.1 defines A1, A2, KD and response-value 149 byte[] a1FirstPart = MD5.bytes(authenticationId + ':' + serviceName + ':' 150 + password); 151 cnonce = StringUtils.randomString(32); 152 byte[] a1 = ByteUtils.concact(a1FirstPart, toBytes(':' + nonce + ':' + cnonce)); 153 digestUri = "xmpp/" + serviceName; 154 hex_hashed_a1 = StringUtils.encodeHex(MD5.bytes(a1)); 155 String responseValue = calcResponse(DigestType.ClientResponse); 156 // @formatter:off 157 // See RFC 2831 2.1.2 digest-response 158 String authzid; 159 if (authorizationId == null) { 160 authzid = ""; 161 } else { 162 authzid = ",authzid=\"" + authorizationId + '"'; 163 } 164 String saslString = "username=\"" + quoteBackslash(authenticationId) + '"' 165 + authzid 166 + ",realm=\"" + serviceName + '"' 167 + ",nonce=\"" + nonce + '"' 168 + ",cnonce=\"" + cnonce + '"' 169 + ",nc=" + INITAL_NONCE 170 + ",qop=auth" 171 + ",digest-uri=\"" + digestUri + '"' 172 + ",response=" + responseValue 173 + ",charset=utf-8"; 174 // @formatter:on 175 response = toBytes(saslString); 176 state = State.RESPONSE_SENT; 177 break; 178 case RESPONSE_SENT: 179 if (verifyServerResponse) { 180 String serverResponse = null; 181 for (String part : challengeParts) { 182 String[] keyValue = part.split("="); 183 assert (keyValue.length == 2); 184 String key = keyValue[0]; 185 String value = keyValue[1]; 186 if ("rspauth".equals(key)) { 187 serverResponse = value; 188 break; 189 } 190 } 191 if (serverResponse == null) { 192 throw new SmackException("No server response received while performing " + NAME 193 + " authentication"); 194 } 195 String expectedServerResponse = calcResponse(DigestType.ServerResponse); 196 if (!serverResponse.equals(expectedServerResponse)) { 197 throw new SmackException("Invalid server response while performing " + NAME 198 + " authentication"); 199 } 200 } 201 state = State.VALID_SERVER_RESPONSE; 202 break; 203 default: 204 throw new IllegalStateException(); 205 } 206 return response; 207 } 208 209 private enum DigestType { 210 ClientResponse, 211 ServerResponse 212 } 213 214 private String calcResponse(DigestType digestType) { 215 StringBuilder a2 = new StringBuilder(); 216 if (digestType == DigestType.ClientResponse) { 217 a2.append("AUTHENTICATE"); 218 } 219 a2.append(':'); 220 a2.append(digestUri); 221 String hex_hashed_a2 = StringUtils.encodeHex(MD5.bytes(a2.toString())); 222 223 StringBuilder kd_argument = new StringBuilder(); 224 kd_argument.append(hex_hashed_a1); 225 kd_argument.append(':'); 226 kd_argument.append(nonce); 227 kd_argument.append(':'); 228 kd_argument.append(INITAL_NONCE); 229 kd_argument.append(':'); 230 kd_argument.append(cnonce); 231 kd_argument.append(':'); 232 kd_argument.append(QOP_VALUE); 233 kd_argument.append(':'); 234 kd_argument.append(hex_hashed_a2); 235 byte[] kd = MD5.bytes(kd_argument.toString()); 236 String responseValue = StringUtils.encodeHex(kd); 237 return responseValue; 238 } 239 240 /** 241 * Quote the backslash in the given String. Replaces all occurrences of "\" with "\\". 242 * <p> 243 * According to RFC 2831 § 7.2 a quoted-string consists either of qdtext or quoted-pair. And since quoted-pair is a 244 * backslash followed by a char, every backslash in qdtext must be quoted, since it otherwise would be treated as 245 * qdtext. 246 * </p> 247 * 248 * @param string the input string. 249 * @return the input string where the every backslash is quoted. 250 */ 251 public static String quoteBackslash(String string) { 252 return string.replace("\\", "\\\\"); 253 } 254}