J. Duke | 319a3b9 | 2007-12-01 00:00:00 +0000 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright 2003-2006 Sun Microsystems, Inc. All Rights Reserved. |
| 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | * |
| 5 | * This code is free software; you can redistribute it and/or modify it |
| 6 | * under the terms of the GNU General Public License version 2 only, as |
| 7 | * published by the Free Software Foundation. Sun designates this |
| 8 | * particular file as subject to the "Classpath" exception as provided |
| 9 | * by Sun in the LICENSE file that accompanied this code. |
| 10 | * |
| 11 | * This code is distributed in the hope that it will be useful, but WITHOUT |
| 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 14 | * version 2 for more details (a copy is included in the LICENSE file that |
| 15 | * accompanied this code). |
| 16 | * |
| 17 | * You should have received a copy of the GNU General Public License version |
| 18 | * 2 along with this work; if not, write to the Free Software Foundation, |
| 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | * |
| 21 | * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, |
| 22 | * CA 95054 USA or visit www.sun.com if you need additional information or |
| 23 | * have any questions. |
| 24 | */ |
| 25 | |
| 26 | package com.sun.security.sasl.digest; |
| 27 | |
| 28 | import java.security.AccessController; |
| 29 | import java.security.Provider; |
| 30 | import java.security.MessageDigest; |
| 31 | import java.security.NoSuchAlgorithmException; |
| 32 | import java.io.ByteArrayOutputStream; |
| 33 | import java.io.ByteArrayInputStream; |
| 34 | import java.io.IOException; |
| 35 | import java.io.UnsupportedEncodingException; |
| 36 | import java.util.Random; |
| 37 | import java.util.StringTokenizer; |
| 38 | import java.util.ArrayList; |
| 39 | import java.util.List; |
| 40 | import java.util.Map; |
| 41 | import java.util.Set; |
| 42 | import java.util.Arrays; |
| 43 | |
| 44 | import java.util.logging.Logger; |
| 45 | import java.util.logging.Level; |
| 46 | |
| 47 | import javax.security.sasl.*; |
| 48 | import javax.security.auth.callback.*; |
| 49 | |
| 50 | /** |
| 51 | * An implementation of the DIGEST-MD5 server SASL mechanism. |
| 52 | * (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>) |
| 53 | * <p> |
| 54 | * The DIGEST-MD5 SASL mechanism specifies two modes of authentication. |
| 55 | * <ul><li>Initial Authentication |
| 56 | * <li>Subsequent Authentication - optional, (currently not supported) |
| 57 | * </ul> |
| 58 | * |
| 59 | * Required callbacks: |
| 60 | * - RealmCallback |
| 61 | * used as key by handler to fetch password |
| 62 | * - NameCallback |
| 63 | * used as key by handler to fetch password |
| 64 | * - PasswordCallback |
| 65 | * handler must enter password for username/realm supplied |
| 66 | * - AuthorizeCallback |
| 67 | * handler must verify that authid/authzids are allowed and set |
| 68 | * authorized ID to be the canonicalized authzid (if applicable). |
| 69 | * |
| 70 | * Environment properties that affect the implementation: |
| 71 | * javax.security.sasl.qop: |
| 72 | * specifies list of qops; default is "auth"; typically, caller should set |
| 73 | * this to "auth, auth-int, auth-conf". |
| 74 | * javax.security.sasl.strength |
| 75 | * specifies low/medium/high strength of encryption; default is all available |
| 76 | * ciphers [high,medium,low]; high means des3 or rc4 (128); medium des or |
| 77 | * rc4-56; low is rc4-40. |
| 78 | * javax.security.sasl.maxbuf |
| 79 | * specifies max receive buf size; default is 65536 |
| 80 | * javax.security.sasl.sendmaxbuffer |
| 81 | * specifies max send buf size; default is 65536 (min of this and client's max |
| 82 | * recv size) |
| 83 | * |
| 84 | * com.sun.security.sasl.digest.utf8: |
| 85 | * "true" means to use UTF-8 charset; "false" to use ISO-8859-1 encoding; |
| 86 | * default is "true". |
| 87 | * com.sun.security.sasl.digest.realm: |
| 88 | * space-separated list of realms; default is server name (fqdn parameter) |
| 89 | * |
| 90 | * @author Rosanna Lee |
| 91 | */ |
| 92 | |
| 93 | final class DigestMD5Server extends DigestMD5Base implements SaslServer { |
| 94 | private static final String MY_CLASS_NAME = DigestMD5Server.class.getName(); |
| 95 | |
| 96 | private static final String UTF8_DIRECTIVE = "charset=utf-8,"; |
| 97 | private static final String ALGORITHM_DIRECTIVE = "algorithm=md5-sess"; |
| 98 | |
| 99 | /* |
| 100 | * Always expect nonce count value to be 1 because we support only |
| 101 | * initial authentication. |
| 102 | */ |
| 103 | private static final int NONCE_COUNT_VALUE = 1; |
| 104 | |
| 105 | /* "true" means use UTF8; "false" ISO 8859-1; default is "true" */ |
| 106 | private static final String UTF8_PROPERTY = |
| 107 | "com.sun.security.sasl.digest.utf8"; |
| 108 | |
| 109 | /* List of space-separated realms used for authentication */ |
| 110 | private static final String REALM_PROPERTY = |
| 111 | "com.sun.security.sasl.digest.realm"; |
| 112 | |
| 113 | /* Directives encountered in responses sent by the client. */ |
| 114 | private static final String[] DIRECTIVE_KEY = { |
| 115 | "username", // exactly once |
| 116 | "realm", // exactly once if sent by server |
| 117 | "nonce", // exactly once |
| 118 | "cnonce", // exactly once |
| 119 | "nonce-count", // atmost once; default is 00000001 |
| 120 | "qop", // atmost once; default is "auth" |
| 121 | "digest-uri", // atmost once; (default?) |
| 122 | "response", // exactly once |
| 123 | "maxbuf", // atmost once; default is 65536 |
| 124 | "charset", // atmost once; default is ISO-8859-1 |
| 125 | "cipher", // exactly once if qop is "auth-conf" |
| 126 | "authzid", // atmost once; default is none |
| 127 | "auth-param", // >= 0 times (ignored) |
| 128 | }; |
| 129 | |
| 130 | /* Indices into DIRECTIVE_KEY */ |
| 131 | private static final int USERNAME = 0; |
| 132 | private static final int REALM = 1; |
| 133 | private static final int NONCE = 2; |
| 134 | private static final int CNONCE = 3; |
| 135 | private static final int NONCE_COUNT = 4; |
| 136 | private static final int QOP = 5; |
| 137 | private static final int DIGEST_URI = 6; |
| 138 | private static final int RESPONSE = 7; |
| 139 | private static final int MAXBUF = 8; |
| 140 | private static final int CHARSET = 9; |
| 141 | private static final int CIPHER = 10; |
| 142 | private static final int AUTHZID = 11; |
| 143 | private static final int AUTH_PARAM = 12; |
| 144 | |
| 145 | /* Server-generated/supplied information */ |
| 146 | private String specifiedQops; |
| 147 | private byte[] myCiphers; |
| 148 | private List<String> serverRealms; |
| 149 | |
| 150 | DigestMD5Server(String protocol, String serverName, Map props, |
| 151 | CallbackHandler cbh) throws SaslException { |
| 152 | super(props, MY_CLASS_NAME, 1, protocol + "/" + serverName, cbh); |
| 153 | |
| 154 | serverRealms = new ArrayList<String>(); |
| 155 | |
| 156 | useUTF8 = true; // default |
| 157 | |
| 158 | if (props != null) { |
| 159 | specifiedQops = (String) props.get(Sasl.QOP); |
| 160 | if ("false".equals((String) props.get(UTF8_PROPERTY))) { |
| 161 | useUTF8 = false; |
| 162 | logger.log(Level.FINE, "DIGEST80:Server supports ISO-Latin-1"); |
| 163 | } |
| 164 | |
| 165 | String realms = (String) props.get(REALM_PROPERTY); |
| 166 | if (realms != null) { |
| 167 | StringTokenizer parser = new StringTokenizer(realms, ", \t\n"); |
| 168 | int tokenCount = parser.countTokens(); |
| 169 | String token = null; |
| 170 | for (int i = 0; i < tokenCount; i++) { |
| 171 | token = parser.nextToken(); |
| 172 | logger.log(Level.FINE, "DIGEST81:Server supports realm {0}", |
| 173 | token); |
| 174 | serverRealms.add(token); |
| 175 | } |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | encoding = (useUTF8 ? "UTF8" : "8859_1"); |
| 180 | |
| 181 | // By default, use server name as realm |
| 182 | if (serverRealms.size() == 0) { |
| 183 | serverRealms.add(serverName); |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | public byte[] evaluateResponse(byte[] response) throws SaslException { |
| 188 | if (response.length > MAX_RESPONSE_LENGTH) { |
| 189 | throw new SaslException( |
| 190 | "DIGEST-MD5: Invalid digest response length. Got: " + |
| 191 | response.length + " Expected < " + MAX_RESPONSE_LENGTH); |
| 192 | } |
| 193 | |
| 194 | byte[] challenge; |
| 195 | switch (step) { |
| 196 | case 1: |
| 197 | if (response.length != 0) { |
| 198 | throw new SaslException( |
| 199 | "DIGEST-MD5 must not have an initial response"); |
| 200 | } |
| 201 | |
| 202 | /* Generate first challenge */ |
| 203 | String supportedCiphers = null; |
| 204 | if ((allQop&PRIVACY_PROTECTION) != 0) { |
| 205 | myCiphers = getPlatformCiphers(); |
| 206 | StringBuffer buf = new StringBuffer(); |
| 207 | |
| 208 | // myCipher[i] is a byte that indicates whether CIPHER_TOKENS[i] |
| 209 | // is supported |
| 210 | for (int i = 0; i < CIPHER_TOKENS.length; i++) { |
| 211 | if (myCiphers[i] != 0) { |
| 212 | if (buf.length() > 0) { |
| 213 | buf.append(','); |
| 214 | } |
| 215 | buf.append(CIPHER_TOKENS[i]); |
| 216 | } |
| 217 | } |
| 218 | supportedCiphers = buf.toString(); |
| 219 | } |
| 220 | |
| 221 | try { |
| 222 | challenge = generateChallenge(serverRealms, specifiedQops, |
| 223 | supportedCiphers); |
| 224 | |
| 225 | step = 3; |
| 226 | return challenge; |
| 227 | } catch (UnsupportedEncodingException e) { |
| 228 | throw new SaslException( |
| 229 | "DIGEST-MD5: Error encoding challenge", e); |
| 230 | } catch (IOException e) { |
| 231 | throw new SaslException( |
| 232 | "DIGEST-MD5: Error generating challenge", e); |
| 233 | } |
| 234 | |
| 235 | // Step 2 is performed by client |
| 236 | |
| 237 | case 3: |
| 238 | /* Validates client's response and generate challenge: |
| 239 | * response-auth = "rspauth" "=" response-value |
| 240 | */ |
| 241 | try { |
| 242 | byte[][] responseVal = parseDirectives(response, DIRECTIVE_KEY, |
| 243 | null, REALM); |
| 244 | challenge = validateClientResponse(responseVal); |
| 245 | } catch (SaslException e) { |
| 246 | throw e; |
| 247 | } catch (UnsupportedEncodingException e) { |
| 248 | throw new SaslException( |
| 249 | "DIGEST-MD5: Error validating client response", e); |
| 250 | } finally { |
| 251 | step = 0; // Set to invalid state |
| 252 | } |
| 253 | |
| 254 | completed = true; |
| 255 | |
| 256 | /* Initialize SecurityCtx implementation */ |
| 257 | if (integrity && privacy) { |
| 258 | secCtx = new DigestPrivacy(false /* not client */); |
| 259 | } else if (integrity) { |
| 260 | secCtx = new DigestIntegrity(false /* not client */); |
| 261 | } |
| 262 | |
| 263 | return challenge; |
| 264 | |
| 265 | default: |
| 266 | // No other possible state |
| 267 | throw new SaslException("DIGEST-MD5: Server at illegal state"); |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Generates challenge to be sent to client. |
| 273 | * digest-challenge = |
| 274 | * 1#( realm | nonce | qop-options | stale | maxbuf | charset |
| 275 | * algorithm | cipher-opts | auth-param ) |
| 276 | * |
| 277 | * realm = "realm" "=" <"> realm-value <"> |
| 278 | * realm-value = qdstr-val |
| 279 | * nonce = "nonce" "=" <"> nonce-value <"> |
| 280 | * nonce-value = qdstr-val |
| 281 | * qop-options = "qop" "=" <"> qop-list <"> |
| 282 | * qop-list = 1#qop-value |
| 283 | * qop-value = "auth" | "auth-int" | "auth-conf" | |
| 284 | * token |
| 285 | * stale = "stale" "=" "true" |
| 286 | * maxbuf = "maxbuf" "=" maxbuf-value |
| 287 | * maxbuf-value = 1*DIGIT |
| 288 | * charset = "charset" "=" "utf-8" |
| 289 | * algorithm = "algorithm" "=" "md5-sess" |
| 290 | * cipher-opts = "cipher" "=" <"> 1#cipher-value <"> |
| 291 | * cipher-value = "3des" | "des" | "rc4-40" | "rc4" | |
| 292 | * "rc4-56" | token |
| 293 | * auth-param = token "=" ( token | quoted-string ) |
| 294 | */ |
| 295 | private byte[] generateChallenge(List<String> realms, String qopStr, |
| 296 | String cipherStr) throws UnsupportedEncodingException, IOException { |
| 297 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| 298 | |
| 299 | // Realms (>= 0) |
| 300 | for (int i = 0; realms != null && i < realms.size(); i++) { |
| 301 | out.write("realm=\"".getBytes(encoding)); |
| 302 | writeQuotedStringValue(out, realms.get(i).getBytes(encoding)); |
| 303 | out.write('"'); |
| 304 | out.write(','); |
| 305 | } |
| 306 | |
| 307 | // Nonce - required (1) |
| 308 | out.write(("nonce=\"").getBytes(encoding)); |
| 309 | nonce = generateNonce(); |
| 310 | writeQuotedStringValue(out, nonce); |
| 311 | out.write('"'); |
| 312 | out.write(','); |
| 313 | |
| 314 | // QOP - optional (1) [default: auth] |
| 315 | // qop="auth,auth-conf,auth-int" |
| 316 | if (qopStr != null) { |
| 317 | out.write(("qop=\"").getBytes(encoding)); |
| 318 | // Check for quotes in case of non-standard qop options |
| 319 | writeQuotedStringValue(out, qopStr.getBytes(encoding)); |
| 320 | out.write('"'); |
| 321 | out.write(','); |
| 322 | } |
| 323 | |
| 324 | // maxbuf - optional (1) [default: 65536] |
| 325 | if (recvMaxBufSize != DEFAULT_MAXBUF) { |
| 326 | out.write(("maxbuf=\"" + recvMaxBufSize + "\",").getBytes(encoding)); |
| 327 | } |
| 328 | |
| 329 | // charset - optional (1) [default: ISO 8859_1] |
| 330 | if (useUTF8) { |
| 331 | out.write(UTF8_DIRECTIVE.getBytes(encoding)); |
| 332 | } |
| 333 | |
| 334 | if (cipherStr != null) { |
| 335 | out.write("cipher=\"".getBytes(encoding)); |
| 336 | // Check for quotes in case of custom ciphers |
| 337 | writeQuotedStringValue(out, cipherStr.getBytes(encoding)); |
| 338 | out.write('"'); |
| 339 | out.write(','); |
| 340 | } |
| 341 | |
| 342 | // algorithm - required (1) |
| 343 | out.write(ALGORITHM_DIRECTIVE.getBytes(encoding)); |
| 344 | |
| 345 | return out.toByteArray(); |
| 346 | } |
| 347 | |
| 348 | /** |
| 349 | * Validates client's response. |
| 350 | * digest-response = 1#( username | realm | nonce | cnonce | |
| 351 | * nonce-count | qop | digest-uri | response | |
| 352 | * maxbuf | charset | cipher | authzid | |
| 353 | * auth-param ) |
| 354 | * |
| 355 | * username = "username" "=" <"> username-value <"> |
| 356 | * username-value = qdstr-val |
| 357 | * cnonce = "cnonce" "=" <"> cnonce-value <"> |
| 358 | * cnonce-value = qdstr-val |
| 359 | * nonce-count = "nc" "=" nc-value |
| 360 | * nc-value = 8LHEX |
| 361 | * qop = "qop" "=" qop-value |
| 362 | * digest-uri = "digest-uri" "=" <"> digest-uri-value <"> |
| 363 | * digest-uri-value = serv-type "/" host [ "/" serv-name ] |
| 364 | * serv-type = 1*ALPHA |
| 365 | * host = 1*( ALPHA | DIGIT | "-" | "." ) |
| 366 | * serv-name = host |
| 367 | * response = "response" "=" response-value |
| 368 | * response-value = 32LHEX |
| 369 | * LHEX = "0" | "1" | "2" | "3" | |
| 370 | * "4" | "5" | "6" | "7" | |
| 371 | * "8" | "9" | "a" | "b" | |
| 372 | * "c" | "d" | "e" | "f" |
| 373 | * cipher = "cipher" "=" cipher-value |
| 374 | * authzid = "authzid" "=" <"> authzid-value <"> |
| 375 | * authzid-value = qdstr-val |
| 376 | * sets: |
| 377 | * negotiatedQop |
| 378 | * negotiatedCipher |
| 379 | * negotiatedRealm |
| 380 | * negotiatedStrength |
| 381 | * digestUri (checked and set to clients to account for case diffs) |
| 382 | * sendMaxBufSize |
| 383 | * authzid (gotten from callback) |
| 384 | * @return response-value ('rspauth') for client to validate |
| 385 | */ |
| 386 | private byte[] validateClientResponse(byte[][] responseVal) |
| 387 | throws SaslException, UnsupportedEncodingException { |
| 388 | |
| 389 | /* CHARSET: optional atmost once */ |
| 390 | if (responseVal[CHARSET] != null) { |
| 391 | // The client should send this directive only if the server has |
| 392 | // indicated it supports UTF-8. |
| 393 | if (!useUTF8 || |
| 394 | !"utf-8".equals(new String(responseVal[CHARSET], encoding))) { |
| 395 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 396 | "violation. Incompatible charset value: " + |
| 397 | new String(responseVal[CHARSET])); |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | // maxbuf: atmost once |
| 402 | int clntMaxBufSize = |
| 403 | (responseVal[MAXBUF] == null) ? DEFAULT_MAXBUF |
| 404 | : Integer.parseInt(new String(responseVal[MAXBUF], encoding)); |
| 405 | |
| 406 | // Max send buf size is min of client's max recv buf size and |
| 407 | // server's max send buf size |
| 408 | sendMaxBufSize = ((sendMaxBufSize == 0) ? clntMaxBufSize : |
| 409 | Math.min(sendMaxBufSize, clntMaxBufSize)); |
| 410 | |
| 411 | /* username: exactly once */ |
| 412 | String username; |
| 413 | if (responseVal[USERNAME] != null) { |
| 414 | username = new String(responseVal[USERNAME], encoding); |
| 415 | logger.log(Level.FINE, "DIGEST82:Username: {0}", username); |
| 416 | } else { |
| 417 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 418 | "violation. Missing username."); |
| 419 | } |
| 420 | |
| 421 | /* realm: exactly once if sent by server */ |
| 422 | negotiatedRealm = ((responseVal[REALM] != null) ? |
| 423 | new String(responseVal[REALM], encoding) : ""); |
| 424 | logger.log(Level.FINE, "DIGEST83:Client negotiated realm: {0}", |
| 425 | negotiatedRealm); |
| 426 | |
| 427 | if (!serverRealms.contains(negotiatedRealm)) { |
| 428 | // Server had sent at least one realm |
| 429 | // Check that response is one of these |
| 430 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 431 | "violation. Nonexistent realm: " + negotiatedRealm); |
| 432 | } |
| 433 | // Else, client specified realm was one of server's or server had none |
| 434 | |
| 435 | /* nonce: exactly once */ |
| 436 | if (responseVal[NONCE] == null) { |
| 437 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 438 | "violation. Missing nonce."); |
| 439 | } |
| 440 | byte[] nonceFromClient = responseVal[NONCE]; |
| 441 | if (!Arrays.equals(nonceFromClient, nonce)) { |
| 442 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 443 | "violation. Mismatched nonce."); |
| 444 | } |
| 445 | |
| 446 | /* cnonce: exactly once */ |
| 447 | if (responseVal[CNONCE] == null) { |
| 448 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 449 | "violation. Missing cnonce."); |
| 450 | } |
| 451 | byte[] cnonce = responseVal[CNONCE]; |
| 452 | |
| 453 | /* nonce-count: atmost once */ |
| 454 | if (responseVal[NONCE_COUNT] != null && |
| 455 | NONCE_COUNT_VALUE != Integer.parseInt( |
| 456 | new String(responseVal[NONCE_COUNT], encoding), 16)) { |
| 457 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 458 | "violation. Nonce count does not match: " + |
| 459 | new String(responseVal[NONCE_COUNT])); |
| 460 | } |
| 461 | |
| 462 | /* qop: atmost once; default is "auth" */ |
| 463 | negotiatedQop = ((responseVal[QOP] != null) ? |
| 464 | new String(responseVal[QOP], encoding) : "auth"); |
| 465 | |
| 466 | logger.log(Level.FINE, "DIGEST84:Client negotiated qop: {0}", |
| 467 | negotiatedQop); |
| 468 | |
| 469 | // Check that QOP is one sent by server |
| 470 | byte cQop; |
| 471 | if (negotiatedQop.equals("auth")) { |
| 472 | cQop = NO_PROTECTION; |
| 473 | } else if (negotiatedQop.equals("auth-int")) { |
| 474 | cQop = INTEGRITY_ONLY_PROTECTION; |
| 475 | integrity = true; |
| 476 | rawSendSize = sendMaxBufSize - 16; |
| 477 | } else if (negotiatedQop.equals("auth-conf")) { |
| 478 | cQop = PRIVACY_PROTECTION; |
| 479 | integrity = privacy = true; |
| 480 | rawSendSize = sendMaxBufSize - 26; |
| 481 | } else { |
| 482 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 483 | "violation. Invalid QOP: " + negotiatedQop); |
| 484 | } |
| 485 | if ((cQop&allQop) == 0) { |
| 486 | throw new SaslException("DIGEST-MD5: server does not support " + |
| 487 | " qop: " + negotiatedQop); |
| 488 | } |
| 489 | |
| 490 | if (privacy) { |
| 491 | negotiatedCipher = ((responseVal[CIPHER] != null) ? |
| 492 | new String(responseVal[CIPHER], encoding) : null); |
| 493 | if (negotiatedCipher == null) { |
| 494 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 495 | "violation. No cipher specified."); |
| 496 | } |
| 497 | |
| 498 | int foundCipher = -1; |
| 499 | logger.log(Level.FINE, "DIGEST85:Client negotiated cipher: {0}", |
| 500 | negotiatedCipher); |
| 501 | |
| 502 | // Check that cipher is one that we offered |
| 503 | for (int j = 0; j < CIPHER_TOKENS.length; j++) { |
| 504 | if (negotiatedCipher.equals(CIPHER_TOKENS[j]) && |
| 505 | myCiphers[j] != 0) { |
| 506 | foundCipher = j; |
| 507 | break; |
| 508 | } |
| 509 | } |
| 510 | if (foundCipher == -1) { |
| 511 | throw new SaslException("DIGEST-MD5: server does not " + |
| 512 | "support cipher: " + negotiatedCipher); |
| 513 | } |
| 514 | // Set negotiatedStrength |
| 515 | if ((CIPHER_MASKS[foundCipher]&HIGH_STRENGTH) != 0) { |
| 516 | negotiatedStrength = "high"; |
| 517 | } else if ((CIPHER_MASKS[foundCipher]&MEDIUM_STRENGTH) != 0) { |
| 518 | negotiatedStrength = "medium"; |
| 519 | } else { |
| 520 | // assume default low |
| 521 | negotiatedStrength = "low"; |
| 522 | } |
| 523 | |
| 524 | logger.log(Level.FINE, "DIGEST86:Negotiated strength: {0}", |
| 525 | negotiatedStrength); |
| 526 | } |
| 527 | |
| 528 | // atmost once |
| 529 | String digestUriFromResponse = ((responseVal[DIGEST_URI]) != null ? |
| 530 | new String(responseVal[DIGEST_URI], encoding) : null); |
| 531 | |
| 532 | if (digestUriFromResponse != null) { |
| 533 | logger.log(Level.FINE, "DIGEST87:digest URI: {0}", |
| 534 | digestUriFromResponse); |
| 535 | } |
| 536 | |
| 537 | // serv-type "/" host [ "/" serv-name ] |
| 538 | // e.g.: smtp/mail3.example.com/example.com |
| 539 | // e.g.: ftp/ftp.example.com |
| 540 | // e.g.: ldap/ldapserver.example.com |
| 541 | |
| 542 | // host should match one of service's configured service names |
| 543 | // Check against digest URI that mech was created with |
| 544 | |
| 545 | if (digestUri.equalsIgnoreCase(digestUriFromResponse)) { |
| 546 | digestUri = digestUriFromResponse; // account for case-sensitive diffs |
| 547 | } else { |
| 548 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 549 | "violation. Mismatched URI: " + digestUriFromResponse + |
| 550 | "; expecting: " + digestUri); |
| 551 | } |
| 552 | |
| 553 | // response: exactly once |
| 554 | byte[] responseFromClient = responseVal[RESPONSE]; |
| 555 | if (responseFromClient == null) { |
| 556 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 557 | " violation. Missing response."); |
| 558 | } |
| 559 | |
| 560 | // authzid: atmost once |
| 561 | byte[] authzidBytes; |
| 562 | String authzidFromClient = ((authzidBytes=responseVal[AUTHZID]) != null? |
| 563 | new String(authzidBytes, encoding) : username); |
| 564 | |
| 565 | if (authzidBytes != null) { |
| 566 | logger.log(Level.FINE, "DIGEST88:Authzid: {0}", |
| 567 | new String(authzidBytes)); |
| 568 | } |
| 569 | |
| 570 | // Ignore auth-param |
| 571 | |
| 572 | // Get password need to generate verifying response |
| 573 | char[] passwd; |
| 574 | try { |
| 575 | // Realm and Name callbacks are used to provide info |
| 576 | RealmCallback rcb = new RealmCallback("DIGEST-MD5 realm: ", |
| 577 | negotiatedRealm); |
| 578 | NameCallback ncb = new NameCallback("DIGEST-MD5 authentication ID: ", |
| 579 | username); |
| 580 | |
| 581 | // PasswordCallback is used to collect info |
| 582 | PasswordCallback pcb = |
| 583 | new PasswordCallback("DIGEST-MD5 password: ", false); |
| 584 | |
| 585 | cbh.handle(new Callback[] {rcb, ncb, pcb}); |
| 586 | passwd = pcb.getPassword(); |
| 587 | pcb.clearPassword(); |
| 588 | |
| 589 | } catch (UnsupportedCallbackException e) { |
| 590 | throw new SaslException( |
| 591 | "DIGEST-MD5: Cannot perform callback to acquire password", e); |
| 592 | |
| 593 | } catch (IOException e) { |
| 594 | throw new SaslException( |
| 595 | "DIGEST-MD5: IO error acquiring password", e); |
| 596 | } |
| 597 | |
| 598 | if (passwd == null) { |
| 599 | throw new SaslException( |
| 600 | "DIGEST-MD5: cannot acquire password for " + username + |
| 601 | " in realm : " + negotiatedRealm); |
| 602 | } |
| 603 | |
| 604 | try { |
| 605 | // Validate response value sent by client |
| 606 | byte[] expectedResponse; |
| 607 | |
| 608 | try { |
| 609 | expectedResponse = generateResponseValue("AUTHENTICATE", |
| 610 | digestUri, negotiatedQop, username, negotiatedRealm, |
| 611 | passwd, nonce /* use own nonce */, |
| 612 | cnonce, NONCE_COUNT_VALUE, authzidBytes); |
| 613 | |
| 614 | } catch (NoSuchAlgorithmException e) { |
| 615 | throw new SaslException( |
| 616 | "DIGEST-MD5: problem duplicating client response", e); |
| 617 | } catch (IOException e) { |
| 618 | throw new SaslException( |
| 619 | "DIGEST-MD5: problem duplicating client response", e); |
| 620 | } |
| 621 | |
| 622 | if (!Arrays.equals(responseFromClient, expectedResponse)) { |
| 623 | throw new SaslException("DIGEST-MD5: digest response format " + |
| 624 | "violation. Mismatched response."); |
| 625 | } |
| 626 | |
| 627 | // Ensure that authzid mapping is OK |
| 628 | try { |
| 629 | AuthorizeCallback acb = |
| 630 | new AuthorizeCallback(username, authzidFromClient); |
| 631 | cbh.handle(new Callback[]{acb}); |
| 632 | |
| 633 | if (acb.isAuthorized()) { |
| 634 | authzid = acb.getAuthorizedID(); |
| 635 | } else { |
| 636 | throw new SaslException("DIGEST-MD5: " + username + |
| 637 | " is not authorized to act as " + authzidFromClient); |
| 638 | } |
| 639 | } catch (SaslException e) { |
| 640 | throw e; |
| 641 | } catch (UnsupportedCallbackException e) { |
| 642 | throw new SaslException( |
| 643 | "DIGEST-MD5: Cannot perform callback to check authzid", e); |
| 644 | } catch (IOException e) { |
| 645 | throw new SaslException( |
| 646 | "DIGEST-MD5: IO error checking authzid", e); |
| 647 | } |
| 648 | |
| 649 | return generateResponseAuth(username, passwd, cnonce, |
| 650 | NONCE_COUNT_VALUE, authzidBytes); |
| 651 | } finally { |
| 652 | // Clear password |
| 653 | for (int i = 0; i < passwd.length; i++) { |
| 654 | passwd[i] = 0; |
| 655 | } |
| 656 | } |
| 657 | } |
| 658 | |
| 659 | /** |
| 660 | * Server sends a message formatted as follows: |
| 661 | * response-auth = "rspauth" "=" response-value |
| 662 | * where response-value is calculated as above, using the values sent in |
| 663 | * step two, except that if qop is "auth", then A2 is |
| 664 | * |
| 665 | * A2 = { ":", digest-uri-value } |
| 666 | * |
| 667 | * And if qop is "auth-int" or "auth-conf" then A2 is |
| 668 | * |
| 669 | * A2 = { ":", digest-uri-value, ":00000000000000000000000000000000" } |
| 670 | * |
| 671 | * Clears password afterwards. |
| 672 | */ |
| 673 | private byte[] generateResponseAuth(String username, char[] passwd, |
| 674 | byte[] cnonce, int nonceCount, byte[] authzidBytes) throws SaslException { |
| 675 | |
| 676 | // Construct response value |
| 677 | |
| 678 | try { |
| 679 | byte[] responseValue = generateResponseValue("", |
| 680 | digestUri, negotiatedQop, username, negotiatedRealm, |
| 681 | passwd, nonce, cnonce, nonceCount, authzidBytes); |
| 682 | |
| 683 | byte[] challenge = new byte[responseValue.length + 8]; |
| 684 | System.arraycopy("rspauth=".getBytes(encoding), 0, challenge, 0, 8); |
| 685 | System.arraycopy(responseValue, 0, challenge, 8, |
| 686 | responseValue.length ); |
| 687 | |
| 688 | return challenge; |
| 689 | |
| 690 | } catch (NoSuchAlgorithmException e) { |
| 691 | throw new SaslException("DIGEST-MD5: problem generating response", e); |
| 692 | } catch (IOException e) { |
| 693 | throw new SaslException("DIGEST-MD5: problem generating response", e); |
| 694 | } |
| 695 | } |
| 696 | |
| 697 | public String getAuthorizationID() { |
| 698 | if (completed) { |
| 699 | return authzid; |
| 700 | } else { |
| 701 | throw new IllegalStateException( |
| 702 | "DIGEST-MD5 server negotiation not complete"); |
| 703 | } |
| 704 | } |
| 705 | } |