001/** 002 * 003 * Copyright 2014-2017 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("=", 2); 119 String key = keyValue[0]; 120 // RFC 2831 § 7.1 about the formatting of the digest-challenge: 121 // "The full form is "<n>#<m>element" indicating at least <n> and 122 // at most <m> elements, each separated by one or more commas 123 // (",") and OPTIONAL linear white space (LWS)." 124 // Which means the key value may be preceded by whitespace, 125 // which is what we remove: *Only the preceding whitespace*. 126 key = key.replaceFirst("^\\s+", ""); 127 String value = keyValue[1]; 128 if ("nonce".equals(key)) { 129 if (nonce != null) { 130 throw new SmackException("Nonce value present multiple times"); 131 } 132 nonce = value.replace("\"", ""); 133 } 134 else if ("qop".equals(key)) { 135 value = value.replace("\"", ""); 136 if (!value.equals("auth")) { 137 throw new SmackException("Unsupported qop operation: " + value); 138 } 139 } 140 } 141 if (nonce == null) { 142 // RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly 143 // once; if not present, or if multiple instances are present, the client should 144 // abort the authentication exchange." 145 throw new SmackException("nonce value not present in initial challenge"); 146 } 147 // RFC 2831 2.1.2.1 defines A1, A2, KD and response-value 148 byte[] a1FirstPart = MD5.bytes(authenticationId + ':' + serviceName + ':' 149 + password); 150 cnonce = StringUtils.randomString(32); 151 byte[] a1 = ByteUtils.concat(a1FirstPart, toBytes(':' + nonce + ':' + cnonce)); 152 digestUri = "xmpp/" + serviceName; 153 hex_hashed_a1 = StringUtils.encodeHex(MD5.bytes(a1)); 154 String responseValue = calcResponse(DigestType.ClientResponse); 155 // @formatter:off 156 // See RFC 2831 2.1.2 digest-response 157 String authzid; 158 if (authorizationId == null) { 159 authzid = ""; 160 } else { 161 authzid = ",authzid=\"" + authorizationId + '"'; 162 } 163 String saslString = "username=\"" + quoteBackslash(authenticationId) + '"' 164 + authzid 165 + ",realm=\"" + serviceName + '"' 166 + ",nonce=\"" + nonce + '"' 167 + ",cnonce=\"" + cnonce + '"' 168 + ",nc=" + INITAL_NONCE 169 + ",qop=auth" 170 + ",digest-uri=\"" + digestUri + '"' 171 + ",response=" + responseValue 172 + ",charset=utf-8"; 173 // @formatter:on 174 response = toBytes(saslString); 175 state = State.RESPONSE_SENT; 176 break; 177 case RESPONSE_SENT: 178 if (verifyServerResponse) { 179 String serverResponse = null; 180 for (String part : challengeParts) { 181 String[] keyValue = part.split("="); 182 assert (keyValue.length == 2); 183 String key = keyValue[0]; 184 String value = keyValue[1]; 185 if ("rspauth".equals(key)) { 186 serverResponse = value; 187 break; 188 } 189 } 190 if (serverResponse == null) { 191 throw new SmackException("No server response received while performing " + NAME 192 + " authentication"); 193 } 194 String expectedServerResponse = calcResponse(DigestType.ServerResponse); 195 if (!serverResponse.equals(expectedServerResponse)) { 196 throw new SmackException("Invalid server response while performing " + NAME 197 + " authentication"); 198 } 199 } 200 state = State.VALID_SERVER_RESPONSE; 201 break; 202 default: 203 throw new IllegalStateException(); 204 } 205 return response; 206 } 207 208 private enum DigestType { 209 ClientResponse, 210 ServerResponse 211 } 212 213 private String calcResponse(DigestType digestType) { 214 StringBuilder a2 = new StringBuilder(); 215 if (digestType == DigestType.ClientResponse) { 216 a2.append("AUTHENTICATE"); 217 } 218 a2.append(':'); 219 a2.append(digestUri); 220 String hex_hashed_a2 = StringUtils.encodeHex(MD5.bytes(a2.toString())); 221 222 StringBuilder kd_argument = new StringBuilder(); 223 kd_argument.append(hex_hashed_a1); 224 kd_argument.append(':'); 225 kd_argument.append(nonce); 226 kd_argument.append(':'); 227 kd_argument.append(INITAL_NONCE); 228 kd_argument.append(':'); 229 kd_argument.append(cnonce); 230 kd_argument.append(':'); 231 kd_argument.append(QOP_VALUE); 232 kd_argument.append(':'); 233 kd_argument.append(hex_hashed_a2); 234 byte[] kd = MD5.bytes(kd_argument.toString()); 235 String responseValue = StringUtils.encodeHex(kd); 236 return responseValue; 237 } 238 239 /** 240 * Quote the backslash in the given String. Replaces all occurrences of "\" with "\\". 241 * <p> 242 * According to RFC 2831 § 7.2 a quoted-string consists either of qdtext or quoted-pair. And since quoted-pair is a 243 * backslash followed by a char, every backslash in qdtext must be quoted, since it otherwise would be treated as 244 * qdtext. 245 * </p> 246 * 247 * @param string the input string. 248 * @return the input string where the every backslash is quoted. 249 */ 250 public static String quoteBackslash(String string) { 251 return string.replace("\\", "\\\\"); 252 } 253}