blob: 78ff24fedac6e3a0a67b79f559d7e4db6fe4e53b [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
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
26package com.sun.security.sasl.digest;
27
28import java.security.AccessController;
29import java.security.Provider;
30import java.security.MessageDigest;
31import java.security.NoSuchAlgorithmException;
32import java.io.ByteArrayOutputStream;
33import java.io.ByteArrayInputStream;
34import java.io.IOException;
35import java.io.UnsupportedEncodingException;
36import java.util.Random;
37import java.util.StringTokenizer;
38import java.util.ArrayList;
39import java.util.List;
40import java.util.Map;
41import java.util.Set;
42import java.util.Arrays;
43
44import java.util.logging.Logger;
45import java.util.logging.Level;
46
47import javax.security.sasl.*;
48import 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
93final 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}