705 lines · 27.0 KB · Java
RawDownload
/* * Copyright (c) 2003, 2006, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation.  Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.security.sasl.digest; import java.security.AccessController;import java.security.Provider;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.io.ByteArrayOutputStream;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.UnsupportedEncodingException;import java.util.Random;import java.util.StringTokenizer;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.Set;import java.util.Arrays; import java.util.logging.Logger;import java.util.logging.Level; import javax.security.sasl.*;import javax.security.auth.callback.*; /**  * An implementation of the DIGEST-MD5 server SASL mechanism.  * (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>)  * <p>  * The DIGEST-MD5 SASL mechanism specifies two modes of authentication.  * <ul><li>Initial Authentication  * <li>Subsequent Authentication - optional, (currently not supported)  * </ul>  *  * Required callbacks:  * - RealmCallback  *      used as key by handler to fetch password  * - NameCallback  *      used as key by handler to fetch password  * - PasswordCallback  *      handler must enter password for username/realm supplied  * - AuthorizeCallback  *      handler must verify that authid/authzids are allowed and set  *      authorized ID to be the canonicalized authzid (if applicable).  *  * Environment properties that affect the implementation:  * javax.security.sasl.qop:  *    specifies list of qops; default is "auth"; typically, caller should set  *    this to "auth, auth-int, auth-conf".  * javax.security.sasl.strength  *    specifies low/medium/high strength of encryption; default is all available  *    ciphers [high,medium,low]; high means des3 or rc4 (128); medium des or  *    rc4-56; low is rc4-40.  * javax.security.sasl.maxbuf  *    specifies max receive buf size; default is 65536  * javax.security.sasl.sendmaxbuffer  *    specifies max send buf size; default is 65536 (min of this and client's max  *    recv size)  *  * com.sun.security.sasl.digest.utf8:  *    "true" means to use UTF-8 charset; "false" to use ISO-8859-1 encoding;  *    default is "true".  * com.sun.security.sasl.digest.realm:  *    space-separated list of realms; default is server name (fqdn parameter)  *  * @author Rosanna Lee  */ final class DigestMD5Server extends DigestMD5Base implements SaslServer {    private static final String MY_CLASS_NAME = DigestMD5Server.class.getName();     private static final String UTF8_DIRECTIVE = "charset=utf-8,";    private static final String ALGORITHM_DIRECTIVE = "algorithm=md5-sess";     /*     * Always expect nonce count value to be 1 because we support only     * initial authentication.     */    private static final int NONCE_COUNT_VALUE = 1;     /* "true" means use UTF8; "false" ISO 8859-1; default is "true" */    private static final String UTF8_PROPERTY =        "com.sun.security.sasl.digest.utf8";     /* List of space-separated realms used for authentication */    private static final String REALM_PROPERTY =        "com.sun.security.sasl.digest.realm";     /* Directives encountered in responses sent by the client. */    private static final String[] DIRECTIVE_KEY = {        "username",    // exactly once        "realm",       // exactly once if sent by server        "nonce",       // exactly once        "cnonce",      // exactly once        "nonce-count", // atmost once; default is 00000001        "qop",         // atmost once; default is "auth"        "digest-uri",  // atmost once; (default?)        "response",    // exactly once        "maxbuf",      // atmost once; default is 65536        "charset",     // atmost once; default is ISO-8859-1        "cipher",      // exactly once if qop is "auth-conf"        "authzid",     // atmost once; default is none        "auth-param",  // >= 0 times (ignored)    };     /* Indices into DIRECTIVE_KEY */    private static final int USERNAME = 0;    private static final int REALM = 1;    private static final int NONCE = 2;    private static final int CNONCE = 3;    private static final int NONCE_COUNT = 4;    private static final int QOP = 5;    private static final int DIGEST_URI = 6;    private static final int RESPONSE = 7;    private static final int MAXBUF = 8;    private static final int CHARSET = 9;    private static final int CIPHER = 10;    private static final int AUTHZID = 11;    private static final int AUTH_PARAM = 12;     /* Server-generated/supplied information */    private String specifiedQops;    private byte[] myCiphers;    private List<String> serverRealms;     DigestMD5Server(String protocol, String serverName, Map props,        CallbackHandler cbh) throws SaslException {        super(props, MY_CLASS_NAME, 1, protocol + "/" + serverName, cbh);         serverRealms = new ArrayList<String>();         useUTF8 = true;  // default         if (props != null) {            specifiedQops = (String) props.get(Sasl.QOP);            if ("false".equals((String) props.get(UTF8_PROPERTY))) {                useUTF8 = false;                logger.log(Level.FINE, "DIGEST80:Server supports ISO-Latin-1");            }             String realms = (String) props.get(REALM_PROPERTY);            if (realms != null) {                StringTokenizer parser = new StringTokenizer(realms, ", \t\n");                int tokenCount = parser.countTokens();                String token = null;                for (int i = 0; i < tokenCount; i++) {                    token = parser.nextToken();                    logger.log(Level.FINE, "DIGEST81:Server supports realm {0}",                        token);                    serverRealms.add(token);                }            }        }         encoding = (useUTF8 ? "UTF8" : "8859_1");         // By default, use server name as realm        if (serverRealms.size() == 0) {            serverRealms.add(serverName);        }    }     public  byte[] evaluateResponse(byte[] response) throws SaslException {        if (response.length > MAX_RESPONSE_LENGTH) {            throw new SaslException(                "DIGEST-MD5: Invalid digest response length. Got:  " +                response.length + " Expected < " + MAX_RESPONSE_LENGTH);        }         byte[] challenge;        switch (step) {        case 1:            if (response.length != 0) {                throw new SaslException(                    "DIGEST-MD5 must not have an initial response");            }             /* Generate first challenge */            String supportedCiphers = null;            if ((allQop&PRIVACY_PROTECTION) != 0) {                myCiphers = getPlatformCiphers();                StringBuffer buf = new StringBuffer();                 // myCipher[i] is a byte that indicates whether CIPHER_TOKENS[i]                // is supported                for (int i = 0; i < CIPHER_TOKENS.length; i++) {                    if (myCiphers[i] != 0) {                        if (buf.length() > 0) {                            buf.append(',');                        }                        buf.append(CIPHER_TOKENS[i]);                    }                }                supportedCiphers = buf.toString();            }             try {                challenge = generateChallenge(serverRealms, specifiedQops,                    supportedCiphers);                 step = 3;                return challenge;            } catch (UnsupportedEncodingException e) {                throw new SaslException(                    "DIGEST-MD5: Error encoding challenge", e);            } catch (IOException e) {                throw new SaslException(                    "DIGEST-MD5: Error generating challenge", e);            }             // Step 2 is performed by client         case 3:            /* Validates client's response and generate challenge:             *    response-auth = "rspauth" "=" response-value             */            try {                byte[][] responseVal = parseDirectives(response, DIRECTIVE_KEY,                    null, REALM);                challenge = validateClientResponse(responseVal);            } catch (SaslException e) {                throw e;            } catch (UnsupportedEncodingException e) {                throw new SaslException(                    "DIGEST-MD5: Error validating client response", e);            } finally {                step = 0;  // Set to invalid state            }             completed = true;             /* Initialize SecurityCtx implementation */            if (integrity && privacy) {                secCtx = new DigestPrivacy(false /* not client */);            } else if (integrity) {                secCtx = new DigestIntegrity(false /* not client */);            }             return challenge;         default:            // No other possible state            throw new SaslException("DIGEST-MD5: Server at illegal state");        }    }     /**     * Generates challenge to be sent to client.     *  digest-challenge  =     *    1#( realm | nonce | qop-options | stale | maxbuf | charset     *               algorithm | cipher-opts | auth-param )     *     *        realm             = "realm" "=" <"> realm-value <">     *        realm-value       = qdstr-val     *        nonce             = "nonce" "=" <"> nonce-value <">     *        nonce-value       = qdstr-val     *        qop-options       = "qop" "=" <"> qop-list <">     *        qop-list          = 1#qop-value     *        qop-value         = "auth" | "auth-int" | "auth-conf" |     *                             token     *        stale             = "stale" "=" "true"     *        maxbuf            = "maxbuf" "=" maxbuf-value     *        maxbuf-value      = 1*DIGIT     *        charset           = "charset" "=" "utf-8"     *        algorithm         = "algorithm" "=" "md5-sess"     *        cipher-opts       = "cipher" "=" <"> 1#cipher-value <">     *        cipher-value      = "3des" | "des" | "rc4-40" | "rc4" |     *                            "rc4-56" | token     *        auth-param        = token "=" ( token | quoted-string )     */    private byte[] generateChallenge(List<String> realms, String qopStr,        String cipherStr) throws UnsupportedEncodingException, IOException {        ByteArrayOutputStream out = new ByteArrayOutputStream();         // Realms (>= 0)        for (int i = 0; realms != null && i < realms.size(); i++) {            out.write("realm=\"".getBytes(encoding));            writeQuotedStringValue(out, realms.get(i).getBytes(encoding));            out.write('"');            out.write(',');        }         // Nonce - required (1)        out.write(("nonce=\"").getBytes(encoding));        nonce = generateNonce();        writeQuotedStringValue(out, nonce);        out.write('"');        out.write(',');         // QOP - optional (1) [default: auth]        // qop="auth,auth-conf,auth-int"        if (qopStr != null) {            out.write(("qop=\"").getBytes(encoding));            // Check for quotes in case of non-standard qop options            writeQuotedStringValue(out, qopStr.getBytes(encoding));            out.write('"');            out.write(',');        }         // maxbuf - optional (1) [default: 65536]        if (recvMaxBufSize != DEFAULT_MAXBUF) {            out.write(("maxbuf=\"" + recvMaxBufSize + "\",").getBytes(encoding));        }         // charset - optional (1) [default: ISO 8859_1]        if (useUTF8) {            out.write(UTF8_DIRECTIVE.getBytes(encoding));        }         if (cipherStr != null) {            out.write("cipher=\"".getBytes(encoding));            // Check for quotes in case of custom ciphers            writeQuotedStringValue(out, cipherStr.getBytes(encoding));            out.write('"');            out.write(',');        }         // algorithm - required (1)        out.write(ALGORITHM_DIRECTIVE.getBytes(encoding));         return out.toByteArray();    }     /**     * Validates client's response.     *   digest-response  = 1#( username | realm | nonce | cnonce |     *                          nonce-count | qop | digest-uri | response |     *                          maxbuf | charset | cipher | authzid |     *                          auth-param )     *     *       username         = "username" "=" <"> username-value <">     *       username-value   = qdstr-val     *       cnonce           = "cnonce" "=" <"> cnonce-value <">     *       cnonce-value     = qdstr-val     *       nonce-count      = "nc" "=" nc-value     *       nc-value         = 8LHEX     *       qop              = "qop" "=" qop-value     *       digest-uri       = "digest-uri" "=" <"> digest-uri-value <">     *       digest-uri-value  = serv-type "/" host [ "/" serv-name ]     *       serv-type        = 1*ALPHA     *       host             = 1*( ALPHA | DIGIT | "-" | "." )     *       serv-name        = host     *       response         = "response" "=" response-value     *       response-value   = 32LHEX     *       LHEX             = "0" | "1" | "2" | "3" |     *                          "4" | "5" | "6" | "7" |     *                          "8" | "9" | "a" | "b" |     *                          "c" | "d" | "e" | "f"     *       cipher           = "cipher" "=" cipher-value     *       authzid          = "authzid" "=" <"> authzid-value <">     *       authzid-value    = qdstr-val     * sets:     *   negotiatedQop     *   negotiatedCipher     *   negotiatedRealm     *   negotiatedStrength     *   digestUri (checked and set to clients to account for case diffs)     *   sendMaxBufSize     *   authzid (gotten from callback)     * @return response-value ('rspauth') for client to validate     */    private byte[] validateClientResponse(byte[][] responseVal)        throws SaslException, UnsupportedEncodingException {         /* CHARSET: optional atmost once */        if (responseVal[CHARSET] != null) {            // The client should send this directive only if the server has            // indicated it supports UTF-8.            if (!useUTF8 ||                !"utf-8".equals(new String(responseVal[CHARSET], encoding))) {                throw new SaslException("DIGEST-MD5: digest response format " +                    "violation. Incompatible charset value: " +                    new String(responseVal[CHARSET]));            }        }         // maxbuf: atmost once        int clntMaxBufSize =            (responseVal[MAXBUF] == null) ? DEFAULT_MAXBUF            : Integer.parseInt(new String(responseVal[MAXBUF], encoding));         // Max send buf size is min of client's max recv buf size and        // server's max send buf size        sendMaxBufSize = ((sendMaxBufSize == 0) ? clntMaxBufSize :            Math.min(sendMaxBufSize, clntMaxBufSize));         /* username: exactly once */        String username;        if (responseVal[USERNAME] != null) {            username = new String(responseVal[USERNAME], encoding);            logger.log(Level.FINE, "DIGEST82:Username: {0}", username);        } else {            throw new SaslException("DIGEST-MD5: digest response format " +                "violation. Missing username.");        }         /* realm: exactly once if sent by server */        negotiatedRealm = ((responseVal[REALM] != null) ?            new String(responseVal[REALM], encoding) : "");        logger.log(Level.FINE, "DIGEST83:Client negotiated realm: {0}",            negotiatedRealm);         if (!serverRealms.contains(negotiatedRealm)) {            // Server had sent at least one realm            // Check that response is one of these            throw new SaslException("DIGEST-MD5: digest response format " +                "violation. Nonexistent realm: " + negotiatedRealm);        }        // Else, client specified realm was one of server's or server had none         /* nonce: exactly once */        if (responseVal[NONCE] == null) {            throw new SaslException("DIGEST-MD5: digest response format " +                "violation. Missing nonce.");        }        byte[] nonceFromClient = responseVal[NONCE];        if (!Arrays.equals(nonceFromClient, nonce)) {            throw new SaslException("DIGEST-MD5: digest response format " +                "violation. Mismatched nonce.");        }         /* cnonce: exactly once */        if (responseVal[CNONCE] == null) {            throw new SaslException("DIGEST-MD5: digest response format " +                "violation. Missing cnonce.");        }        byte[] cnonce = responseVal[CNONCE];         /* nonce-count: atmost once */        if (responseVal[NONCE_COUNT] != null &&            NONCE_COUNT_VALUE != Integer.parseInt(                new String(responseVal[NONCE_COUNT], encoding), 16)) {            throw new SaslException("DIGEST-MD5: digest response format " +                "violation. Nonce count does not match: " +                new String(responseVal[NONCE_COUNT]));        }         /* qop: atmost once; default is "auth" */        negotiatedQop = ((responseVal[QOP] != null) ?            new String(responseVal[QOP], encoding) : "auth");         logger.log(Level.FINE, "DIGEST84:Client negotiated qop: {0}",            negotiatedQop);         // Check that QOP is one sent by server        byte cQop;        if (negotiatedQop.equals("auth")) {            cQop = NO_PROTECTION;        } else if (negotiatedQop.equals("auth-int")) {            cQop = INTEGRITY_ONLY_PROTECTION;            integrity = true;            rawSendSize = sendMaxBufSize - 16;        } else if (negotiatedQop.equals("auth-conf")) {            cQop = PRIVACY_PROTECTION;            integrity = privacy = true;            rawSendSize = sendMaxBufSize - 26;        } else {            throw new SaslException("DIGEST-MD5: digest response format " +                "violation. Invalid QOP: " + negotiatedQop);        }        if ((cQop&allQop) == 0) {            throw new SaslException("DIGEST-MD5: server does not support " +                " qop: " + negotiatedQop);        }         if (privacy) {            negotiatedCipher = ((responseVal[CIPHER] != null) ?                new String(responseVal[CIPHER], encoding) : null);            if (negotiatedCipher == null) {                throw new SaslException("DIGEST-MD5: digest response format " +                    "violation. No cipher specified.");            }             int foundCipher = -1;            logger.log(Level.FINE, "DIGEST85:Client negotiated cipher: {0}",                negotiatedCipher);

Lines 1–500 of 705.