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