/*
 * Copyright (c) 2000, 2012, 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.util.Map;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.math.BigInteger;
import java.util.Random;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.io.IOException;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.security.spec.KeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.InvalidAlgorithmParameterException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.Mac;
import javax.crypto.SecretKeyFactory;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.DESedeKeySpec;

import javax.security.sasl.*;
import com.sun.security.sasl.util.AbstractSaslImpl;

import javax.security.auth.callback.CallbackHandler;

/**
 * Utility class for DIGEST-MD5 mechanism. Provides utility methods
 * and contains two inner classes which implement the SecurityCtx
 * interface. The inner classes provide the funtionality to allow
 * for quality-of-protection (QOP) with integrity checking and
 * privacy.
 *
 * @author Jonathan Bruce
 * @author Rosanna Lee
 */
abstract class DigestMD5Base extends AbstractSaslImpl {
    /* ------------------------- Constants ------------------------ */

    // Used for logging
    private static final String DI_CLASS_NAME = DigestIntegrity.class.getName();
    private static final String DP_CLASS_NAME = DigestPrivacy.class.getName();

    /* Constants - defined in RFC2831 */
    protected static final int MAX_CHALLENGE_LENGTH = 2048;
    protected static final int MAX_RESPONSE_LENGTH = 4096;
    protected static final int DEFAULT_MAXBUF = 65536;

    /* Supported ciphers for 'auth-conf' */
    protected static final int DES3 = 0;
    protected static final int RC4 = 1;
    protected static final int DES = 2;
    protected static final int RC4_56 = 3;
    protected static final int RC4_40 = 4;
    protected static final String[] CIPHER_TOKENS = { "3des",
                                                      "rc4",
                                                      "des",
                                                      "rc4-56",
                                                      "rc4-40" };
    private static final String[] JCE_CIPHER_NAME = {
        "DESede/CBC/NoPadding",
        "RC4",
        "DES/CBC/NoPadding",
    };

    /*
     * If QOP is set to 'auth-conf', a DIGEST-MD5 mechanism must have
     * support for the DES and Triple DES cipher algorithms (optionally,
     * support for RC4 [128/56/40 bit keys] ciphers) to provide for
     * confidentiality. See RFC 2831 for details. This implementation
     * provides support for DES, Triple DES and RC4 ciphers.
     *
     * The value of strength effects the strength of cipher used. The mappings
     * of 'high', 'medium', and 'low' give the following behaviour.
     *
     *  HIGH_STRENGTH   - Triple DES
     *                  - RC4 (128bit)
     *  MEDIUM_STRENGTH - DES
     *                  - RC4 (56bit)
     *  LOW_SRENGTH     - RC4 (40bit)
     */
    protected static final byte DES_3_STRENGTH = HIGH_STRENGTH;
    protected static final byte RC4_STRENGTH = HIGH_STRENGTH;
    protected static final byte DES_STRENGTH = MEDIUM_STRENGTH;
    protected static final byte RC4_56_STRENGTH = MEDIUM_STRENGTH;
    protected static final byte RC4_40_STRENGTH = LOW_STRENGTH;
    protected static final byte UNSET = (byte)0;
    protected static final byte[] CIPHER_MASKS = { DES_3_STRENGTH,
                                                   RC4_STRENGTH,
                                                   DES_STRENGTH,
                                                   RC4_56_STRENGTH,
                                                   RC4_40_STRENGTH };

    private static final String SECURITY_LAYER_MARKER =
        ":00000000000000000000000000000000";

    protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    /* ------------------- Variable Fields ----------------------- */

    /* Used to track progress of authentication; step numbers from RFC 2831 */
    protected int step;

    /* Used to get username/password, choose realm for client */
    /* Used to obtain authorization, pw info, canonicalized authzid for server */
    protected CallbackHandler cbh;

    protected SecurityCtx secCtx;
    protected byte[] H_A1; // component of response-value

    protected byte[] nonce;         // server generated nonce

    /* Variables set when parsing directives in digest challenge/response. */
    protected String negotiatedStrength;
    protected String negotiatedCipher;
    protected String negotiatedQop;
    protected String negotiatedRealm;
    protected boolean useUTF8 = false;
    protected String encoding = "8859_1";  // default unless server specifies utf-8

    protected String digestUri;
    protected String authzid;       // authzid or canonicalized authzid

    /**
     * Constucts an instance of DigestMD5Base. Calls super constructor
     * to parse properties for mechanism.
     *
     * @param props A map of property/value pairs
     * @param className name of class to use for logging
     * @param firstStep number of first step in authentication state machine
     * @param digestUri digestUri used in authentication
     * @param cbh callback handler used to get info required for auth
     *
     * @throws SaslException If invalid value found in props.
     */
    protected DigestMD5Base(Map<String, ?> props, String className,
        int firstStep, String digestUri, CallbackHandler cbh)
        throws SaslException {
        super(props, className); // sets QOP, STENGTH and BUFFER_SIZE

        step = firstStep;
        this.digestUri = digestUri;
        this.cbh = cbh;
    }

    /**
     * Retrieves the SASL mechanism IANA name.
     *
     * @return The String "DIGEST-MD5"
     */
    public String getMechanismName() {
        return "DIGEST-MD5";
    }

    /**
     * Unwrap the incoming message using the wrap method of the secCtx object
     * instance.
     *
     * @param incoming The byte array containing the incoming bytes.
     * @param start The offset from which to read the byte array.
     * @param len The number of bytes to read from the offset.
     * @return The unwrapped message according to either the integrity or
     * privacy quality-of-protection specifications.
     * @throws SaslException if an error occurs when unwrapping the incoming
     * message
     */
    public byte[] unwrap(byte[] incoming, int start, int len) throws SaslException {
        if (!completed) {
            throw new IllegalStateException(
                "DIGEST-MD5 authentication not completed");
        }

        if (secCtx == null) {
            throw new IllegalStateException(
                "Neither integrity nor privacy was negotiated");
        }

        return (secCtx.unwrap(incoming, start, len));
    }

    /**
     * Wrap outgoing bytes using the wrap method of the secCtx object
     * instance.
     *
     * @param outgoing The byte array containing the outgoing bytes.
     * @param start The offset from which to read the byte array.
     * @param len The number of bytes to read from the offset.
     * @return The wrapped message according to either the integrity or
     * privacy quality-of-protection specifications.
     * @throws SaslException if an error occurs when wrapping the outgoing
     * message
     */
    public byte[] wrap(byte[] outgoing, int start, int len) throws SaslException {
        if (!completed) {
            throw new IllegalStateException(
                "DIGEST-MD5 authentication not completed");
        }

        if (secCtx == null) {
            throw new IllegalStateException(
                "Neither integrity nor privacy was negotiated");
        }

        return (secCtx.wrap(outgoing, start, len));
    }

    public void dispose() throws SaslException {
        if (secCtx != null) {
            secCtx = null;
        }
    }

    public Object getNegotiatedProperty(String propName) {
        if (completed) {
            if (propName.equals(Sasl.STRENGTH)) {
                return negotiatedStrength;
            } else if (propName.equals(Sasl.BOUND_SERVER_NAME)) {
                return digestUri.substring(digestUri.indexOf('/') + 1);
            } else {
                return super.getNegotiatedProperty(propName);
            }
        } else {
            throw new IllegalStateException(
                "DIGEST-MD5 authentication not completed");
        }
    }

    /* ----------------- Digest-MD5 utilities ---------------- */
    /**
     * Generate random-string used for digest-response.
     * This method uses Random to get random bytes and then
     * base64 encodes the bytes. Could also use binaryToHex() but this
     * is slightly faster and a more compact representation of the same info.
     * @return A non-null byte array containing the nonce value for the
     * digest challenge or response.
     * Could use SecureRandom to be more secure but it is very slow.
     */

    /** This array maps the characters to their 6 bit values */
    private final static char pem_array[] = {
        //       0   1   2   3   4   5   6   7
                'A','B','C','D','E','F','G','H', // 0
                'I','J','K','L','M','N','O','P', // 1
                'Q','R','S','T','U','V','W','X', // 2
                'Y','Z','a','b','c','d','e','f', // 3
                'g','h','i','j','k','l','m','n', // 4
                'o','p','q','r','s','t','u','v', // 5
                'w','x','y','z','0','1','2','3', // 6
                '4','5','6','7','8','9','+','/'  // 7
    };

    // Make sure that this is a multiple of 3
    private static final int RAW_NONCE_SIZE = 30;

    // Base 64 encoding turns each 3 bytes into 4
    private static final int ENCODED_NONCE_SIZE = RAW_NONCE_SIZE*4/3;

    protected static final byte[] generateNonce() {

        // SecureRandom random = new SecureRandom();
        Random random = new Random();
        byte[] randomData = new byte[RAW_NONCE_SIZE];
        random.nextBytes(randomData);

        byte[] nonce = new byte[ENCODED_NONCE_SIZE];

        // Base64-encode bytes
        byte a, b, c;
        int j = 0;
        for (int i = 0; i < randomData.length; i += 3) {
            a = randomData[i];
            b = randomData[i+1];
            c = randomData[i+2];
            nonce[j++] = (byte)(pem_array[(a >>> 2) & 0x3F]);
            nonce[j++] = (byte)(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
            nonce[j++] = (byte)(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
            nonce[j++] = (byte)(pem_array[c & 0x3F]);
        }

        return nonce;

        // %%% For testing using RFC 2831 example, uncomment the following 2 lines
        // System.out.println("!!!Using RFC 2831's cnonce for testing!!!");
        // return "OA6MHXh6VqTrRk".getBytes();
    }

    /**
     * Checks if a byte[] contains characters that must be quoted
     * and write the resulting, possibly escaped, characters to out.
     */
    protected static void writeQuotedStringValue(ByteArrayOutputStream out,
        byte[] buf) {

        int len = buf.length;
        byte ch;
        for (int i = 0; i < len; i++) {
            ch = buf[i];
            if (needEscape((char)ch)) {
                out.write('\\');
            }
            out.write(ch);
        }
    }

    // See Section 7.2 of RFC 2831; double-quote character is not allowed
    // unless escaped; also escape the escape character and CTL chars except LWS
    private static boolean needEscape(String str) {
        int len = str.length();
        for (int i = 0; i < len; i++) {
            if (needEscape(str.charAt(i))) {
                return true;
            }
        }
        return false;
    }

    // Determines whether a character needs to be escaped in a quoted string
    private static boolean needEscape(char ch) {
        return ch == '"' ||  // escape char
            ch == '\\' ||    // quote
            ch == 127 ||     // DEL

            // 0 <= ch <= 31 except CR, HT and LF
            (ch >= 0 && ch <= 31 && ch != 13 && ch != 9 && ch != 10);
    }

    protected static String quotedStringValue(String str) {
        if (needEscape(str)) {
            int len = str.length();
            char[] buf = new char[len+len];
            int j = 0;
            char ch;
            for (int i = 0; i < len; i++) {
                ch = str.charAt(i);
                if (needEscape(ch)) {
                    buf[j++] =  '\\';
                }
                buf[j++] = ch;
            }
            return new String(buf, 0, j);
        } else {
            return str;
        }
    }

    /**
     * Convert a byte array to hexadecimal string.
     *
     * @param a non-null byte array
     * @return a non-null String contain the HEX value
     */
    protected byte[] binaryToHex(byte[] digest) throws
    UnsupportedEncodingException {

        StringBuffer digestString = new StringBuffer();

        for (int i = 0; i < digest.length; i ++) {
            if ((digest[i] & 0x000000ff) < 0x10) {
                digestString.append("0"+
                    Integer.toHexString(digest[i] & 0x000000ff));
            } else {
                digestString.append(
                    Integer.toHexString(digest[i] & 0x000000ff));
            }
        }
        return digestString.toString().getBytes(encoding);
    }

    /**
     * Used to convert username-value, passwd or realm to 8859_1 encoding
     * if all chars in string are within the 8859_1 (Latin 1) encoding range.
     *
     * @param a non-null String
     * @return a non-nuill byte array containing the correct character encoding
     * for username, paswd or realm.
     */
    protected byte[] stringToByte_8859_1(String str) throws SaslException {

        char[] buffer = str.toCharArray();

        try {
            if (useUTF8) {
                for( int i = 0; i< buffer.length; i++ ) {
                    if( buffer[i] > '\u00FF' ) {
                        return str.getBytes("UTF8");
                    }
                }
            }
            return str.getBytes("8859_1");
        } catch (UnsupportedEncodingException e) {
            throw new SaslException(
                "cannot encode string in UTF8 or 8859-1 (Latin-1)", e);
        }
    }

    protected static byte[] getPlatformCiphers() {
        byte[] ciphers = new byte[CIPHER_TOKENS.length];

        for (int i = 0; i < JCE_CIPHER_NAME.length; i++) {
            try {
                // Checking whether the transformation is available from the
                // current installed providers.
                Cipher.getInstance(JCE_CIPHER_NAME[i]);

                logger.log(Level.FINE, "DIGEST01:Platform supports {0}", JCE_CIPHER_NAME[i]);
                ciphers[i] |= CIPHER_MASKS[i];
            } catch (NoSuchAlgorithmException e) {
                // no implementation found for requested algorithm.
            } catch (NoSuchPaddingException e) {
                // no implementation found for requested algorithm.
            }
        }

        if (ciphers[RC4] != UNSET) {
            ciphers[RC4_56] |= CIPHER_MASKS[RC4_56];
            ciphers[RC4_40] |= CIPHER_MASKS[RC4_40];
        }

        return ciphers;
    }

    /**
     * Assembles response-value for digest-response.
     *
     * @param authMethod "AUTHENTICATE" for client-generated response;
     *        "" for server-generated response
     * @return A non-null byte array containing the repsonse-value.
     * @throws NoSuchAlgorithmException if the platform does not have MD5
     * digest support.
     * @throws UnsupportedEncodingException if a an error occurs
     * encoding a string into either Latin-1 or UTF-8.
     * @throws IOException if an error occurs writing to the output
     * byte array buffer.
     */
    protected byte[] generateResponseValue(
        String authMethod,
        String digestUriValue,
        String qopValue,
        String usernameValue,
        String realmValue,
        char[] passwdValue,
        byte[] nonceValue,
        byte[] cNonceValue,
        int nonceCount,
        byte[] authzidValue
        ) throws NoSuchAlgorithmException,
            UnsupportedEncodingException,
            IOException {

        MessageDigest md5 = MessageDigest.getInstance("MD5");
        byte[] hexA1, hexA2;
        ByteArrayOutputStream A2, beginA1, A1, KD;

        // A2
        // --
        // A2 = { "AUTHENTICATE:", digest-uri-value,
        // [:00000000000000000000000000000000] }  // if auth-int or auth-conf
        //
        A2 = new ByteArrayOutputStream();
        A2.write((authMethod + ":" + digestUriValue).getBytes(encoding));
        if (qopValue.equals("auth-conf") ||
            qopValue.equals("auth-int")) {

            logger.log(Level.FINE, "DIGEST04:QOP: {0}", qopValue);

            A2.write(SECURITY_LAYER_MARKER.getBytes(encoding));
        }

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "DIGEST05:A2: {0}", A2.toString());
        }

        md5.update(A2.toByteArray());
        byte[] digest = md5.digest();
        hexA2 = binaryToHex(digest);

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "DIGEST06:HEX(H(A2)): {0}", new String(hexA2));
        }

        // A1
        // --
        // H(user-name : realm-value : passwd)
        //
        beginA1 = new ByteArrayOutputStream();
        beginA1.write(stringToByte_8859_1(usernameValue));
        beginA1.write(':');
        // if no realm, realm will be an empty string
        beginA1.write(stringToByte_8859_1(realmValue));
        beginA1.write(':');
        beginA1.write(stringToByte_8859_1(new String(passwdValue)));

        md5.update(beginA1.toByteArray());
        digest = md5.digest();

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "DIGEST07:H({0}) = {1}",
                new Object[]{beginA1.toString(), new String(binaryToHex(digest))});
        }

        // A1
        // --
        // A1 = { H ( {user-name : realm-value : passwd } ),
        // : nonce-value, : cnonce-value : authzid-value
        //
        A1 = new ByteArrayOutputStream();
        A1.write(digest);
        A1.write(':');
        A1.write(nonceValue);
        A1.write(':');
        A1.write(cNonceValue);

        if (authzidValue != null) {
            A1.write(':');
            A1.write(authzidValue);
        }
        md5.update(A1.toByteArray());
        digest = md5.digest();
        H_A1 = digest; // Record H(A1). Use for integrity & privacy.
        hexA1 = binaryToHex(digest);

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "DIGEST08:H(A1) = {0}", new String(hexA1));
        }

        //
        // H(k, : , s);
        //
        KD = new ByteArrayOutputStream();
        KD.write(hexA1);
        KD.write(':');
        KD.write(nonceValue);
        KD.write(':');
        KD.write(nonceCountToHex(nonceCount).getBytes(encoding));
        KD.write(':');
        KD.write(cNonceValue);
        KD.write(':');
        KD.write(qopValue.getBytes(encoding));
        KD.write(':');
        KD.write(hexA2);

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "DIGEST09:KD: {0}", KD.toString());
        }

        md5.update(KD.toByteArray());
        digest = md5.digest();

        byte[] answer = binaryToHex(digest);

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "DIGEST10:response-value: {0}",
                new String(answer));
        }
        return (answer);
    }

    /**
     * Takes 'nonceCount' value and returns HEX value of the value.
     *
     * @return A non-null String representing the current NONCE-COUNT
     */
    protected static String nonceCountToHex(int count) {

        String str = Integer.toHexString(count);
        StringBuffer pad = new StringBuffer();

        if (str.length() < 8) {
            for (int i = 0; i < 8-str.length(); i ++) {
                pad.append("0");
            }
        }

        return pad.toString() + str;
    }

    /**
     * Parses digest-challenge string, extracting each token
     * and value(s)
     *
     * @param buf A non-null digest-challenge string.
     * @param multipleAllowed true if multiple qop or realm or QOP directives
     *  are allowed.
     * @throws SaslException if the buf cannot be parsed according to RFC 2831
     */
    protected static byte[][] parseDirectives(byte[] buf,
        String[]keyTable, List<byte[]> realmChoices, int realmIndex) throws SaslException {

        byte[][] valueTable = new byte[keyTable.length][];

        ByteArrayOutputStream key = new ByteArrayOutputStream(10);
        ByteArrayOutputStream value = new ByteArrayOutputStream(10);
        boolean gettingKey = true;
        boolean gettingQuotedValue = false;
        boolean expectSeparator = false;
        byte bch;

        int i = skipLws(buf, 0);
        while (i < buf.length) {
            bch = buf[i];

            if (gettingKey) {
                if (bch == ',') {
                    if (key.size() != 0) {
                        throw new SaslException("Directive key contains a ',':" +
                            key);
                    }
                    // Empty element, skip separator and lws
                    i = skipLws(buf, i+1);

                } else if (bch == '=') {
                    if (key.size() == 0) {
                        throw new SaslException("Empty directive key");
                    }
                    gettingKey = false;      // Termination of key
                    i = skipLws(buf, i+1);   // Skip to next nonwhitespace

                    // Check whether value is quoted
                    if (i < buf.length) {
                        if (buf[i] == '"') {
                            gettingQuotedValue = true;
                            ++i; // Skip quote
                        }
                    } else {
                        throw new SaslException(
                            "Valueless directive found: " + key.toString());
                    }
                } else if (isLws(bch)) {
                    // LWS that occurs after key
                    i = skipLws(buf, i+1);

                    // Expecting '='
                    if (i < buf.length) {
                        if (buf[i] != '=') {
                            throw new SaslException("'=' expected after key: " +
                                key.toString());
                        }
                    } else {
                        throw new SaslException(
                            "'=' expected after key: " + key.toString());
                    }
                } else {
                    key.write(bch);    // Append to key
                    ++i;               // Advance
                }
            } else if (gettingQuotedValue) {
                // Getting a quoted value
                if (bch == '\\') {
                    // quoted-pair = "\" CHAR  ==> CHAR
                    ++i;       // Skip escape
                    if (i < buf.length) {
                        value.write(buf[i]);
                        ++i;   // Advance
                    } else {
                        // Trailing escape in a quoted value
                        throw new SaslException(
                            "Unmatched quote found for directive: "
                            + key.toString() + " with value: " + value.toString());
                    }
                } else if (bch == '"') {
                    // closing quote
                    ++i;  // Skip closing quote
                    gettingQuotedValue = false;
                    expectSeparator = true;
                } else {
                    value.write(bch);
                    ++i;  // Advance
                }

            } else if (isLws(bch) || bch == ',') {
                //  Value terminated

                extractDirective(key.toString(), value.toByteArray(),
                    keyTable, valueTable, realmChoices, realmIndex);
                key.reset();
                value.reset();
                gettingKey = true;
                gettingQuotedValue = expectSeparator = false;
                i = skipLws(buf, i+1);   // Skip separator and LWS

            } else if (expectSeparator) {
                throw new SaslException(
                    "Expecting comma or linear whitespace after quoted string: \""
                        + value.toString() + "\"");
            } else {
                value.write(bch);   // Unquoted value
                ++i;                // Advance
            }
        }

        if (gettingQuotedValue) {
            throw new SaslException(
                "Unmatched quote found for directive: " + key.toString() +
                " with value: " + value.toString());
        }

        // Get last pair
        if (key.size() > 0) {
            extractDirective(key.toString(), value.toByteArray(),
                keyTable, valueTable, realmChoices, realmIndex);
        }

        return valueTable;
    }

    // Is character a linear white space?
    // LWS            = [CRLF] 1*( SP | HT )
    // %%% Note that we're checking individual bytes instead of CRLF
    private static boolean isLws(byte b) {
        switch (b) {
        case 13:   // US-ASCII CR, carriage return
        case 10:   // US-ASCII LF, linefeed
        case 32:   // US-ASCII SP, space
        case 9:    // US-ASCII HT, horizontal-tab
            return true;
        }
        return false;
    }

    // Skip all linear white spaces
    private static int skipLws(byte[] buf, int start) {
        int i;
        for (i = start; i < buf.length; i++) {
            if (!isLws(buf[i])) {
                return i;
            }
        }
        return i;
    }

    /**
     * Processes directive/value pairs from the digest-challenge and
     * fill out the challengeVal array.
     *
     * @param key A non-null String challenge token name.
     * @param value A non-null String token value.
     * @throws SaslException if a either the key or the value is null
     */
    private static void  extractDirective(String key, byte[] value,
        String[] keyTable, byte[][] valueTable,
        List<byte[]> realmChoices, int realmIndex) throws SaslException {

        for (int i = 0; i < keyTable.length; i++) {
            if (key.equalsIgnoreCase(keyTable[i])) {
                if (valueTable[i] == null) {
                    valueTable[i] = value;
                    if (logger.isLoggable(Level.FINE)) {
                        logger.log(Level.FINE, "DIGEST11:Directive {0} = {1}",
                            new Object[]{
                                keyTable[i],
                                new String(valueTable[i])});
                    }
                } else if (realmChoices != null && i == realmIndex) {
                    // > 1 realm specified
                    if (realmChoices.isEmpty()) {
                        realmChoices.add(valueTable[i]); // add existing one
                    }
                    realmChoices.add(value);  // add new one
                } else {
                    throw new SaslException(
                        "DIGEST-MD5: peer sent more than one " +
                        key + " directive: " + new String(value));
                }

                break; // end search
            }
        }
     }


    /**
     * Implementation of the SecurityCtx interface allowing for messages
     * between the client and server to be integrity checked. After a
     * successful DIGEST-MD5 authentication, integtrity checking is invoked
     * if the SASL QOP (quality-of-protection) is set to 'auth-int'.
     * <p>
     * Further details on the integrity-protection mechanism can be found
     * at section 2.3 - Integrity protection in the
     * <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC2831</a> definition.
     *
     * @author Jonathan Bruce
     */
    class DigestIntegrity implements SecurityCtx {
        /* Used for generating integrity keys - specified in RFC 2831*/
        static final private String CLIENT_INT_MAGIC = "Digest session key to " +
            "client-to-server signing key magic constant";
        static final private String SVR_INT_MAGIC = "Digest session key to " +
            "server-to-client signing key magic constant";

        /* Key pairs for integrity checking */
        protected byte[] myKi;     // == Kic for client; == Kis for server
        protected byte[] peerKi;   // == Kis for client; == Kic for server

        protected int mySeqNum = 0;
        protected int peerSeqNum = 0;

        // outgoing messageType and sequenceNum
        protected final byte[] messageType = new byte[2];
        protected final byte[] sequenceNum = new byte[4];

        /**
         * Initializes DigestIntegrity implementation of SecurityCtx to
         * enable DIGEST-MD5 integrity checking.
         *
         * @throws SaslException if an error is encountered generating the
         * key-pairs for integrity checking.
         */
        DigestIntegrity(boolean clientMode) throws SaslException {
            /* Initialize magic strings */

            try {
                generateIntegrityKeyPair(clientMode);

            } catch (UnsupportedEncodingException e) {
                throw new SaslException(
                    "DIGEST-MD5: Error encoding strings into UTF-8", e);

            } catch (IOException e) {
                throw new SaslException("DIGEST-MD5: Error accessing buffers " +
                    "required to create integrity key pairs", e);

            } catch (NoSuchAlgorithmException e) {
                throw new SaslException("DIGEST-MD5: Unsupported digest " +
                    "algorithm used to create integrity key pairs", e);
            }

            /* Message type is a fixed value */
            intToNetworkByteOrder(1, messageType, 0, 2);
        }

        /**
         * Generate client-server, server-client key pairs for DIGEST-MD5
         * integrity checking.
         *
         * @throws UnsupportedEncodingException if the UTF-8 encoding is not
         * supported on the platform.
         * @throws IOException if an error occurs when writing to or from the
         * byte array output buffers.
         * @throws NoSuchAlgorithmException if the MD5 message digest algorithm
         * cannot loaded.
         */
        private void generateIntegrityKeyPair(boolean clientMode)
            throws UnsupportedEncodingException, IOException,
                NoSuchAlgorithmException {

            byte[] cimagic = CLIENT_INT_MAGIC.getBytes(encoding);
            byte[] simagic = SVR_INT_MAGIC.getBytes(encoding);

            MessageDigest md5 = MessageDigest.getInstance("MD5");

            // Both client-magic-keys and server-magic-keys are the same length
            byte[] keyBuffer = new byte[H_A1.length + cimagic.length];

            // Kic: Key for protecting msgs from client to server.
            System.arraycopy(H_A1, 0, keyBuffer, 0, H_A1.length);
            System.arraycopy(cimagic, 0, keyBuffer, H_A1.length, cimagic.length);
            md5.update(keyBuffer);
            byte[] Kic = md5.digest();

            // Kis: Key for protecting msgs from server to client
            // No need to recopy H_A1
            System.arraycopy(simagic, 0, keyBuffer, H_A1.length, simagic.length);

            md5.update(keyBuffer);
            byte[] Kis = md5.digest();

            if (logger.isLoggable(Level.FINER)) {
                traceOutput(DI_CLASS_NAME, "generateIntegrityKeyPair",
                    "DIGEST12:Kic: ", Kic);
                traceOutput(DI_CLASS_NAME, "generateIntegrityKeyPair",
                    "DIGEST13:Kis: ", Kis);
            }

            if (clientMode) {
                myKi = Kic;
                peerKi = Kis;
            } else {
                myKi = Kis;
                peerKi = Kic;
            }
        }

        /**
         * Append MAC onto outgoing message.
         *
         * @param outgoing A non-null byte array containing the outgoing message.
         * @param start The offset from which to read the byte array.
         * @param len The non-zero number of bytes for be read from the offset.
         * @return The message including the integrity MAC
         * @throws SaslException if an error is encountered converting a string
         * into a UTF-8 byte encoding, or if the MD5 message digest algorithm
         * cannot be found or if there is an error writing to the byte array
         * output buffers.
         */
        public byte[] wrap(byte[] outgoing, int start, int len)
            throws SaslException {

            if (len == 0) {
                return EMPTY_BYTE_ARRAY;
            }

            /* wrapped = message, MAC, message type, sequence number */
            byte[] wrapped = new byte[len+10+2+4];

            /* Start with message itself */
            System.arraycopy(outgoing, start, wrapped, 0, len);

            incrementSeqNum();

            /* Calculate MAC */
            byte[] mac = getHMAC(myKi, sequenceNum, outgoing, start, len);

            if (logger.isLoggable(Level.FINEST)) {
                traceOutput(DI_CLASS_NAME, "wrap", "DIGEST14:outgoing: ",
                    outgoing, start, len);
                traceOutput(DI_CLASS_NAME, "wrap", "DIGEST15:seqNum: ",
                    sequenceNum);
                traceOutput(DI_CLASS_NAME, "wrap", "DIGEST16:MAC: ", mac);
            }

            /* Add MAC[0..9] to message */
            System.arraycopy(mac, 0, wrapped, len, 10);

            /* Add message type [0..1] */
            System.arraycopy(messageType, 0, wrapped, len+10, 2);

            /* Add sequence number [0..3] */
            System.arraycopy(sequenceNum, 0, wrapped, len+12, 4);
            if (logger.isLoggable(Level.FINEST)) {
                traceOutput(DI_CLASS_NAME, "wrap", "DIGEST17:wrapped: ", wrapped);
            }
            return wrapped;
        }

        /**
         * Return verified message without MAC - only if the received MAC
         * and re-generated MAC are the same.
         *
         * @param incoming A non-null byte array containing the incoming
         * message.
         * @param start The offset from which to read the byte array.
         * @param len The non-zero number of bytes to read from the offset
         * position.
         * @return The verified message or null if integrity checking fails.
         * @throws SaslException if an error is encountered converting a string
         * into a UTF-8 byte encoding, or if the MD5 message digest algorithm
         * cannot be found or if there is an error writing to the byte array
         * output buffers
         */
        public byte[] unwrap(byte[] incoming, int start, int len)
            throws SaslException {

            if (len == 0) {
                return EMPTY_BYTE_ARRAY;
            }

            // shave off last 16 bytes of message
            byte[] mac = new byte[10];
            byte[] msg = new byte[len - 16];
            byte[] msgType = new byte[2];
            byte[] seqNum = new byte[4];

            /* Get Msg, MAC, msgType, sequenceNum */
            System.arraycopy(incoming, start, msg, 0, msg.length);
            System.arraycopy(incoming, start+msg.length, mac, 0, 10);
            System.arraycopy(incoming, start+msg.length+10, msgType, 0, 2);
            System.arraycopy(incoming, start+msg.length+12, seqNum, 0, 4);

            /* Calculate MAC to ensure integrity */
            byte[] expectedMac = getHMAC(peerKi, seqNum, msg, 0, msg.length);

            if (logger.isLoggable(Level.FINEST)) {
                traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST18:incoming: ",
                    msg);
                traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST19:MAC: ",
                    mac);
                traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST20:messageType: ",
                    msgType);
                traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST21:sequenceNum: ",
                    seqNum);
                traceOutput(DI_CLASS_NAME, "unwrap", "DIGEST22:expectedMAC: ",
                    expectedMac);
            }

            /* First, compare MAC's before updating any of our state */
            if (!Arrays.equals(mac, expectedMac)) {
                //  Discard message and do not increment sequence number
                logger.log(Level.INFO, "DIGEST23:Unmatched MACs");
                return EMPTY_BYTE_ARRAY;
            }

            /* Ensure server-sequence numbers are correct */
            if (peerSeqNum != networkByteOrderToInt(seqNum, 0, 4)) {
                throw new SaslException("DIGEST-MD5: Out of order " +
                    "sequencing of messages from server. Got: " +
                    networkByteOrderToInt(seqNum, 0, 4) +
                    " Expected: " +     peerSeqNum);
            }

            if (!Arrays.equals(messageType, msgType)) {
                throw new SaslException("DIGEST-MD5: invalid message type: " +
                    networkByteOrderToInt(msgType, 0, 2));
            }

            // Increment sequence number and return message
            peerSeqNum++;
            return msg;
        }

        /**
         * Generates MAC to be appended onto out-going messages.
         *
         * @param Ki A non-null byte array containing the key for the digest
         * @param SeqNum A non-null byte array contain the sequence number
         * @param msg  The message to be digested
         * @param start The offset from which to read the msg byte array
         * @param len The non-zero number of bytes to be read from the offset
         * @return The MAC of a message.
         *
         * @throws SaslException if an error occurs when generating MAC.
         */
        protected byte[] getHMAC(byte[] Ki, byte[] seqnum, byte[] msg,
            int start, int len) throws SaslException {

            byte[] seqAndMsg = new byte[4+len];
            System.arraycopy(seqnum, 0, seqAndMsg, 0, 4);
            System.arraycopy(msg, start, seqAndMsg, 4, len);

            try {
                SecretKey keyKi = new SecretKeySpec(Ki, "HmacMD5");
                Mac m = Mac.getInstance("HmacMD5");
                m.init(keyKi);
                m.update(seqAndMsg);
                byte[] hMAC_MD5 = m.doFinal();

                /* First 10 bytes of HMAC_MD5 digest */
                byte macBuffer[] = new byte[10];
                System.arraycopy(hMAC_MD5, 0, macBuffer, 0, 10);

                return macBuffer;
            } catch (InvalidKeyException e) {
                throw new SaslException("DIGEST-MD5: Invalid bytes used for " +
                    "key of HMAC-MD5 hash.", e);
            } catch (NoSuchAlgorithmException e) {
                throw new SaslException("DIGEST-MD5: Error creating " +
                    "instance of MD5 digest algorithm", e);
            }
        }

        /**
         * Increment own sequence number and set answer in NBO sequenceNum field.
         */
        protected void incrementSeqNum() {
            intToNetworkByteOrder(mySeqNum++, sequenceNum, 0, 4);
        }
    }

    /**
     * Implementation of the SecurityCtx interface allowing for messages
     * between the client and server to be integrity checked and encrypted.
     * After a successful DIGEST-MD5 authentication, privacy is invoked if the
     * SASL QOP (quality-of-protection) is set to 'auth-conf'.
     * <p>
     * Further details on the integrity-protection mechanism can be found
     * at section 2.4 - Confidentiality protection in
     * <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC2831</a> definition.
     *
     * @author Jonathan Bruce
     */
    final class DigestPrivacy extends DigestIntegrity implements SecurityCtx {
        /* Used for generating privacy keys - specified in RFC 2831 */
        static final private String CLIENT_CONF_MAGIC =
            "Digest H(A1) to client-to-server sealing key magic constant";
        static final private String SVR_CONF_MAGIC =
            "Digest H(A1) to server-to-client sealing key magic constant";

        private Cipher encCipher;
        private Cipher decCipher;

        /**
         * Initializes the cipher object instances for encryption and decryption.
         *
         * @throws SaslException if an error occurs with the Key
         * initialization, or a string cannot be encoded into a byte array
         * using the UTF-8 encoding, or an error occurs when writing to a
         * byte array output buffers or the mechanism cannot load the MD5
         * message digest algorithm or invalid initialization parameters are
         * passed to the cipher object instances.
         */
        DigestPrivacy(boolean clientMode) throws SaslException {

            super(clientMode); // generate Kic, Kis keys for integrity-checking.

            try {
                generatePrivacyKeyPair(clientMode);

            } catch (SaslException e) {
                throw e;

            } catch (UnsupportedEncodingException e) {
                throw new SaslException(
                    "DIGEST-MD5: Error encoding string value into UTF-8", e);

            } catch (IOException e) {
                throw new SaslException("DIGEST-MD5: Error accessing " +
                    "buffers required to generate cipher keys", e);
            } catch (NoSuchAlgorithmException e) {
                throw new SaslException("DIGEST-MD5: Error creating " +
                    "instance of required cipher or digest", e);
            }
        }

        /**
         * Generates client-server and server-client keys to encrypt and
         * decrypt messages. Also generates IVs for DES ciphers.
         *
         * @throws IOException if an error occurs when writing to or from the
         * byte array output buffers.
         * @throws NoSuchAlgorithmException if the MD5 message digest algorithm
         * cannot loaded.
         * @throws UnsupportedEncodingException if an UTF-8 encoding is not
         * supported on the platform.
         * @throw SaslException if an error occurs initializing the keys and
         * IVs for the chosen cipher.
         */
        private void generatePrivacyKeyPair(boolean clientMode)
            throws IOException, UnsupportedEncodingException,
            NoSuchAlgorithmException, SaslException {

            byte[] ccmagic = CLIENT_CONF_MAGIC.getBytes(encoding);
            byte[] scmagic = SVR_CONF_MAGIC.getBytes(encoding);

            /* Kcc = MD5{H(A1)[0..n], "Digest ... client-to-server"} */
            MessageDigest md5 = MessageDigest.getInstance("MD5");

            int n;
            if (negotiatedCipher.equals(CIPHER_TOKENS[RC4_40])) {
                n = 5;          /* H(A1)[0..5] */
            } else if (negotiatedCipher.equals(CIPHER_TOKENS[RC4_56])) {
                n = 7;          /* H(A1)[0..7] */
            } else { // des and 3des and rc4
                n = 16;         /* H(A1)[0..16] */
            }

            /* {H(A1)[0..n], "Digest ... client-to-server..."} */
            // Both client-magic-keys and server-magic-keys are the same length
            byte[] keyBuffer =  new byte[n + ccmagic.length];
            System.arraycopy(H_A1, 0, keyBuffer, 0, n);   // H(A1)[0..n]

            /* Kcc: Key for encrypting messages from client->server */
            System.arraycopy(ccmagic, 0, keyBuffer, n, ccmagic.length);
            md5.update(keyBuffer);
            byte[] Kcc = md5.digest();

            /* Kcs: Key for decrypting messages from server->client */
            // No need to copy H_A1 again since it hasn't changed
            System.arraycopy(scmagic, 0, keyBuffer, n, scmagic.length);
            md5.update(keyBuffer);
            byte[] Kcs = md5.digest();

            if (logger.isLoggable(Level.FINER)) {
                traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
                    "DIGEST24:Kcc: ", Kcc);
                traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
                    "DIGEST25:Kcs: ", Kcs);
            }

            byte[] myKc;
            byte[] peerKc;

            if (clientMode) {
                myKc = Kcc;
                peerKc = Kcs;
            } else {
                myKc = Kcs;
                peerKc = Kcc;
            }

            try {
                SecretKey encKey;
                SecretKey decKey;

                /* Initialize cipher objects */
                if (negotiatedCipher.indexOf(CIPHER_TOKENS[RC4]) > -1) {
                    encCipher = Cipher.getInstance("RC4");
                    decCipher = Cipher.getInstance("RC4");

                    encKey = new SecretKeySpec(myKc, "RC4");
                    decKey = new SecretKeySpec(peerKc, "RC4");

                    encCipher.init(Cipher.ENCRYPT_MODE, encKey);
                    decCipher.init(Cipher.DECRYPT_MODE, decKey);

                } else if ((negotiatedCipher.equals(CIPHER_TOKENS[DES])) ||
                    (negotiatedCipher.equals(CIPHER_TOKENS[DES3]))) {

                    // DES or 3DES
                    String cipherFullname, cipherShortname;

                        // Use "NoPadding" when specifying cipher names
                        // RFC 2831 already defines padding rules for producing
                        // 8-byte aligned blocks
                    if (negotiatedCipher.equals(CIPHER_TOKENS[DES])) {
                        cipherFullname = "DES/CBC/NoPadding";
                        cipherShortname = "des";
                    } else {
                        /* 3DES */
                        cipherFullname = "DESede/CBC/NoPadding";
                        cipherShortname = "desede";
                    }

                    encCipher = Cipher.getInstance(cipherFullname);
                    decCipher = Cipher.getInstance(cipherFullname);

                    encKey = makeDesKeys(myKc, cipherShortname);
                    decKey = makeDesKeys(peerKc, cipherShortname);

                    // Set up the DES IV, which is the last 8 bytes of Kcc/Kcs
                    IvParameterSpec encIv = new IvParameterSpec(myKc, 8, 8);
                    IvParameterSpec decIv = new IvParameterSpec(peerKc, 8, 8);

                    // Initialize cipher objects
                    encCipher.init(Cipher.ENCRYPT_MODE, encKey, encIv);
                    decCipher.init(Cipher.DECRYPT_MODE, decKey, decIv);

                    if (logger.isLoggable(Level.FINER)) {
                        traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
                            "DIGEST26:" + negotiatedCipher + " IVcc: ",
                            encIv.getIV());
                        traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
                            "DIGEST27:" + negotiatedCipher + " IVcs: ",
                            decIv.getIV());
                        traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
                            "DIGEST28:" + negotiatedCipher + " encryption key: ",
                            encKey.getEncoded());
                        traceOutput(DP_CLASS_NAME, "generatePrivacyKeyPair",
                            "DIGEST29:" + negotiatedCipher + " decryption key: ",
                            decKey.getEncoded());
                    }
                }
            } catch (InvalidKeySpecException e) {
                throw new SaslException("DIGEST-MD5: Unsupported key " +
                    "specification used.", e);
            } catch (InvalidAlgorithmParameterException e) {
                throw new SaslException("DIGEST-MD5: Invalid cipher " +
                    "algorithem parameter used to create cipher instance", e);
            } catch (NoSuchPaddingException e) {
                throw new SaslException("DIGEST-MD5: Unsupported " +
                    "padding used for chosen cipher", e);
            } catch (InvalidKeyException e) {
                throw new SaslException("DIGEST-MD5: Invalid data " +
                    "used to initialize keys", e);
            }
        }

        // -------------------------------------------------------------------

        /**
         * Encrypt out-going message.
         *
         * @param outgoing A non-null byte array containing the outgoing message.
         * @param start The offset from which to read the byte array.
         * @param len The non-zero number of bytes to be read from the offset.
         * @return The encrypted message.
         *
         * @throws SaslException if an error occurs when writing to or from the
         * byte array output buffers or if the MD5 message digest algorithm
         * cannot loaded or if an UTF-8 encoding is not supported on the
         * platform.
         */
        public byte[] wrap(byte[] outgoing, int start, int len)
            throws SaslException {

            if (len == 0) {
                return EMPTY_BYTE_ARRAY;
            }

            /* HMAC(Ki, {SeqNum, msg})[0..9] */
            incrementSeqNum();
            byte[] mac = getHMAC(myKi, sequenceNum, outgoing, start, len);

            if (logger.isLoggable(Level.FINEST)) {
                traceOutput(DP_CLASS_NAME, "wrap", "DIGEST30:Outgoing: ",
                    outgoing, start, len);
                traceOutput(DP_CLASS_NAME, "wrap", "seqNum: ",
                    sequenceNum);
                traceOutput(DP_CLASS_NAME, "wrap", "MAC: ", mac);
            }

            // Calculate padding
            int bs = encCipher.getBlockSize();
            byte[] padding;
            if (bs > 1 ) {
                int pad = bs - ((len + 10) % bs); // add 10 for HMAC[0..9]
                padding = new byte[pad];
                for (int i=0; i < pad; i++) {
                    padding[i] = (byte)pad;
                }
            } else {
                padding = EMPTY_BYTE_ARRAY;
            }

            byte[] toBeEncrypted = new byte[len+padding.length+10];

            /* {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])} */
            System.arraycopy(outgoing, start, toBeEncrypted, 0, len);
            System.arraycopy(padding, 0, toBeEncrypted, len, padding.length);
            System.arraycopy(mac, 0, toBeEncrypted, len+padding.length, 10);

            if (logger.isLoggable(Level.FINEST)) {
                traceOutput(DP_CLASS_NAME, "wrap",
                    "DIGEST31:{msg, pad, KicMAC}: ", toBeEncrypted);
            }

            /* CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])}) */
            byte[] cipherBlock;
            try {
                // Do CBC (chaining) across packets
                cipherBlock = encCipher.update(toBeEncrypted);

                if (cipherBlock == null) {
                    // update() can return null
                    throw new IllegalBlockSizeException(""+toBeEncrypted.length);
                }
            } catch (IllegalBlockSizeException e) {
                throw new SaslException(
                    "DIGEST-MD5: Invalid block size for cipher", e);
            }

            byte[] wrapped = new byte[cipherBlock.length+2+4];
            System.arraycopy(cipherBlock, 0, wrapped, 0, cipherBlock.length);
            System.arraycopy(messageType, 0, wrapped, cipherBlock.length, 2);
            System.arraycopy(sequenceNum, 0, wrapped, cipherBlock.length+2, 4);

            if (logger.isLoggable(Level.FINEST)) {
                traceOutput(DP_CLASS_NAME, "wrap", "DIGEST32:Wrapped: ", wrapped);
            }

            return wrapped;
        }

        /*
         * Decrypt incoming messages and verify their integrity.
         *
         * @param incoming A non-null byte array containing the incoming
         * encrypted message.
         * @param start The offset from which to read the byte array.
         * @param len The non-zero number of bytes to read from the offset
         * position.
         * @return The decrypted, verified message or null if integrity
         * checking
         * fails.
         * @throws SaslException if there are the SASL buffer is empty or if
         * if an error occurs reading the SASL buffer.
         */
        public byte[] unwrap(byte[] incoming, int start, int len)
            throws SaslException {

            if (len == 0) {
                return EMPTY_BYTE_ARRAY;
            }

            byte[] encryptedMsg = new byte[len - 6];
            byte[] msgType = new byte[2];
            byte[] seqNum = new byte[4];

            /* Get cipherMsg; msgType; sequenceNum */
            System.arraycopy(incoming, start,
                encryptedMsg, 0, encryptedMsg.length);
            System.arraycopy(incoming, start+encryptedMsg.length,
                msgType, 0, 2);
            System.arraycopy(incoming, start+encryptedMsg.length+2,
                seqNum, 0, 4);

            if (logger.isLoggable(Level.FINEST)) {
                logger.log(Level.FINEST,
                    "DIGEST33:Expecting sequence num: {0}",
                    new Integer(peerSeqNum));
                traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST34:incoming: ",
                    encryptedMsg);
            }

            // Decrypt message
            /* CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])}) */
            byte[] decryptedMsg;

            try {
                // Do CBC (chaining) across packets
                decryptedMsg = decCipher.update(encryptedMsg);

                if (decryptedMsg == null) {
                    // update() can return null
                    throw new IllegalBlockSizeException(""+encryptedMsg.length);
                }
            } catch (IllegalBlockSizeException e) {
                throw new SaslException("DIGEST-MD5: Illegal block " +
                    "sizes used with chosen cipher", e);
            }

            byte[] msgWithPadding = new byte[decryptedMsg.length - 10];
            byte[] mac = new byte[10];

            System.arraycopy(decryptedMsg, 0,
                msgWithPadding, 0, msgWithPadding.length);
            System.arraycopy(decryptedMsg, msgWithPadding.length,
                mac, 0, 10);

            if (logger.isLoggable(Level.FINEST)) {
                traceOutput(DP_CLASS_NAME, "unwrap",
                    "DIGEST35:Unwrapped (w/padding): ", msgWithPadding);
                traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST36:MAC: ", mac);
                traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST37:messageType: ",
                    msgType);
                traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST38:sequenceNum: ",
                    seqNum);
            }

            int msgLength = msgWithPadding.length;
            int blockSize = decCipher.getBlockSize();
            if (blockSize > 1) {
                // get value of last octet of the byte array
                msgLength -= (int)msgWithPadding[msgWithPadding.length - 1];
                if (msgLength < 0) {
                    //  Discard message and do not increment sequence number
                    if (logger.isLoggable(Level.INFO)) {
                        logger.log(Level.INFO,
                            "DIGEST39:Incorrect padding: {0}",
                            new Byte(msgWithPadding[msgWithPadding.length - 1]));
                    }
                    return EMPTY_BYTE_ARRAY;
                }
            }

            /* Re-calculate MAC to ensure integrity */
            byte[] expectedMac = getHMAC(peerKi, seqNum, msgWithPadding,
                0, msgLength);

            if (logger.isLoggable(Level.FINEST)) {
                traceOutput(DP_CLASS_NAME, "unwrap", "DIGEST40:KisMAC: ",
                    expectedMac);
            }

            // First, compare MACs before updating state
            if (!Arrays.equals(mac, expectedMac)) {
                //  Discard message and do not increment sequence number
                logger.log(Level.INFO, "DIGEST41:Unmatched MACs");
                return EMPTY_BYTE_ARRAY;
            }

            /* Ensure sequence number is correct */
            if (peerSeqNum != networkByteOrderToInt(seqNum, 0, 4)) {
                throw new SaslException("DIGEST-MD5: Out of order " +
                    "sequencing of messages from server. Got: " +
                    networkByteOrderToInt(seqNum, 0, 4) + " Expected: " +
                    peerSeqNum);
            }

            /* Check message type */
            if (!Arrays.equals(messageType, msgType)) {
                throw new SaslException("DIGEST-MD5: invalid message type: " +
                    networkByteOrderToInt(msgType, 0, 2));
            }

            // Increment sequence number and return message
            peerSeqNum++;

            if (msgLength == msgWithPadding.length) {
                return msgWithPadding; // no padding
            } else {
                // Get a copy of the message without padding
                byte[] clearMsg = new byte[msgLength];
                System.arraycopy(msgWithPadding, 0, clearMsg, 0, msgLength);
                return clearMsg;
            }
        }
    }

    // ---------------- DES and 3 DES key manipulation routines

    private static final BigInteger MASK = new BigInteger("7f", 16);

    /**
     * Sets the parity bit (0th bit) in each byte so that each byte
     * contains an odd number of 1's.
     */
    private static void setParityBit(byte[] key) {
        for (int i = 0; i < key.length; i++) {
            int b = key[i] & 0xfe;
            b |= (Integer.bitCount(b) & 1) ^ 1;
            key[i] = (byte) b;
        }
    }

    /**
     * Expands a 7-byte array into an 8-byte array that contains parity bits
     * The binary format of a cryptographic key is:
     *     (B1,B2,...,B7,P1,B8,...B14,P2,B15,...,B49,P7,B50,...,B56,P8)
     * where (B1,B2,...,B56) are the independent bits of a DES key and
     * (PI,P2,...,P8) are reserved for parity bits computed on the preceding
     * seven independent bits and set so that the parity of the octet is odd,
     * i.e., there is an odd number of "1" bits in the octet.
     */
    private static byte[] addDesParity(byte[] input, int offset, int len) {
        if (len != 7)
            throw new IllegalArgumentException(
                "Invalid length of DES Key Value:" + len);

        byte[] raw = new byte[7];
        System.arraycopy(input, offset, raw, 0, len);

        byte[] result = new byte[8];
        BigInteger in = new BigInteger(raw);

        // Shift 7 bits each time into a byte
        for (int i=result.length-1; i>=0; i--) {
            result[i] = in.and(MASK).toByteArray()[0];
            result[i] <<= 1;         // make room for parity bit
            in = in.shiftRight(7);
        }
        setParityBit(result);
        return result;
    }

    /**
     * Create parity-adjusted keys suitable for DES / DESede encryption.
     *
     * @param input A non-null byte array containing key material for
     * DES / DESede.
     * @param desStrength A string specifying eithe a DES or a DESede key.
     * @return SecretKey An instance of either DESKeySpec or DESedeKeySpec.
     *
     * @throws NoSuchAlgorithmException if the either the DES or DESede
     * algorithms cannote be lodaed by JCE.
     * @throws InvalidKeyException if an invalid array of bytes is used
     * as a key for DES or DESede.
     * @throws InvalidKeySpecException in an invalid parameter is passed
     * to either te DESKeySpec of the DESedeKeySpec constructors.
     */
    private static SecretKey makeDesKeys(byte[] input, String desStrength)
        throws NoSuchAlgorithmException, InvalidKeyException,
            InvalidKeySpecException {

        // Generate first subkey using first 7 bytes
        byte[] subkey1 = addDesParity(input, 0, 7);

        KeySpec spec = null;
        SecretKeyFactory desFactory =
            SecretKeyFactory.getInstance(desStrength);
        switch (desStrength) {
            case "des":
                spec = new DESKeySpec(subkey1, 0);
                if (logger.isLoggable(Level.FINEST)) {
                    traceOutput(DP_CLASS_NAME, "makeDesKeys",
                        "DIGEST42:DES key input: ", input);
                    traceOutput(DP_CLASS_NAME, "makeDesKeys",
                        "DIGEST43:DES key parity-adjusted: ", subkey1);
                    traceOutput(DP_CLASS_NAME, "makeDesKeys",
                        "DIGEST44:DES key material: ", ((DESKeySpec)spec).getKey());
                    logger.log(Level.FINEST, "DIGEST45: is parity-adjusted? {0}",
                        Boolean.valueOf(DESKeySpec.isParityAdjusted(subkey1, 0)));
                }
                break;
            case "desede":
                // Generate second subkey using second 7 bytes
                byte[] subkey2 = addDesParity(input, 7, 7);
                // Construct 24-byte encryption-decryption-encryption sequence
                byte[] ede = new byte[subkey1.length*2+subkey2.length];
                System.arraycopy(subkey1, 0, ede, 0, subkey1.length);
                System.arraycopy(subkey2, 0, ede, subkey1.length, subkey2.length);
                System.arraycopy(subkey1, 0, ede, subkey1.length+subkey2.length,
                    subkey1.length);
                spec = new DESedeKeySpec(ede, 0);
                if (logger.isLoggable(Level.FINEST)) {
                    traceOutput(DP_CLASS_NAME, "makeDesKeys",
                        "DIGEST46:3DES key input: ", input);
                    traceOutput(DP_CLASS_NAME, "makeDesKeys",
                        "DIGEST47:3DES key ede: ", ede);
                    traceOutput(DP_CLASS_NAME, "makeDesKeys",
                        "DIGEST48:3DES key material: ",
                        ((DESedeKeySpec)spec).getKey());
                    logger.log(Level.FINEST, "DIGEST49: is parity-adjusted? ",
                        Boolean.valueOf(DESedeKeySpec.isParityAdjusted(ede, 0)));
                }
                break;
            default:
                throw new IllegalArgumentException("Invalid DES strength:" +
                    desStrength);
        }
        return desFactory.generateSecret(spec);
    }
}
