/*
 * @(#)DigestUtils.java	1.22 01/02/16
 *
 * Copyright 2000 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * This software is the proprietary information of Sun Microsystems, Inc.  
 * Use is subject to license terms.
 * 
 */

package com.sun.security.sasl.digest;

import java.util.*;
import java.io.*;
import java.security.*;
import java.security.spec.*;

import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;

import com.sun.security.sasl.preview.*;
import com.sun.security.sasl.util.*;

import javax.crypto.*;
import javax.crypto.spec.*;

import sun.misc.HexDumpEncoder;

/**
 * Utility class for DIGEST-MD5 mechanism. Provides utility methods 
 * and contains two inner classes which implement the DigestSecurityCtx
 * interface. The inner classes provide the funtionality to allow
 * for quality-of-protection (QOP) with integrity checking and
 * privacy.
 *
 * @author Jonathan Bruce
 */
public class DigestUtils extends SaslImpl {
    private final byte[] EMPTY_BYTE_ARRAY = new byte[0];
    protected DigestSecurityCtx secCtx;
    protected byte[] H_A1; // component of response-value
    protected String encoding = "8859_1";  // default unless server specifies utf-8
    protected OutputStream outStream = null;

    protected String cipherSuite;

    /* Supported ciphers for 'auth-conf' */
    static protected final int DES3 = 0;
    static protected final int RC4 = 1;
    static protected final int DES = 2;
    static protected final int RC4_56 = 3;
    static protected final int RC4_40 = 4;
    static protected final String[] CIPHER_TOKENS = { "3des",
						      "rc4",
						      "des",	
						      "rc4-56",
						      "rc4-40" };
    /**
     * Constuctor for DigestUtils class. Calls super constructor
     * to parse properties for mechanism.
     *
     * @params props A Hashtable of properties/value pairs
     * @throws SaslException if a mis-match between the user specified
     * cipher strength and the cipher strength that JCE can support.
     */
    public DigestUtils(Hashtable props) throws SaslException {
        super(props); // sets QOP, STENGTH and BUFFER_SIZE

	if (debug) {
	    outStream = System.err;
	}
    }

    /**
     * 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 {
	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 {
	return (secCtx.wrap(outgoing, start, len));	
    }

    /**
     * Outputs a byte array and converts
     */
    protected final void traceOutput(String traceTag, byte[] output) 
	throws UnsupportedEncodingException, IOException {
	if (debug) {	    
	    outStream.write('\n');	    
	    outStream.write(traceTag.getBytes("UTF8"));
	    outStream.write('\n');
	    
	    new HexDumpEncoder().encodeBuffer(
		new ByteArrayInputStream(output, 0, output.length), outStream);
	}
    }

    protected static final boolean debug = SaslImpl.debug;
        	      
    /**
     * Implementation of the DigestSecurityCtx 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 DigestSecurityCtx {

	/* 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[] Kic; 
	protected byte[] Kis;
	protected int cltSvrSeqNum = 0;
	protected int svrCltSeqNum = 0;

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

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

	    try {
		generateIntegrityKeyPair(); 

	    } 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()
	    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);
	    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);
	    Kis = md5.digest();	    
	}

	/**
	 * 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;
	    }	    	    

	    try {
		byte[] wrapped = new byte[len+10+2+4];

		System.arraycopy(outgoing, start, wrapped, 0, len);

		/* Calculate MAC */
		incrementSeqNum(sequenceNum, 0, 4);

		byte[] KicMAC = getHMAC(Kic, sequenceNum, outgoing, start, len);

		/* Apply MAC to message [0..9] */
		System.arraycopy(KicMAC, 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 (debug) {		    
 		    traceOutput("DigestIntegrity.wrap():", wrapped);
		}
		return wrapped;

	    } catch (NoSuchAlgorithmException e) {
		throw new SaslException("DIGEST-MD5: Error creating " +
		    "instance of MD5 digest alogorithm", e);

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

	    } catch (IOException e) {
		throw new SaslException("DIGEST-MD5: Error accessing " +
		    "buffers required to generate MAC", e);
	    }
	}

	/**
	 * 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;
	    }

	    try {
		// shave off last 16 bytes of message
		byte[] serverMAC = new byte[10];
		byte[] msg = new byte[len - 16];
		byte[] srvMsgType = new byte[2];
		byte[] srvSeqNum = new byte[4];

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

		if (debug) {
		    System.err.println("DigestIntegrity.unwrap():");
		    traceOutput("msg:", msg);
		    traceOutput("serverMAC:", serverMAC);
		    traceOutput("messageType:", srvMsgType);
		    traceOutput("sequenceNum:", srvSeqNum);
		}

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

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

		/* Re-calculate MAC to ensure integrity */
		byte[] KisMAC = getHMAC(Kis, srvSeqNum, msg, 0, msg.length);
		
		/* Compare MAC's */
		boolean msgOK = compareMACs(serverMAC, KisMAC);		

		if (msgOK) {
		    // Increment seq count only if MACs are OK; 
		    // Otherwise discard message and do not increment count
		    svrCltSeqNum++;
		    return msg;
		} else {
		    if (debug) {
			System.err.println("DigestIntegrity.unwrap():");
			traceOutput("unmatched KisMAC:", KisMAC);
		    }
		    return EMPTY_BYTE_ARRAY;
		}

	    } catch (NoSuchAlgorithmException e) {
		throw new SaslException("DIGEST-MD5: Error creating " +
		    "instance of MD5 digest algorithm", e);

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

	    } catch (IOException e) {
		throw new SaslException("DIGEST-MD5: Error accessing " +
		    "buffers required to regenerate MAC", e);
	    }
	}

	/**
         * 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 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 the UTF-8 is not supported
         * on the platform.
	 */
	protected byte[] getHMAC(byte[] Ki, byte[] seqnum, byte[] msg,
	    int start, int len) throws 
	    IOException, NoSuchAlgorithmException, UnsupportedEncodingException {

	    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);
 	    }
	}

	/**
	 * Compare two byte arrays. 
         * 
         * @param A non-null byte array containing a MAC block
         * @param A non-null byte array containing a MAC block
         * @return true if the byte arrays are the same, false otherwise.
         */
	protected boolean compareMACs(byte[] mac1, byte[] mac2) {
	    return (Arrays.equals(mac1, mac2));
	}

	/**
	 * Incremnet client-server sequence count and set anwer in NBO in
	 * sequenceNum array
	 *
	 * @param A non-null byte array for the resulting sequence number in NBO
	 * @param start The offset from where to read the byte array.
	 * @param count The number of bytes to read from the off
	 */
	protected void incrementSeqNum(byte[] sequenceNum, int start, int count) {
	    intToNetworkByteOrder(cltSvrSeqNum++, sequenceNum, start, count);
	}
    }
    

    /**
     * Implementation of the DigestSecurityCtx 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 DigestSecurityCtx {
	/* 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 AlgorithmParameterSpec IVcc; // client-to-server DES IV
	private AlgorithmParameterSpec IVcs; // server-to-client DES IV
	private int seqNum;

	private Cipher cipherCc;
	private Cipher cipherCs;
	private SecretKey keyCc;
	private SecretKey keyCs;

	/**
	 * Initializes the cipher object instances for client-server encryption
	 * and server-client 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.
	 */
	public DigestPrivacy() throws SaslException {	

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

	    try {

		generatePrivacyKeyPair();	
	
		/* Initialise cipher objects */
		cipherCc.init(Cipher.ENCRYPT_MODE, keyCc, IVcc);
		cipherCs.init(Cipher.DECRYPT_MODE, keyCs, IVcs);

	    } catch (InvalidKeyException e) {
		throw new SaslException("DIGEST-MD5: Invalid byte array " +
		    "used to create cipher keys", 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", e);

	    } catch (InvalidAlgorithmParameterException e) {
		throw new SaslException("DIGEST-MD5: Invalid cipher " +
		    "algorithem parameter used to create cipher instance", 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() throws IOException,
	    NoSuchAlgorithmException, UnsupportedEncodingException,
	     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 (cipherSuite.equals(CIPHER_TOKENS[RC4_40])) {
		n = 5; 		/* H(A1)[0..5] */
	    } else if (cipherSuite.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();
	    
	    /* Initialize cipher objects */
	    if (cipherSuite.indexOf(CIPHER_TOKENS[RC4]) > -1) {
		try {
		    cipherCc = Cipher.getInstance("RC4");
		    cipherCs = Cipher.getInstance("RC4");
		    keyCc = new SecretKeySpec(Kcc, "RC4");
		    keyCs = new SecretKeySpec(Kcs, "RC4");
		} catch (javax.crypto.NoSuchPaddingException e) {
		    throw new SaslException(
			"DIGEST-MD5: Incorrect padding used for RC4 cipher", e);
 		}

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

		// DES or 3DES 

		try {
		    String cipherFullname, cipherShortname;
		    int useKeyLen, padLen;
		    
		    if (cipherSuite.equals(CIPHER_TOKENS[DES])) {
			cipherFullname = "DES/CBC/PKCS5Padding";
			cipherShortname = "des";
			useKeyLen = 7;
			padLen = 1;
			
		    } else {
			/* 3DES */
			cipherFullname = "DESede/CBC/PKCS5Padding";
			cipherShortname = "desede";
			useKeyLen = 14;
			padLen = 10;
		    }

		    cipherCc = Cipher.getInstance(cipherFullname);
		    cipherCs = Cipher.getInstance(cipherFullname);

		    byte[] desKeyBuffer = new byte[useKeyLen+padLen];
		    for (int i = 0; i < padLen ; i++) {
			desKeyBuffer[useKeyLen+i] = (byte)0;
		    }

		    /* Generate client-server key */
		    System.arraycopy(Kcc, 0, desKeyBuffer, 0, useKeyLen);
		    keyCc = makeDESKeys(desKeyBuffer, 0, cipherShortname);

		    /* Generate server-client key */
		    System.arraycopy(Kcs, 0, desKeyBuffer, 0, useKeyLen);
		    keyCs = makeDESKeys(desKeyBuffer, 0, cipherShortname);
		    
		    // Set up the DES IV, which is the last 8 bytes of Kcc or Kcs
		    IVcc = new IvParameterSpec(Kcc, 8, 8);
		    IVcs = new IvParameterSpec(Kcs, 8, 8);
		    
		} 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);

		} catch (InvalidKeySpecException e) {
		    throw new SaslException("DIGEST-MD5: Unsupported key " + 
			"specification used.", e);
		}
	    }
	}

	/** 
	 * Create keys suitable for DES / DESede encryption. 
	 * 
         * @param input A non-null byte array containing data for 
	 * DES / DESede key.
	 * @param offset A possibly zero offset, indicating offset from
	 * where the byte array should be read.
	 * @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 SecretKey makeDESKeys(byte[] input,
	    int offset,
	    String desStrength) 
	    throws NoSuchAlgorithmException, InvalidKeyException,
		   InvalidKeySpecException {
		
	    KeySpec spec = null;
	    SecretKeyFactory desFactory = 
		SecretKeyFactory.getInstance(desStrength);

	    if (desStrength.equals("des")) {
		spec = new DESKeySpec(input, offset);
	    } else if (desStrength.equals("desede")) {
		spec = new DESedeKeySpec(input, offset);
	    }

	    return desFactory.generateSecret(spec);
	}

	/**
	 * 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;
	    }

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

		// Calculate padding
		int bs = cipherCc.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(KicMAC, 0, toBeEncrypted, len+padding.length, 10);

		if (debug) {
		    traceOutput("DigestPrivacy.wrap() [before encryption]",
			toBeEncrypted);
		}
		
		/* CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])}) */
		byte[] cipherBlock;
		try {
		    cipherBlock = cipherCc.doFinal(toBeEncrypted);

		} catch (BadPaddingException e) {
		    throw new SaslException(
			"DIGEST-MD5: Invalid padding used for block cipher", e);

		} 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 (debug) {
		    traceOutput("DigestPrivacy.wrap() [after encryption]", wrapped);
		}

		return wrapped;

	    } catch (NoSuchAlgorithmException e) {
		throw new SaslException("DIGEST-MD5: Error creating " +
		    "instance of cipher alogorithm", e);

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

	    } catch (IOException e) {
		throw new SaslException("DIGEST-MD5: Error accessing buffer " +
		    "required to generate encypted data", e);
	    }
	}

	/*
	 * 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;
	    }
	    
	    try {
		byte[] encryptedMsg = new byte[len - 6];
		byte[] srvMsgType = new byte[2];
		byte[] srvSeqNum = new byte[4];

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

		/* Sequence number */		
		if (svrCltSeqNum != networkByteOrderToInt(srvSeqNum, 0, 4)) {
		    throw new SaslException("DIGEST-MD5: Out of order " +
			"sequencing of messages from server. Got: " +
			networkByteOrderToInt(srvSeqNum, 0, 4) + " Expected: " +
			svrCltSeqNum);		
		}

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

		/* CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg}[0..9])}) */
		try {		    
		    byte[] decryptedMsg = cipherCs.doFinal(encryptedMsg);
		    
		    byte[] msgWithPadding = new byte[decryptedMsg.length - 10];
		    byte[] serverMAC = new byte[10];
		    
		    System.arraycopy(decryptedMsg, 0, 
			msgWithPadding, 0, msgWithPadding.length);
		    System.arraycopy(decryptedMsg, msgWithPadding.length,
			serverMAC, 0, 10);

		    int blockSize = cipherCs.getBlockSize();
		    int msgLength = msgWithPadding.length;
		    if (blockSize > 1) {
			// get value of last octet of the byte array 
			msgLength -= 
			    (int)msgWithPadding[msgWithPadding.length - 1];
		    }
		    
		    /* Re-calculate MAC to ensure integrity */
		    byte[] KisMAC =
			getHMAC(Kis, srvSeqNum, msgWithPadding, 0, msgLength);
		    
		    boolean msgOK = compareMACs(serverMAC, KisMAC);

		    if (msgOK) {
			// Increment seq count only if MACs are OK; 
			// Otherwise discard message and do not increment count
			svrCltSeqNum++;

			if (msgLength == msgWithPadding.length) {
			    return msgWithPadding; // no padding
			} else {
			    byte[] clearMsg = new byte[msgLength];
			    System.arraycopy(msgWithPadding, 0, 
				clearMsg, 0, msgLength);
			    return clearMsg;
			}
		    } else {
			if (debug) {
			    System.err.println("DigestIntegrity.unwrap():");
			    traceOutput("unmatched KisMAC:", KisMAC);
			}
			return EMPTY_BYTE_ARRAY;
		    }
		} catch (BadPaddingException e) {
		    throw new SaslException("DIGEST-MD5: Incorrect padding " + 
			"used with chosen cipher", e);

		} catch (IllegalBlockSizeException e) {
		    throw new SaslException("DIGEST-MD5: Illegal block " +
			"sizes used with chosen cipher", e);

		} catch (NoSuchAlgorithmException e) {
		    throw new SaslException("DIGEST-MD5: Unsupported cipher " +
			"used to decrypt data", e);
		}
		    
	    } catch (IOException e) {
		throw new SaslException("DIGEST-MD5: Error reading from " +
		    "byte array buffers", e);
	    }
	}
    }
}
