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 String value = keyValue[1]; 108 if ("nonce".equals(key)) { 109 if (nonce != null) { 110 throw new SmackException("Nonce value present multiple times"); 111 } 112 nonce = value.replace("\"", ""); 113 } 114 else if ("qop".equals(key)) { 115 value = value.replace("\"", ""); 116 if (!value.equals("auth")) { 117 throw new SmackException("Unsupported qop operation: " + value); 118 } 119 } 120 } 121 if (nonce == null) { 122 // RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly 123 // once; if not present, or if multiple instances are present, the client should 124 // abort the authentication exchange." 125 throw new SmackException("nonce value not present in initial challenge"); 126 } 127 // RFC 2831 2.1.2.1 defines A1, A2, KD and response-value 128 byte[] a1FirstPart = MD5.bytes(authenticationId + ':' + serviceName + ':' 129 + password); 130 cnonce = StringUtils.randomString(32); 131 byte[] a1 = ByteUtils.concact(a1FirstPart, toBytes(':' + nonce + ':' + cnonce)); 132 digestUri = "xmpp/" + serviceName; 133 hex_hashed_a1 = StringUtils.encodeHex(MD5.bytes(a1)); 134 String responseValue = calcResponse(DigestType.ClientResponse); 135 // @formatter:off 136 // See RFC 2831 2.1.2 digest-response 137 String saslString = "username=\"" + authenticationId + '"' 138 + ",realm=\"" + serviceName + '"' 139 + ",nonce=\"" + nonce + '"' 140 + ",cnonce=\"" + cnonce + '"' 141 + ",nc=" + INITAL_NONCE 142 + ",qop=auth" 143 + ",digest-uri=\"" + digestUri + '"' 144 + ",response=" + responseValue 145 + ",charset=utf-8"; 146 // @formatter:on 147 response = toBytes(saslString); 148 state = State.RESPONSE_SENT; 149 break; 150 case RESPONSE_SENT: 151 if (verifyServerResponse) { 152 String serverResponse = null; 153 for (String part : challengeParts) { 154 String[] keyValue = part.split("="); 155 assert (keyValue.length == 2); 156 String key = keyValue[0]; 157 String value = keyValue[1]; 158 if ("rspauth".equals(key)) { 159 serverResponse = value; 160 break; 161 } 162 } 163 if (serverResponse == null) { 164 throw new SmackException("No server response received while performing " + NAME 165 + " authentication"); 166 } 167 String expectedServerResponse = calcResponse(DigestType.ServerResponse); 168 if (!serverResponse.equals(expectedServerResponse)) { 169 throw new SmackException("Invalid server response while performing " + NAME 170 + " authentication"); 171 } 172 } 173 state = State.VALID_SERVER_RESPONSE; 174 break; 175 default: 176 throw new IllegalStateException(); 177 } 178 return response; 179 } 180 181 private enum DigestType { 182 ClientResponse, 183 ServerResponse 184 } 185 186 private String calcResponse(DigestType digestType) { 187 StringBuilder a2 = new StringBuilder(); 188 if (digestType == DigestType.ClientResponse) { 189 a2.append("AUTHENTICATE"); 190 } 191 a2.append(':'); 192 a2.append(digestUri); 193 String hex_hashed_a2 = StringUtils.encodeHex(MD5.bytes(a2.toString())); 194 195 StringBuilder kd_argument = new StringBuilder(); 196 kd_argument.append(hex_hashed_a1); 197 kd_argument.append(':'); 198 kd_argument.append(nonce); 199 kd_argument.append(':'); 200 kd_argument.append(INITAL_NONCE); 201 kd_argument.append(':'); 202 kd_argument.append(cnonce); 203 kd_argument.append(':'); 204 kd_argument.append(QOP_VALUE); 205 kd_argument.append(':'); 206 kd_argument.append(hex_hashed_a2); 207 byte[] kd = MD5.bytes(kd_argument.toString()); 208 String responseValue = StringUtils.encodeHex(kd); 209 return responseValue; 210 } 211 212}