696 lines · 26.3 KB · Java
RawDownload
/* * Copyright (c) 2000, 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.MessageDigest;import java.security.NoSuchAlgorithmException;import java.io.ByteArrayOutputStream;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.UnsupportedEncodingException;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.CallbackHandler;import javax.security.auth.callback.PasswordCallback;import javax.security.auth.callback.NameCallback;import javax.security.auth.callback.Callback;import javax.security.auth.callback.UnsupportedCallbackException; /**  * An implementation of the DIGEST-MD5  * (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>) SASL  * (<a href="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222</a>) mechanism.  *  * The DIGEST-MD5 SASL mechanism specifies two modes of authentication.  * - Initial Authentication  * - Subsequent Authentication - optional, (currently unsupported)  *  * Required callbacks:  * - RealmChoiceCallback  *    shows user list of realms server has offered; handler must choose one  *    from list  * - RealmCallback  *    shows user the only realm server has offered or none; handler must  *    enter realm to use  * - NameCallback  *    handler must enter username to use for authentication  * - PasswordCallback  *    handler must enter password for username to use for authentication  *  * Environment properties that affect behavior of implementation:  *  * javax.security.sasl.qop  *    quality of protection; list of auth, auth-int, auth-conf; default is "auth"  * javax.security.sasl.strength  *    auth-conf strength; list of high, medium, low; default is highest  *    available on platform ["high,medium,low"].  *    high means des3 or rc4 (128); medium des or rc4-56; low is rc4-40;  *    choice of cipher depends on its availablility on platform  * javax.security.sasl.maxbuf  *    max receive buffer size; default is 65536  * javax.security.sasl.sendmaxbuffer  *    max send buffer size; default is 65536; (min with server max recv size)  *  * com.sun.security.sasl.digest.cipher  *    name a specific cipher to use; setting must be compatible with the  *    setting of the javax.security.sasl.strength property.  *  * @see <a href="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222</a>  * - Simple Authentication and Security Layer (SASL)  * @see <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>  * - Using Digest Authentication as a SASL Mechanism  * @see <a href="http://java.sun.com/products/jce">Java(TM)  * Cryptography Extension 1.2.1 (JCE)</a>  * @see <a href="http://java.sun.com/products/jaas">Java(TM)  * Authentication and Authorization Service (JAAS)</a>  *  * @author Jonathan Bruce  * @author Rosanna Lee  */final class DigestMD5Client extends DigestMD5Base implements SaslClient {    private static final String MY_CLASS_NAME = DigestMD5Client.class.getName();     // Property for specifying cipher explicitly    private static final String CIPHER_PROPERTY =        "com.sun.security.sasl.digest.cipher";     /* Directives encountered in challenges sent by the server. */    private static final String[] DIRECTIVE_KEY = {        "realm",      // >= 0 times        "qop",        // atmost once; default is "auth"        "algorithm",  // exactly once        "nonce",      // exactly once        "maxbuf",     // atmost once; default is 65536        "charset",    // atmost once; default is ISO 8859-1        "cipher",     // exactly once if qop is "auth-conf"        "rspauth",    // exactly once in 2nd challenge        "stale",      // atmost once for in subsequent auth (not supported)    };     /* Indices into DIRECTIVE_KEY */    private static final int REALM = 0;    private static final int QOP = 1;    private static final int ALGORITHM = 2;    private static final int NONCE = 3;    private static final int MAXBUF = 4;    private static final int CHARSET = 5;    private static final int CIPHER = 6;    private static final int RESPONSE_AUTH = 7;    private static final int STALE = 8;     private int nonceCount; // number of times nonce has been used/seen     /* User-supplied/generated information */    private String specifiedCipher;  // cipher explicitly requested by user    private byte[] cnonce;        // client generated nonce    private String username;    private char[] passwd;    private byte[] authzidBytes;  // byte repr of authzid     /**      * Constructor for DIGEST-MD5 mechanism.      *      * @param authzid A non-null String representing the principal      * for which authorization is being granted..      * @param digestURI A non-null String representing detailing the      * combined protocol and host being used for authentication.      * @param props The possibly null properties to be used by the SASL      * mechanism to configure the authentication exchange.      * @param cbh The non-null CallbackHanlder object for callbacks      * @throws SaslException if no authentication ID or password is supplied      */    DigestMD5Client(String authzid, String protocol, String serverName,        Map props, CallbackHandler cbh) throws SaslException {         super(props, MY_CLASS_NAME, 2, protocol + "/" + serverName, cbh);         // authzID can only be encoded in UTF8 - RFC 2222        if (authzid != null) {            this.authzid = authzid;            try {                authzidBytes = authzid.getBytes("UTF8");             } catch (UnsupportedEncodingException e) {                throw new SaslException(                    "DIGEST-MD5: Error encoding authzid value into UTF-8", e);            }        }         if (props != null) {            specifiedCipher = (String)props.get(CIPHER_PROPERTY);             logger.log(Level.FINE, "DIGEST60:Explicitly specified cipher: {0}",                specifiedCipher);        }   }     /**     * DIGEST-MD5 has no initial response     *     * @return false     */    public boolean hasInitialResponse() {        return false;    }     /**     * Process the challenge data.     *     * The server sends a digest-challenge which the client must reply to     * in a digest-response. When the authentication is complete, the     * completed field is set to true.     *     * @param challengeData A non-null byte array containing the challenge     * data from the server.     * @return A possibly null byte array containing the response to     * be sent to the server.     *     * @throws SaslException If the platform does not have MD5 digest support     * or if the server sends an invalid challenge.     */    public byte[] evaluateChallenge(byte[] challengeData) throws SaslException {         if (challengeData.length > MAX_CHALLENGE_LENGTH) {            throw new SaslException(                "DIGEST-MD5: Invalid digest-challenge length. Got:  " +                challengeData.length + " Expected < " + MAX_CHALLENGE_LENGTH);        }         /* Extract and process digest-challenge */        byte[][] challengeVal;         switch (step) {        case 2:            /* Process server's first challenge (from Step 1) */            /* Get realm, qop, maxbuf, charset, algorithm, cipher, nonce               directives */            List<byte[]> realmChoices = new ArrayList<byte[]>(3);            challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,                realmChoices, REALM);             try {                processChallenge(challengeVal, realmChoices);                checkQopSupport(challengeVal[QOP], challengeVal[CIPHER]);                ++step;                return generateClientResponse(challengeVal[CHARSET]);            } catch (SaslException e) {                step = 0;                clearPassword();                throw e; // rethrow            } catch (IOException e) {                step = 0;                clearPassword();                throw new SaslException("DIGEST-MD5: Error generating " +                    "digest response-value", e);            }         case 3:            try {                /* Process server's step 3 (server response to digest response) */                /* Get rspauth directive */                challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,                    null, REALM);                validateResponseValue(challengeVal[RESPONSE_AUTH]);                  /* Initialize SecurityCtx implementation */                if (integrity && privacy) {                    secCtx = new DigestPrivacy(true /* client */);                } else if (integrity) {                    secCtx = new DigestIntegrity(true /* client */);                }                 return null; // Mechanism has completed.            } finally {                clearPassword();                step = 0;  // Set to invalid state                completed = true;            }         default:            // No other possible state            throw new SaslException("DIGEST-MD5: Client at illegal state");        }    }     /**    * Record information from the challengeVal array into variables/fields.    * Check directive values that are multi-valued and ensure that mandatory    * directives not missing from the digest-challenge.    *    * @throws SaslException if a sasl is a the mechanism cannot    * correcly handle a callbacks or if a violation in the    * digest challenge format is detected.    */    private void processChallenge(byte[][] challengeVal, List<byte[]> realmChoices)        throws SaslException, UnsupportedEncodingException {         /* CHARSET: optional atmost once */        if (challengeVal[CHARSET] != null) {            if (!"utf-8".equals(new String(challengeVal[CHARSET], encoding))) {                throw new SaslException("DIGEST-MD5: digest-challenge format " +                    "violation. Unrecognised charset value: " +                    new String(challengeVal[CHARSET]));            } else {                encoding = "UTF8";                useUTF8 = true;            }        }         /* ALGORITHM: required exactly once */        if (challengeVal[ALGORITHM] == null) {            throw new SaslException("DIGEST-MD5: Digest-challenge format " +                "violation: algorithm directive missing");        } else if (!"md5-sess".equals(new String(challengeVal[ALGORITHM], encoding))) {            throw new SaslException("DIGEST-MD5: Digest-challenge format " +                "violation. Invalid value for 'algorithm' directive: " +                challengeVal[ALGORITHM]);        }         /* NONCE: required exactly once */        if (challengeVal[NONCE] == null) {            throw new SaslException("DIGEST-MD5: Digest-challenge format " +                "violation: nonce directive missing");        } else {            nonce = challengeVal[NONCE];        }         try {            /* REALM: optional, if multiple, stored in realmChoices */            String[] realmTokens = null;             if (challengeVal[REALM] != null) {                if (realmChoices == null || realmChoices.size() <= 1) {                    // Only one realm specified                    negotiatedRealm = new String(challengeVal[REALM], encoding);                } else {                    realmTokens = new String[realmChoices.size()];                    for (int i = 0; i < realmTokens.length; i++) {                        realmTokens[i] =                            new String(realmChoices.get(i), encoding);                    }                }            }             NameCallback ncb = authzid == null ?                new NameCallback("DIGEST-MD5 authentication ID: ") :                new NameCallback("DIGEST-MD5 authentication ID: ", authzid);            PasswordCallback pcb =                new PasswordCallback("DIGEST-MD5 password: ", false);             if (realmTokens == null) {                // Server specified <= 1 realm                // If 0, RFC 2831: the client SHOULD solicit a realm from the user.                RealmCallback tcb =                    (negotiatedRealm == null? new RealmCallback("DIGEST-MD5 realm: ") :                        new RealmCallback("DIGEST-MD5 realm: ", negotiatedRealm));                 cbh.handle(new Callback[] {tcb, ncb, pcb});                 /* Acquire realm from RealmCallback */                negotiatedRealm = tcb.getText();                if (negotiatedRealm == null) {                    negotiatedRealm = "";                }            } else {                RealmChoiceCallback ccb = new RealmChoiceCallback(                    "DIGEST-MD5 realm: ",                    realmTokens,                    0, false);                cbh.handle(new Callback[] {ccb, ncb, pcb});                 /* Acquire realm from RealmChoiceCallback*/                negotiatedRealm = realmTokens[ccb.getSelectedIndexes()[0]];            }             passwd = pcb.getPassword();            pcb.clearPassword();            username = ncb.getName();         } catch (UnsupportedCallbackException e) {            throw new SaslException("DIGEST-MD5: Cannot perform callback to " +                "acquire realm, authentication ID or password", e);         } catch (IOException e) {            throw new SaslException(                "DIGEST-MD5: Error acquiring realm, authentication ID or password", e);        }         if (username == null || passwd == null) {            throw new SaslException(                "DIGEST-MD5: authentication ID and password must be specified");        }         /* MAXBUF: optional atmost once */        int srvMaxBufSize =            (challengeVal[MAXBUF] == null) ? DEFAULT_MAXBUF            : Integer.parseInt(new String(challengeVal[MAXBUF], encoding));        sendMaxBufSize =            (sendMaxBufSize == 0) ? srvMaxBufSize            : Math.min(sendMaxBufSize, srvMaxBufSize);    }     /**     * Parses the 'qop' directive. If 'auth-conf' is specified by     * the client and offered as a QOP option by the server, then a check     * is client-side supported ciphers is performed.     *     * @throws IOException     */    private void checkQopSupport(byte[] qopInChallenge, byte[] ciphersInChallenge)        throws IOException {         /* QOP: optional; if multiple, merged earlier */        String qopOptions;         if (qopInChallenge == null) {            qopOptions = "auth";        } else {            qopOptions = new String(qopInChallenge, encoding);        }         // process        String[] serverQopTokens = new String[3];        byte[] serverQop = parseQop(qopOptions, serverQopTokens,            true /* ignore unrecognized tokens */);        byte serverAllQop = combineMasks(serverQop);         switch (findPreferredMask(serverAllQop, qop)) {        case 0:            throw new SaslException("DIGEST-MD5: No common protection " +                "layer between client and server");         case NO_PROTECTION:            negotiatedQop = "auth";            // buffer sizes not applicable            break;         case INTEGRITY_ONLY_PROTECTION:            negotiatedQop = "auth-int";            integrity = true;            rawSendSize = sendMaxBufSize - 16;            break;         case PRIVACY_PROTECTION:            negotiatedQop = "auth-conf";            privacy = integrity = true;            rawSendSize = sendMaxBufSize - 26;            checkStrengthSupport(ciphersInChallenge);            break;        }         if (logger.isLoggable(Level.FINE)) {            logger.log(Level.FINE, "DIGEST61:Raw send size: {0}",                new Integer(rawSendSize));        }     }     /**     * Processes the 'cipher' digest-challenge directive. This allows the     * mechanism to check for client-side support against the list of     * supported ciphers send by the server. If no match is found,     * the mechanism aborts.     *     * @throws SaslException If an error is encountered in processing     * the cipher digest-challenge directive or if no client-side     * support is found.     */    private void checkStrengthSupport(byte[] ciphersInChallenge)        throws IOException {         /* CIPHER: required exactly once if qop=auth-conf */        if (ciphersInChallenge == null) {            throw new SaslException("DIGEST-MD5: server did not specify " +                "cipher to use for 'auth-conf'");        }         // First determine ciphers that server supports        String cipherOptions = new String(ciphersInChallenge, encoding);        StringTokenizer parser = new StringTokenizer(cipherOptions, ", \t\n");        int tokenCount = parser.countTokens();        String token = null;        byte[] serverCiphers = { UNSET,                                 UNSET,                                 UNSET,                                 UNSET,                                 UNSET };        String[] serverCipherStrs = new String[serverCiphers.length];         // Parse ciphers in challenge; mark each that server supports        for (int i = 0; i < tokenCount; i++) {            token = parser.nextToken();            for (int j = 0; j < CIPHER_TOKENS.length; j++) {                if (token.equals(CIPHER_TOKENS[j])) {                    serverCiphers[j] |= CIPHER_MASKS[j];                    serverCipherStrs[j] = token; // keep for replay to server                    logger.log(Level.FINE, "DIGEST62:Server supports {0}", token);                }            }        }         // Determine which ciphers are available on client        byte[] clntCiphers = getPlatformCiphers();         // Take intersection of server and client supported ciphers        byte inter = 0;        for (int i = 0; i < serverCiphers.length; i++) {            serverCiphers[i] &= clntCiphers[i];            inter |= serverCiphers[i];        }         if (inter == UNSET) {            throw new SaslException(                "DIGEST-MD5: Client supports none of these cipher suites: " +                cipherOptions);        } 

Lines 1–500 of 696.