1 /*
   2  * Copyright (c) 2000, 2013, Oracle and/or its affiliates. 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.security.sasl.digest;
  27 
  28 import java.security.NoSuchAlgorithmException;
  29 import java.io.ByteArrayOutputStream;
  30 import java.io.IOException;
  31 import java.io.UnsupportedEncodingException;
  32 import java.util.StringTokenizer;
  33 import java.util.ArrayList;
  34 import java.util.List;
  35 import java.util.Map;
  36 import java.util.Arrays;
  37 
  38 import java.util.logging.Level;
  39 
  40 import javax.security.sasl.*;
  41 import javax.security.auth.callback.CallbackHandler;
  42 import javax.security.auth.callback.PasswordCallback;
  43 import javax.security.auth.callback.NameCallback;
  44 import javax.security.auth.callback.Callback;
  45 import javax.security.auth.callback.UnsupportedCallbackException;
  46 
  47 /**
  48   * An implementation of the DIGEST-MD5
  49   * (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>) SASL
  50   * (<a href="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222</a>) mechanism.
  51   *
  52   * The DIGEST-MD5 SASL mechanism specifies two modes of authentication.
  53   * - Initial Authentication
  54   * - Subsequent Authentication - optional, (currently unsupported)
  55   *
  56   * Required callbacks:
  57   * - RealmChoiceCallback
  58   *    shows user list of realms server has offered; handler must choose one
  59   *    from list
  60   * - RealmCallback
  61   *    shows user the only realm server has offered or none; handler must
  62   *    enter realm to use
  63   * - NameCallback
  64   *    handler must enter username to use for authentication
  65   * - PasswordCallback
  66   *    handler must enter password for username to use for authentication
  67   *
  68   * Environment properties that affect behavior of implementation:
  69   *
  70   * javax.security.sasl.qop
  71   *    quality of protection; list of auth, auth-int, auth-conf; default is "auth"
  72   * javax.security.sasl.strength
  73   *    auth-conf strength; list of high, medium, low; default is highest
  74   *    available on platform ["high,medium,low"].
  75   *    high means des3 or rc4 (128); medium des or rc4-56; low is rc4-40;
  76   *    choice of cipher depends on its availablility on platform
  77   * javax.security.sasl.maxbuf
  78   *    max receive buffer size; default is 65536
  79   * javax.security.sasl.sendmaxbuffer
  80   *    max send buffer size; default is 65536; (min with server max recv size)
  81   *
  82   * com.sun.security.sasl.digest.cipher
  83   *    name a specific cipher to use; setting must be compatible with the
  84   *    setting of the javax.security.sasl.strength property.
  85   *
  86   * @see <a href="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222</a>
  87   * - Simple Authentication and Security Layer (SASL)
  88   * @see <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>
  89   * - Using Digest Authentication as a SASL Mechanism
  90   * @see <a href="http://java.sun.com/products/jce">Java(TM)
  91   * Cryptography Extension 1.2.1 (JCE)</a>
  92   * @see <a href="http://java.sun.com/products/jaas">Java(TM)
  93   * Authentication and Authorization Service (JAAS)</a>
  94   *
  95   * @author Jonathan Bruce
  96   * @author Rosanna Lee
  97   */
  98 final class DigestMD5Client extends DigestMD5Base implements SaslClient {
  99     private static final String MY_CLASS_NAME = DigestMD5Client.class.getName();
 100 
 101     // Property for specifying cipher explicitly
 102     private static final String CIPHER_PROPERTY =
 103         "com.sun.security.sasl.digest.cipher";
 104 
 105     /* Directives encountered in challenges sent by the server. */
 106     private static final String[] DIRECTIVE_KEY = {
 107         "realm",      // >= 0 times
 108         "qop",        // atmost once; default is "auth"
 109         "algorithm",  // exactly once
 110         "nonce",      // exactly once
 111         "maxbuf",     // atmost once; default is 65536
 112         "charset",    // atmost once; default is ISO 8859-1
 113         "cipher",     // exactly once if qop is "auth-conf"
 114         "rspauth",    // exactly once in 2nd challenge
 115         "stale",      // atmost once for in subsequent auth (not supported)
 116     };
 117 
 118     /* Indices into DIRECTIVE_KEY */
 119     private static final int REALM = 0;
 120     private static final int QOP = 1;
 121     private static final int ALGORITHM = 2;
 122     private static final int NONCE = 3;
 123     private static final int MAXBUF = 4;
 124     private static final int CHARSET = 5;
 125     private static final int CIPHER = 6;
 126     private static final int RESPONSE_AUTH = 7;
 127     private static final int STALE = 8;
 128 
 129     private int nonceCount; // number of times nonce has been used/seen
 130 
 131     /* User-supplied/generated information */
 132     private String specifiedCipher;  // cipher explicitly requested by user
 133     private byte[] cnonce;        // client generated nonce
 134     private String username;
 135     private char[] passwd;
 136     private byte[] authzidBytes;  // byte repr of authzid
 137 
 138     /**
 139       * Constructor for DIGEST-MD5 mechanism.
 140       *
 141       * @param authzid A non-null String representing the principal
 142       * for which authorization is being granted..
 143       * @param digestURI A non-null String representing detailing the
 144       * combined protocol and host being used for authentication.
 145       * @param props The possibly null properties to be used by the SASL
 146       * mechanism to configure the authentication exchange.
 147       * @param cbh The non-null CallbackHanlder object for callbacks
 148       * @throws SaslException if no authentication ID or password is supplied
 149       */
 150     DigestMD5Client(String authzid, String protocol, String serverName,
 151         Map<String, ?> props, CallbackHandler cbh) throws SaslException {
 152 
 153         super(props, MY_CLASS_NAME, 2, protocol + "/" + serverName, cbh);
 154 
 155         // authzID can only be encoded in UTF8 - RFC 2222
 156         if (authzid != null) {
 157             this.authzid = authzid;
 158             try {
 159                 authzidBytes = authzid.getBytes("UTF8");
 160 
 161             } catch (UnsupportedEncodingException e) {
 162                 throw new SaslException(
 163                     "DIGEST-MD5: Error encoding authzid value into UTF-8", e);
 164             }
 165         }
 166 
 167         if (props != null) {
 168             specifiedCipher = (String)props.get(CIPHER_PROPERTY);
 169 
 170             logger.log(Level.FINE, "DIGEST60:Explicitly specified cipher: {0}",
 171                 specifiedCipher);
 172         }
 173    }
 174 
 175     /**
 176      * DIGEST-MD5 has no initial response
 177      *
 178      * @return false
 179      */
 180     public boolean hasInitialResponse() {
 181         return false;
 182     }
 183 
 184     /**
 185      * Process the challenge data.
 186      *
 187      * The server sends a digest-challenge which the client must reply to
 188      * in a digest-response. When the authentication is complete, the
 189      * completed field is set to true.
 190      *
 191      * @param challengeData A non-null byte array containing the challenge
 192      * data from the server.
 193      * @return A possibly null byte array containing the response to
 194      * be sent to the server.
 195      *
 196      * @throws SaslException If the platform does not have MD5 digest support
 197      * or if the server sends an invalid challenge.
 198      */
 199     public byte[] evaluateChallenge(byte[] challengeData) throws SaslException {
 200 
 201         if (challengeData.length > MAX_CHALLENGE_LENGTH) {
 202             throw new SaslException(
 203                 "DIGEST-MD5: Invalid digest-challenge length. Got:  " +
 204                 challengeData.length + " Expected < " + MAX_CHALLENGE_LENGTH);
 205         }
 206 
 207         /* Extract and process digest-challenge */
 208         byte[][] challengeVal;
 209 
 210         switch (step) {
 211         case 2:
 212             /* Process server's first challenge (from Step 1) */
 213             /* Get realm, qop, maxbuf, charset, algorithm, cipher, nonce
 214                directives */
 215             List<byte[]> realmChoices = new ArrayList<byte[]>(3);
 216             challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,
 217                 realmChoices, REALM);
 218 
 219             try {
 220                 processChallenge(challengeVal, realmChoices);
 221                 checkQopSupport(challengeVal[QOP], challengeVal[CIPHER]);
 222                 ++step;
 223                 return generateClientResponse(challengeVal[CHARSET]);
 224             } catch (SaslException e) {
 225                 step = 0;
 226                 clearPassword();
 227                 throw e; // rethrow
 228             } catch (IOException e) {
 229                 step = 0;
 230                 clearPassword();
 231                 throw new SaslException("DIGEST-MD5: Error generating " +
 232                     "digest response-value", e);
 233             }
 234 
 235         case 3:
 236             try {
 237                 /* Process server's step 3 (server response to digest response) */
 238                 /* Get rspauth directive */
 239                 challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,
 240                     null, REALM);
 241                 validateResponseValue(challengeVal[RESPONSE_AUTH]);
 242 
 243 
 244                 /* Initialize SecurityCtx implementation */
 245                 if (integrity && privacy) {
 246                     secCtx = new DigestPrivacy(true /* client */);
 247                 } else if (integrity) {
 248                     secCtx = new DigestIntegrity(true /* client */);
 249                 }
 250 
 251                 return null; // Mechanism has completed.
 252             } finally {
 253                 clearPassword();
 254                 step = 0;  // Set to invalid state
 255                 completed = true;
 256             }
 257 
 258         default:
 259             // No other possible state
 260             throw new SaslException("DIGEST-MD5: Client at illegal state");
 261         }
 262     }
 263 
 264 
 265    /**
 266     * Record information from the challengeVal array into variables/fields.
 267     * Check directive values that are multi-valued and ensure that mandatory
 268     * directives not missing from the digest-challenge.
 269     *
 270     * @throws SaslException if a sasl is a the mechanism cannot
 271     * correcly handle a callbacks or if a violation in the
 272     * digest challenge format is detected.
 273     */
 274     private void processChallenge(byte[][] challengeVal, List<byte[]> realmChoices)
 275         throws SaslException, UnsupportedEncodingException {
 276 
 277         /* CHARSET: optional atmost once */
 278         if (challengeVal[CHARSET] != null) {
 279             if (!"utf-8".equals(new String(challengeVal[CHARSET], encoding))) {
 280                 throw new SaslException("DIGEST-MD5: digest-challenge format " +
 281                     "violation. Unrecognised charset value: " +
 282                     new String(challengeVal[CHARSET]));
 283             } else {
 284                 encoding = "UTF8";
 285                 useUTF8 = true;
 286             }
 287         }
 288 
 289         /* ALGORITHM: required exactly once */
 290         if (challengeVal[ALGORITHM] == null) {
 291             throw new SaslException("DIGEST-MD5: Digest-challenge format " +
 292                 "violation: algorithm directive missing");
 293         } else if (!"md5-sess".equals(new String(challengeVal[ALGORITHM], encoding))) {
 294             throw new SaslException("DIGEST-MD5: Digest-challenge format " +
 295                 "violation. Invalid value for 'algorithm' directive: " +
 296                 challengeVal[ALGORITHM]);
 297         }
 298 
 299         /* NONCE: required exactly once */
 300         if (challengeVal[NONCE] == null) {
 301             throw new SaslException("DIGEST-MD5: Digest-challenge format " +
 302                 "violation: nonce directive missing");
 303         } else {
 304             nonce = challengeVal[NONCE];
 305         }
 306 
 307         try {
 308             /* REALM: optional, if multiple, stored in realmChoices */
 309             String[] realmTokens = null;
 310 
 311             if (challengeVal[REALM] != null) {
 312                 if (realmChoices == null || realmChoices.size() <= 1) {
 313                     // Only one realm specified
 314                     negotiatedRealm = new String(challengeVal[REALM], encoding);
 315                 } else {
 316                     realmTokens = new String[realmChoices.size()];
 317                     for (int i = 0; i < realmTokens.length; i++) {
 318                         realmTokens[i] =
 319                             new String(realmChoices.get(i), encoding);
 320                     }
 321                 }
 322             }
 323 
 324             NameCallback ncb = authzid == null ?
 325                 new NameCallback("DIGEST-MD5 authentication ID: ") :
 326                 new NameCallback("DIGEST-MD5 authentication ID: ", authzid);
 327             PasswordCallback pcb =
 328                 new PasswordCallback("DIGEST-MD5 password: ", false);
 329 
 330             if (realmTokens == null) {
 331                 // Server specified <= 1 realm
 332                 // If 0, RFC 2831: the client SHOULD solicit a realm from the user.
 333                 RealmCallback tcb =
 334                     (negotiatedRealm == null? new RealmCallback("DIGEST-MD5 realm: ") :
 335                         new RealmCallback("DIGEST-MD5 realm: ", negotiatedRealm));
 336 
 337                 cbh.handle(new Callback[] {tcb, ncb, pcb});
 338 
 339                 /* Acquire realm from RealmCallback */
 340                 negotiatedRealm = tcb.getText();
 341                 if (negotiatedRealm == null) {
 342                     negotiatedRealm = "";
 343                 }
 344             } else {
 345                 RealmChoiceCallback ccb = new RealmChoiceCallback(
 346                     "DIGEST-MD5 realm: ",
 347                     realmTokens,
 348                     0, false);
 349                 cbh.handle(new Callback[] {ccb, ncb, pcb});
 350 
 351                 // Acquire realm from RealmChoiceCallback
 352                 int[] selected = ccb.getSelectedIndexes();
 353                 if (selected == null
 354                         || selected[0] < 0
 355                         || selected[0] >= realmTokens.length) {
 356                     throw new SaslException("DIGEST-MD5: Invalid realm chosen");
 357                 }
 358                 negotiatedRealm = realmTokens[selected[0]];
 359             }
 360 
 361             passwd = pcb.getPassword();
 362             pcb.clearPassword();
 363             username = ncb.getName();
 364 
 365         } catch (SaslException se) {
 366             throw se;
 367 
 368         } catch (UnsupportedCallbackException e) {
 369             throw new SaslException("DIGEST-MD5: Cannot perform callback to " +
 370                 "acquire realm, authentication ID or password", e);
 371 
 372         } catch (IOException e) {
 373             throw new SaslException(
 374                 "DIGEST-MD5: Error acquiring realm, authentication ID or password", e);
 375         }
 376 
 377         if (username == null || passwd == null) {
 378             throw new SaslException(
 379                 "DIGEST-MD5: authentication ID and password must be specified");
 380         }
 381 
 382         /* MAXBUF: optional atmost once */
 383         int srvMaxBufSize =
 384             (challengeVal[MAXBUF] == null) ? DEFAULT_MAXBUF
 385             : Integer.parseInt(new String(challengeVal[MAXBUF], encoding));
 386         sendMaxBufSize =
 387             (sendMaxBufSize == 0) ? srvMaxBufSize
 388             : Math.min(sendMaxBufSize, srvMaxBufSize);
 389     }
 390 
 391     /**
 392      * Parses the 'qop' directive. If 'auth-conf' is specified by
 393      * the client and offered as a QOP option by the server, then a check
 394      * is client-side supported ciphers is performed.
 395      *
 396      * @throws IOException
 397      */
 398     private void checkQopSupport(byte[] qopInChallenge, byte[] ciphersInChallenge)
 399         throws IOException {
 400 
 401         /* QOP: optional; if multiple, merged earlier */
 402         String qopOptions;
 403 
 404         if (qopInChallenge == null) {
 405             qopOptions = "auth";
 406         } else {
 407             qopOptions = new String(qopInChallenge, encoding);
 408         }
 409 
 410         // process
 411         String[] serverQopTokens = new String[3];
 412         byte[] serverQop = parseQop(qopOptions, serverQopTokens,
 413             true /* ignore unrecognized tokens */);
 414         byte serverAllQop = combineMasks(serverQop);
 415 
 416         switch (findPreferredMask(serverAllQop, qop)) {
 417         case 0:
 418             throw new SaslException("DIGEST-MD5: No common protection " +
 419                 "layer between client and server");
 420 
 421         case NO_PROTECTION:
 422             negotiatedQop = "auth";
 423             // buffer sizes not applicable
 424             break;
 425 
 426         case INTEGRITY_ONLY_PROTECTION:
 427             negotiatedQop = "auth-int";
 428             integrity = true;
 429             rawSendSize = sendMaxBufSize - 16;
 430             break;
 431 
 432         case PRIVACY_PROTECTION:
 433             negotiatedQop = "auth-conf";
 434             privacy = integrity = true;
 435             rawSendSize = sendMaxBufSize - 26;
 436             checkStrengthSupport(ciphersInChallenge);
 437             break;
 438         }
 439 
 440         if (logger.isLoggable(Level.FINE)) {
 441             logger.log(Level.FINE, "DIGEST61:Raw send size: {0}",
 442                 rawSendSize);
 443         }
 444      }
 445 
 446     /**
 447      * Processes the 'cipher' digest-challenge directive. This allows the
 448      * mechanism to check for client-side support against the list of
 449      * supported ciphers send by the server. If no match is found,
 450      * the mechanism aborts.
 451      *
 452      * @throws SaslException If an error is encountered in processing
 453      * the cipher digest-challenge directive or if no client-side
 454      * support is found.
 455      */
 456     private void checkStrengthSupport(byte[] ciphersInChallenge)
 457         throws IOException {
 458 
 459         /* CIPHER: required exactly once if qop=auth-conf */
 460         if (ciphersInChallenge == null) {
 461             throw new SaslException("DIGEST-MD5: server did not specify " +
 462                 "cipher to use for 'auth-conf'");
 463         }
 464 
 465         // First determine ciphers that server supports
 466         String cipherOptions = new String(ciphersInChallenge, encoding);
 467         StringTokenizer parser = new StringTokenizer(cipherOptions, ", \t\n");
 468         int tokenCount = parser.countTokens();
 469         String token = null;
 470         byte[] serverCiphers = { UNSET,
 471                                  UNSET,
 472                                  UNSET,
 473                                  UNSET,
 474                                  UNSET };
 475         String[] serverCipherStrs = new String[serverCiphers.length];
 476 
 477         // Parse ciphers in challenge; mark each that server supports
 478         for (int i = 0; i < tokenCount; i++) {
 479             token = parser.nextToken();
 480             for (int j = 0; j < CIPHER_TOKENS.length; j++) {
 481                 if (token.equals(CIPHER_TOKENS[j])) {
 482                     serverCiphers[j] |= CIPHER_MASKS[j];
 483                     serverCipherStrs[j] = token; // keep for replay to server
 484                     logger.log(Level.FINE, "DIGEST62:Server supports {0}", token);
 485                 }
 486             }
 487         }
 488 
 489         // Determine which ciphers are available on client
 490         byte[] clntCiphers = getPlatformCiphers();
 491 
 492         // Take intersection of server and client supported ciphers
 493         byte inter = 0;
 494         for (int i = 0; i < serverCiphers.length; i++) {
 495             serverCiphers[i] &= clntCiphers[i];
 496             inter |= serverCiphers[i];
 497         }
 498 
 499         if (inter == UNSET) {
 500             throw new SaslException(
 501                 "DIGEST-MD5: Client supports none of these cipher suites: " +
 502                 cipherOptions);
 503         }
 504 
 505         // now have a clear picture of user / client; client / server
 506         // cipher options. Leverage strength array against what is
 507         // supported to choose a cipher.
 508         negotiatedCipher = findCipherAndStrength(serverCiphers, serverCipherStrs);
 509 
 510         if (negotiatedCipher == null) {
 511             throw new SaslException("DIGEST-MD5: Unable to negotiate " +
 512                 "a strength level for 'auth-conf'");
 513         }
 514         logger.log(Level.FINE, "DIGEST63:Cipher suite: {0}", negotiatedCipher);
 515     }
 516 
 517     /**
 518      * Steps through the ordered 'strength' array, and compares it with
 519      * the 'supportedCiphers' array. The cipher returned represents
 520      * the best possible cipher based on the strength preference and the
 521      * available ciphers on both the server and client environments.
 522      *
 523      * @param tokens The array of cipher tokens sent by server
 524      * @return The agreed cipher.
 525      */
 526     private String findCipherAndStrength(byte[] supportedCiphers,
 527         String[] tokens) {
 528         byte s;
 529         for (int i = 0; i < strength.length; i++) {
 530             if ((s=strength[i]) != 0) {
 531                 for (int j = 0; j < supportedCiphers.length; j++) {
 532 
 533                     // If user explicitly requested cipher, then it
 534                     // must be the one we choose
 535 
 536                     if (s == supportedCiphers[j] &&
 537                         (specifiedCipher == null ||
 538                             specifiedCipher.equals(tokens[j]))) {
 539                         switch (s) {
 540                         case HIGH_STRENGTH:
 541                             negotiatedStrength = "high";
 542                             break;
 543                         case MEDIUM_STRENGTH:
 544                             negotiatedStrength = "medium";
 545                             break;
 546                         case LOW_STRENGTH:
 547                             negotiatedStrength = "low";
 548                             break;
 549                         }
 550 
 551                         return tokens[j];
 552                     }
 553                 }
 554             }
 555         }
 556 
 557         return null;  // none found
 558     }
 559 
 560     /**
 561      * Returns digest-response suitable for an initial authentication.
 562      *
 563      * The following are qdstr-val (quoted string values) as per RFC 2831,
 564      * which means that any embedded quotes must be escaped.
 565      *    realm-value
 566      *    nonce-value
 567      *    username-value
 568      *    cnonce-value
 569      *    authzid-value
 570      * @returns <tt>digest-response</tt> in a byte array
 571      * @throws SaslException if there is an error generating the
 572      * response value or the cnonce value.
 573      */
 574     private byte[] generateClientResponse(byte[] charset) throws IOException {
 575 
 576         ByteArrayOutputStream digestResp = new ByteArrayOutputStream();
 577 
 578         if (useUTF8) {
 579             digestResp.write("charset=".getBytes(encoding));
 580             digestResp.write(charset);
 581             digestResp.write(',');
 582         }
 583 
 584         digestResp.write(("username=\"" +
 585             quotedStringValue(username) + "\",").getBytes(encoding));
 586 
 587         if (negotiatedRealm.length() > 0) {
 588             digestResp.write(("realm=\"" +
 589                 quotedStringValue(negotiatedRealm) + "\",").getBytes(encoding));
 590         }
 591 
 592         digestResp.write("nonce=\"".getBytes(encoding));
 593         writeQuotedStringValue(digestResp, nonce);
 594         digestResp.write('"');
 595         digestResp.write(',');
 596 
 597         nonceCount = getNonceCount(nonce);
 598         digestResp.write(("nc=" +
 599             nonceCountToHex(nonceCount) + ",").getBytes(encoding));
 600 
 601         cnonce = generateNonce();
 602         digestResp.write("cnonce=\"".getBytes(encoding));
 603         writeQuotedStringValue(digestResp, cnonce);
 604         digestResp.write("\",".getBytes(encoding));
 605         digestResp.write(("digest-uri=\"" + digestUri + "\",").getBytes(encoding));
 606 
 607         digestResp.write("maxbuf=".getBytes(encoding));
 608         digestResp.write(String.valueOf(recvMaxBufSize).getBytes(encoding));
 609         digestResp.write(',');
 610 
 611         try {
 612             digestResp.write("response=".getBytes(encoding));
 613             digestResp.write(generateResponseValue("AUTHENTICATE",
 614                 digestUri, negotiatedQop, username,
 615                 negotiatedRealm, passwd, nonce, cnonce,
 616                 nonceCount, authzidBytes));
 617             digestResp.write(',');
 618         } catch (Exception e) {
 619             throw new SaslException(
 620                 "DIGEST-MD5: Error generating response value", e);
 621         }
 622 
 623         digestResp.write(("qop=" + negotiatedQop).getBytes(encoding));
 624 
 625         if (negotiatedCipher != null) {
 626             digestResp.write((",cipher=\"" + negotiatedCipher + "\"").getBytes(encoding));
 627         }
 628 
 629         if (authzidBytes != null) {
 630             digestResp.write(",authzid=\"".getBytes(encoding));
 631             writeQuotedStringValue(digestResp, authzidBytes);
 632             digestResp.write("\"".getBytes(encoding));
 633         }
 634 
 635         if (digestResp.size() > MAX_RESPONSE_LENGTH) {
 636             throw new SaslException ("DIGEST-MD5: digest-response size too " +
 637                 "large. Length: "  + digestResp.size());
 638         }
 639         return digestResp.toByteArray();
 640      }
 641 
 642 
 643     /**
 644      * From RFC 2831, Section 2.1.3: Step Three
 645      * [Server] sends a message formatted as follows:
 646      *     response-auth = "rspauth" "=" response-value
 647      * where response-value is calculated as above, using the values sent in
 648      * step two, except that if qop is "auth", then A2 is
 649      *
 650      *  A2 = { ":", digest-uri-value }
 651      *
 652      * And if qop is "auth-int" or "auth-conf" then A2 is
 653      *
 654      *  A2 = { ":", digest-uri-value, ":00000000000000000000000000000000" }
 655      */
 656     private void validateResponseValue(byte[] fromServer) throws SaslException {
 657         if (fromServer == null) {
 658             throw new SaslException("DIGEST-MD5: Authenication failed. " +
 659                 "Expecting 'rspauth' authentication success message");
 660         }
 661 
 662         try {
 663             byte[] expected = generateResponseValue("",
 664                 digestUri, negotiatedQop, username, negotiatedRealm,
 665                 passwd, nonce, cnonce,  nonceCount, authzidBytes);
 666             if (!Arrays.equals(expected, fromServer)) {
 667                 /* Server's rspauth value does not match */
 668                 throw new SaslException(
 669                     "Server's rspauth value does not match what client expects");
 670             }
 671         } catch (NoSuchAlgorithmException e) {
 672             throw new SaslException(
 673                 "Problem generating response value for verification", e);
 674         } catch (IOException e) {
 675             throw new SaslException(
 676                 "Problem generating response value for verification", e);
 677         }
 678     }
 679 
 680     /**
 681      * Returns the number of requests (including current request)
 682      * that the client has sent in response to nonceValue.
 683      * This is 1 the first time nonceValue is seen.
 684      *
 685      * We don't cache nonce values seen, and we don't support subsequent
 686      * authentication, so the value is always 1.
 687      */
 688     private static int getNonceCount(byte[] nonceValue) {
 689         return 1;
 690     }
 691 
 692     private void clearPassword() {
 693         if (passwd != null) {
 694             for (int i = 0; i < passwd.length; i++) {
 695                 passwd[i] = 0;
 696             }
 697             passwd = null;
 698         }
 699     }
 700 }