1/*
2 * Copyright (c) 2003, 2012, 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
26package com.sun.security.sasl.digest;
27
28import java.security.NoSuchAlgorithmException;
29import java.io.ByteArrayOutputStream;
30import java.io.IOException;
31import java.io.UnsupportedEncodingException;
32import java.util.StringTokenizer;
33import java.util.ArrayList;
34import java.util.List;
35import java.util.Map;
36import java.util.Arrays;
37
38import java.util.logging.Level;
39
40import javax.security.sasl.*;
41import javax.security.auth.callback.*;
42
43/**
44  * An implementation of the DIGEST-MD5 server SASL mechanism.
45  * (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>)
46  * <p>
47  * The DIGEST-MD5 SASL mechanism specifies two modes of authentication.
48  * <ul><li>Initial Authentication
49  * <li>Subsequent Authentication - optional, (currently not supported)
50  * </ul>
51  *
52  * Required callbacks:
53  * - RealmCallback
54  *      used as key by handler to fetch password
55  * - NameCallback
56  *      used as key by handler to fetch password
57  * - PasswordCallback
58  *      handler must enter password for username/realm supplied
59  * - AuthorizeCallback
60  *      handler must verify that authid/authzids are allowed and set
61  *      authorized ID to be the canonicalized authzid (if applicable).
62  *
63  * Environment properties that affect the implementation:
64  * javax.security.sasl.qop:
65  *    specifies list of qops; default is "auth"; typically, caller should set
66  *    this to "auth, auth-int, auth-conf".
67  * javax.security.sasl.strength
68  *    specifies low/medium/high strength of encryption; default is all available
69  *    ciphers [high,medium,low]; high means des3 or rc4 (128); medium des or
70  *    rc4-56; low is rc4-40.
71  * javax.security.sasl.maxbuf
72  *    specifies max receive buf size; default is 65536
73  * javax.security.sasl.sendmaxbuffer
74  *    specifies max send buf size; default is 65536 (min of this and client's max
75  *    recv size)
76  *
77  * com.sun.security.sasl.digest.utf8:
78  *    "true" means to use UTF-8 charset; "false" to use ISO-8859-1 encoding;
79  *    default is "true".
80  * com.sun.security.sasl.digest.realm:
81  *    space-separated list of realms; default is server name (fqdn parameter)
82  *
83  * @author Rosanna Lee
84  */
85
86final class DigestMD5Server extends DigestMD5Base implements SaslServer {
87    private static final String MY_CLASS_NAME = DigestMD5Server.class.getName();
88
89    private static final String UTF8_DIRECTIVE = "charset=utf-8,";
90    private static final String ALGORITHM_DIRECTIVE = "algorithm=md5-sess";
91
92    /*
93     * Always expect nonce count value to be 1 because we support only
94     * initial authentication.
95     */
96    private static final int NONCE_COUNT_VALUE = 1;
97
98    /* "true" means use UTF8; "false" ISO 8859-1; default is "true" */
99    private static final String UTF8_PROPERTY =
100        "com.sun.security.sasl.digest.utf8";
101
102    /* List of space-separated realms used for authentication */
103    private static final String REALM_PROPERTY =
104        "com.sun.security.sasl.digest.realm";
105
106    /* Directives encountered in responses sent by the client. */
107    private static final String[] DIRECTIVE_KEY = {
108        "username",    // exactly once
109        "realm",       // exactly once if sent by server
110        "nonce",       // exactly once
111        "cnonce",      // exactly once
112        "nonce-count", // atmost once; default is 00000001
113        "qop",         // atmost once; default is "auth"
114        "digest-uri",  // atmost once; (default?)
115        "response",    // exactly once
116        "maxbuf",      // atmost once; default is 65536
117        "charset",     // atmost once; default is ISO-8859-1
118        "cipher",      // exactly once if qop is "auth-conf"
119        "authzid",     // atmost once; default is none
120        "auth-param",  // >= 0 times (ignored)
121    };
122
123    /* Indices into DIRECTIVE_KEY */
124    private static final int USERNAME = 0;
125    private static final int REALM = 1;
126    private static final int NONCE = 2;
127    private static final int CNONCE = 3;
128    private static final int NONCE_COUNT = 4;
129    private static final int QOP = 5;
130    private static final int DIGEST_URI = 6;
131    private static final int RESPONSE = 7;
132    private static final int MAXBUF = 8;
133    private static final int CHARSET = 9;
134    private static final int CIPHER = 10;
135    private static final int AUTHZID = 11;
136    private static final int AUTH_PARAM = 12;
137
138    /* Server-generated/supplied information */
139    private String specifiedQops;
140    private byte[] myCiphers;
141    private List<String> serverRealms;
142
143    DigestMD5Server(String protocol, String serverName, Map<String, ?> props,
144            CallbackHandler cbh) throws SaslException {
145        super(props, MY_CLASS_NAME, 1,
146                protocol + "/" + (serverName==null?"*":serverName),
147                cbh);
148
149        serverRealms = new ArrayList<String>();
150
151        useUTF8 = true;  // default
152
153        if (props != null) {
154            specifiedQops = (String) props.get(Sasl.QOP);
155            if ("false".equals((String) props.get(UTF8_PROPERTY))) {
156                useUTF8 = false;
157                logger.log(Level.FINE, "DIGEST80:Server supports ISO-Latin-1");
158            }
159
160            String realms = (String) props.get(REALM_PROPERTY);
161            if (realms != null) {
162                StringTokenizer parser = new StringTokenizer(realms, ", \t\n");
163                int tokenCount = parser.countTokens();
164                String token = null;
165                for (int i = 0; i < tokenCount; i++) {
166                    token = parser.nextToken();
167                    logger.log(Level.FINE, "DIGEST81:Server supports realm {0}",
168                        token);
169                    serverRealms.add(token);
170                }
171            }
172        }
173
174        encoding = (useUTF8 ? "UTF8" : "8859_1");
175
176        // By default, use server name as realm
177        if (serverRealms.isEmpty()) {
178            if (serverName == null) {
179                throw new SaslException(
180                        "A realm must be provided in props or serverName");
181            } else {
182                serverRealms.add(serverName);
183            }
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                StringBuilder sb = new StringBuilder();
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 (sb.length() > 0) {
213                            sb.append(',');
214                        }
215                        sb.append(CIPHER_TOKENS[i]);
216                    }
217                }
218                supportedCiphers = sb.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        switch (negotiatedQop) {
472            case "auth":
473                cQop = NO_PROTECTION;
474                break;
475            case "auth-int":
476                cQop = INTEGRITY_ONLY_PROTECTION;
477                integrity = true;
478                rawSendSize = sendMaxBufSize - 16;
479                break;
480            case "auth-conf":
481                cQop = PRIVACY_PROTECTION;
482                integrity = privacy = true;
483                rawSendSize = sendMaxBufSize - 26;
484                break;
485            default:
486                throw new SaslException("DIGEST-MD5: digest response format " +
487                    "violation. Invalid QOP: " + negotiatedQop);
488        }
489        if ((cQop&allQop) == 0) {
490            throw new SaslException("DIGEST-MD5: server does not support " +
491                " qop: " + negotiatedQop);
492        }
493
494        if (privacy) {
495            negotiatedCipher = ((responseVal[CIPHER] != null) ?
496                new String(responseVal[CIPHER], encoding) : null);
497            if (negotiatedCipher == null) {
498                throw new SaslException("DIGEST-MD5: digest response format " +
499                    "violation. No cipher specified.");
500            }
501
502            int foundCipher = -1;
503            logger.log(Level.FINE, "DIGEST85:Client negotiated cipher: {0}",
504                negotiatedCipher);
505
506            // Check that cipher is one that we offered
507            for (int j = 0; j < CIPHER_TOKENS.length; j++) {
508                if (negotiatedCipher.equals(CIPHER_TOKENS[j]) &&
509                    myCiphers[j] != 0) {
510                    foundCipher = j;
511                    break;
512                }
513            }
514            if (foundCipher == -1) {
515                throw new SaslException("DIGEST-MD5: server does not " +
516                    "support cipher: " + negotiatedCipher);
517            }
518            // Set negotiatedStrength
519            if ((CIPHER_MASKS[foundCipher]&HIGH_STRENGTH) != 0) {
520                negotiatedStrength = "high";
521            } else if ((CIPHER_MASKS[foundCipher]&MEDIUM_STRENGTH) != 0) {
522                negotiatedStrength = "medium";
523            } else {
524                // assume default low
525                negotiatedStrength = "low";
526            }
527
528            logger.log(Level.FINE, "DIGEST86:Negotiated strength: {0}",
529                negotiatedStrength);
530        }
531
532        // atmost once
533        String digestUriFromResponse = ((responseVal[DIGEST_URI]) != null ?
534            new String(responseVal[DIGEST_URI], encoding) : null);
535
536        if (digestUriFromResponse != null) {
537            logger.log(Level.FINE, "DIGEST87:digest URI: {0}",
538                digestUriFromResponse);
539        }
540
541        // serv-type "/" host [ "/" serv-name ]
542        // e.g.: smtp/mail3.example.com/example.com
543        // e.g.: ftp/ftp.example.com
544        // e.g.: ldap/ldapserver.example.com
545
546        // host should match one of service's configured service names
547        // Check against digest URI that mech was created with
548
549        if (uriMatches(digestUri, digestUriFromResponse)) {
550            digestUri = digestUriFromResponse; // account for case-sensitive diffs
551        } else {
552            throw new SaslException("DIGEST-MD5: digest response format " +
553                "violation. Mismatched URI: " + digestUriFromResponse +
554                "; expecting: " + digestUri);
555        }
556
557        // response: exactly once
558        byte[] responseFromClient = responseVal[RESPONSE];
559        if (responseFromClient == null) {
560            throw new SaslException("DIGEST-MD5: digest response format " +
561                " violation. Missing response.");
562        }
563
564        // authzid: atmost once
565        byte[] authzidBytes;
566        String authzidFromClient = ((authzidBytes=responseVal[AUTHZID]) != null?
567            new String(authzidBytes, encoding) : username);
568
569        if (authzidBytes != null) {
570            logger.log(Level.FINE, "DIGEST88:Authzid: {0}",
571                new String(authzidBytes));
572        }
573
574        // Ignore auth-param
575
576        // Get password need to generate verifying response
577        char[] passwd;
578        try {
579            // Realm and Name callbacks are used to provide info
580            RealmCallback rcb = new RealmCallback("DIGEST-MD5 realm: ",
581                negotiatedRealm);
582            NameCallback ncb = new NameCallback("DIGEST-MD5 authentication ID: ",
583                username);
584
585            // PasswordCallback is used to collect info
586            PasswordCallback pcb =
587                new PasswordCallback("DIGEST-MD5 password: ", false);
588
589            cbh.handle(new Callback[] {rcb, ncb, pcb});
590            passwd = pcb.getPassword();
591            pcb.clearPassword();
592
593        } catch (UnsupportedCallbackException e) {
594            throw new SaslException(
595                "DIGEST-MD5: Cannot perform callback to acquire password", e);
596
597        } catch (IOException e) {
598            throw new SaslException(
599                "DIGEST-MD5: IO error acquiring password", e);
600        }
601
602        if (passwd == null) {
603            throw new SaslException(
604                "DIGEST-MD5: cannot acquire password for " + username +
605                " in realm : " + negotiatedRealm);
606        }
607
608        try {
609            // Validate response value sent by client
610            byte[] expectedResponse;
611
612            try {
613                expectedResponse = generateResponseValue("AUTHENTICATE",
614                    digestUri, negotiatedQop, username, negotiatedRealm,
615                    passwd, nonce /* use own nonce */,
616                    cnonce, NONCE_COUNT_VALUE, authzidBytes);
617
618            } catch (NoSuchAlgorithmException e) {
619                throw new SaslException(
620                    "DIGEST-MD5: problem duplicating client response", e);
621            } catch (IOException e) {
622                throw new SaslException(
623                    "DIGEST-MD5: problem duplicating client response", e);
624            }
625
626            if (!Arrays.equals(responseFromClient, expectedResponse)) {
627                throw new SaslException("DIGEST-MD5: digest response format " +
628                    "violation. Mismatched response.");
629            }
630
631            // Ensure that authzid mapping is OK
632            try {
633                AuthorizeCallback acb =
634                    new AuthorizeCallback(username, authzidFromClient);
635                cbh.handle(new Callback[]{acb});
636
637                if (acb.isAuthorized()) {
638                    authzid = acb.getAuthorizedID();
639                } else {
640                    throw new SaslException("DIGEST-MD5: " + username +
641                        " is not authorized to act as " + authzidFromClient);
642                }
643            } catch (SaslException e) {
644                throw e;
645            } catch (UnsupportedCallbackException e) {
646                throw new SaslException(
647                    "DIGEST-MD5: Cannot perform callback to check authzid", e);
648            } catch (IOException e) {
649                throw new SaslException(
650                    "DIGEST-MD5: IO error checking authzid", e);
651            }
652
653            return generateResponseAuth(username, passwd, cnonce,
654                NONCE_COUNT_VALUE, authzidBytes);
655        } finally {
656            // Clear password
657            for (int i = 0; i < passwd.length; i++) {
658                passwd[i] = 0;
659            }
660        }
661    }
662
663    private static boolean uriMatches(String thisUri, String incomingUri) {
664        // Full match
665        if (thisUri.equalsIgnoreCase(incomingUri)) {
666            return true;
667        }
668        // Unbound match
669        if (thisUri.endsWith("/*")) {
670            int protoAndSlash = thisUri.length() - 1;
671            String thisProtoAndSlash = thisUri.substring(0, protoAndSlash);
672            String incomingProtoAndSlash = incomingUri.substring(0, protoAndSlash);
673            return thisProtoAndSlash.equalsIgnoreCase(incomingProtoAndSlash);
674        }
675        return false;
676    }
677
678    /**
679     * Server sends a message formatted as follows:
680     *    response-auth = "rspauth" "=" response-value
681     *   where response-value is calculated as above, using the values sent in
682     *   step two, except that if qop is "auth", then A2 is
683     *
684     *       A2 = { ":", digest-uri-value }
685     *
686     *   And if qop is "auth-int" or "auth-conf" then A2 is
687     *
688     *       A2 = { ":", digest-uri-value, ":00000000000000000000000000000000" }
689     *
690     * Clears password afterwards.
691     */
692    private byte[] generateResponseAuth(String username, char[] passwd,
693        byte[] cnonce, int nonceCount, byte[] authzidBytes) throws SaslException {
694
695        // Construct response value
696
697        try {
698            byte[] responseValue = generateResponseValue("",
699                digestUri, negotiatedQop, username, negotiatedRealm,
700                passwd, nonce, cnonce, nonceCount, authzidBytes);
701
702            byte[] challenge = new byte[responseValue.length + 8];
703            System.arraycopy("rspauth=".getBytes(encoding), 0, challenge, 0, 8);
704            System.arraycopy(responseValue, 0, challenge, 8,
705                responseValue.length );
706
707            return challenge;
708
709        } catch (NoSuchAlgorithmException e) {
710            throw new SaslException("DIGEST-MD5: problem generating response", e);
711        } catch (IOException e) {
712            throw new SaslException("DIGEST-MD5: problem generating response", e);
713        }
714    }
715
716    public String getAuthorizationID() {
717        if (completed) {
718            return authzid;
719        } else {
720            throw new IllegalStateException(
721                "DIGEST-MD5 server negotiation not complete");
722        }
723    }
724}
725