/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program 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. 
 * 
 * This program 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 at /legal/license.txt). 
 * 
 * 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 Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package com.sun.midp.ssl;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;

import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;

import com.sun.midp.crypto.*;
import com.sun.midp.pki.*;

/**
 * Implements an SSL record layer that sits atop a TCP connection
 * and beneath the user-visible interface to an SSL socket. It 
 * maintains all the state information necessary to encode/decode
 * application data.
 */
class Record {
    /*
     * IMPL_NOTE: We should try to avoid multiple buffer copies by defining a 
     * new message class that allocates a large enough buffer to begin with 
     * so new headers can be prepended and MAC/padding can be appended by 
     * simple pointer manipulation.
     */ 

    /*
     * SSLRecord types: CCS (Change Cipher Spec), ALRT (Alert),
     * HNDSHK (Handshake) and APP (Application Data)
     */
    /** Change Cipher Spec (20). */ 
    static final byte CCS    = 20;
    /** Alert (21). */
    static final byte ALRT   = 21;
    /** Handshake (22). */
    static final byte HNDSHK = 22;
    /** Application data (23). */
    static final byte APP    = 23;

    // There are two severity levels for alerts
    /** Warning severity level for alerts (1). */
    static final byte WARNING     = 1;
    /** Fatal severity level for alerts (2). */
    static final byte FATAL       = 2; 
    
    // Next, we have various Alert types
    /** Close notification alert type (0). */
    static final byte CLOSE_NTFY  = 0;
    /** Unexpected message alert type (10). */
    static final byte UNEXP_MSG   = 10;
    /** Bad MAC alert type (20). */
    static final byte BAD_MAC     = 20;
    // static final byte DECOMP_FAIL = 30;  we do not support compression
    /** Handshake failure alert type (40). */
    static final byte HNDSHK_FAIL = 40;
    /** No certificate found alert type (41). */
    static final byte NO_CERT     = 41;
    /** Bad certificate alert type (42). */
    static final byte BAD_CERT    = 42;
    /** Unsupported certificate alert type (43). */
    static final byte UNSUP_CERT  = 43;
    /** Certificate revoked alert type (44). */
    static final byte CERT_REVKD  = 44;
    /** Certificate expired alaert type (45). */
    static final byte CERT_EXPRD  = 45;
    /** Unknown certificate feature alert type (46). */
    static final byte CERT_UNKWN  = 46;
    /** Bad parameter alert type (47). */
    static final byte BAD_PARAM   = 47;
    
    /*
     * Possible roles for this SSL record layer (client or server).
     */
    /** Server role for SSL record layout (0). */ 
    static final byte SERVER    = 0; 
    /** Client role for SSL record layout (1). */
    static final byte CLIENT    = 1;  
    
     /** Size of record header */
    private final int HEADER_SIZE = 5;
    /** Underlying input stream beneath the record layer. */
    private InputStream in; 
    /** Underlying output stream beneath the record layer. */
    private OutputStream out;
    
    // Connection state information
    /** Flag indicating change cipher spec received. */
    private byte rActive = 0; 
    /** Flag indicating change cipher spec has been sent. */
    private byte wActive = 0; 

    /** The SSL version in one byte (0x30=3.0). */
    private byte ver;
    /** Current input record header. */
    private byte[] inputHeader = new byte[HEADER_SIZE];
    /** How many bytes of the record header have been read. */
    private int headerBytesRead;
    /** How many bytes of the data in the record. */
    private int dataLength;
    /** How many bytes of the data in the record have been read. */
    private int dataBytesRead;
    /** Shutdown flag, true if connection has been shutdown. */
    private boolean shutdown;

    /** Current input record data. */
    byte[] inputData;
    /** Length of the plain text in the input buffer */
    int plainTextLength;

    /** Records encoder */
    private RecordEncoder encoder = null;
    /** Records decoder */
    private RecordDecoder decoder = null;
    
    
    /**
     * Creates a new SSL record layer.
     * 
     * @param ins  input stream belonging to the underlying TCP connection
     * @param outs output stream belonging to the underlying TCP connection
     */
    Record(InputStream ins, OutputStream outs) {
        in = ins;
        out = outs;
        ver = (byte) 0x30;  // IMPL_NOTE: This is hardcoded for now
    }
        
        
    /**
     * Chops up a master secret into the client and server MAC secrets, 
     * bulk encryption keys and IVs. Also initializes the Cipher and 
     * MessageDigest objects used in record encoding/decoding. 
     * 
     * @param role role (either CLIENT or SERVER) of this side in the SSL 
     *             negotiation
     * @param clientRand 32-byte random value chosen by the client
     * @param serverRand 32-byte random value chosen by the server
     * @param suite negotiated cipher suite
     * @param masterSecret master secret resulting from the key exchange
     *
     * @exception Exception if the negotiated cipher suite involves an 
     *                      unsupported hash or cipher algorithm
     */
    void init(byte role, byte[] clientRand, byte[] serverRand, 
              byte suite, byte[] masterSecret) throws Exception {
        CipherSuiteData data = new CipherSuiteData(suite);
        data.generateKeys(clientRand, serverRand, masterSecret);

        // depending on role, we choose corresponding MAC secrets 
        // and cipher bulk keys for encoder and decoder
        byte[] encodeSecret;
        byte[] decodeSecret;
        SecretKey decodeCipherKey;
        SecretKey encodeCipherKey;
        if (role == CLIENT) {
            encodeSecret = data.getClientMACSecret();
            decodeSecret = data.getServerMACSecret();
            encodeCipherKey = data.getClientBulkKey();
            decodeCipherKey = data.getServerBulkKey();
        } else {
            encodeSecret = data.getServerMACSecret();
            decodeSecret = data.getClientMACSecret();
            encodeCipherKey = data.getServerBulkKey();
            decodeCipherKey = data.getClientBulkKey();
        }
        
        Cipher encodeCipher = data.getEncodeCipher();
        encodeCipher.init(Cipher.ENCRYPT_MODE, encodeCipherKey);
        Cipher decodeCipher = data.getDecodeCipher();
        decodeCipher.init(Cipher.DECRYPT_MODE, decodeCipherKey);
        
        encoder = new RecordEncoder(data.getEncodeDigest(), encodeSecret,
                data.getPadLength(), encodeCipher);
        decoder = new RecordDecoder(data.getDecodeDigest(), decodeSecret,
                data.getPadLength(), decodeCipher);
    }
            
            
    /**
     * Reads and returns a record (including the 5-byte header) of 
     * the specified type. If the caller asks for application data 
     * and a close_notify warning alert is found as the next available
     * record, this method sets plainTextLength to -1 to signal the end of the 
     * input stream.
     *
     * @param block if true the method will not return until data is available,
     *              or end of stream
     * @param type desired SSL record type
     *
     * @exception IOException if an unexpected record type or SSL alert is
     *                        found in the underlying sockets input stream 
     */ 
    void rdRec(boolean block, byte type) throws IOException {
        if (!rdRec(block)) {
            return;
        }

        if (inputHeader[0] == type) {
            // success
            return;
        }

        // Signal end of stream.
        plainTextLength = -1;

        switch (inputHeader[0]) {
        case CCS: 
            // Can change_cipher_spec can be passed to handshake clients?
            // if (type == HNDSHK) return r; // fall through otherwise
        case HNDSHK:
        case APP:
        default:
            alert(FATAL, UNEXP_MSG);
            throw new IOException("Unexpected SSL record, type: " +
                                  inputHeader[0]);
            
        case ALRT:
            // An Alert record needs to be atleast 2 bytes of data
            if (inputData.length < 2) {
                throw new IOException("Bad alert length");
            }

            if ((inputData[0] == WARNING) && 
                (inputData[1] == CLOSE_NTFY) && 
                (type == APP)) {
                /*
                 * We got a close_notify warning
                 * Shutdown the connection.
                 */ 
                shutdownConnection();
                return;  // signal end of InputStream
            }

            if ((inputData[0] < WARNING) || (inputData[0] > FATAL)) {
                throw new IOException("Bad alert level");
            }

            throw new IOException("Alert (" + inputData[0] + 
                                  "," + inputData[1] + ")");
        }
    }
            
    /**
     * Returns the next record read from the record layer (the 5-byte
     * SSL record header is included). Set plainTextLength to length of the
     * record or -1 for end of stream.
     *
     * @param block if true the method will not return until data is available,
     *              or end of stream
     *
     * @return true if a record has been read
     *
     * @exception IOException if an I/O error occurs
     */ 
    private boolean rdRec(boolean block) throws IOException {
        int b;

        plainTextLength = 0;

        if (!block && in.available() == 0) {
            return false;
        }

        /*
         * This method could have returned last time after reading the
         * part of the record, in that case headerBytesRead will not be 0.
         */
        if (headerBytesRead == 0) {
            b = in.read(inputHeader, 0, 1);
            if (b == -1) {
                /*
                 * Peer closed SSL connection without close_notify
                 */ 
                plainTextLength = -1;
                return false;
            }

            headerBytesRead = 1;
            dataBytesRead = 0;
            dataLength = 0;
        }

        while (headerBytesRead < inputHeader.length) {
            if (!block && in.available() == 0) {
                return false;
            }

            b = in.read(inputHeader, headerBytesRead,
                        inputHeader.length - headerBytesRead);
            if (b == -1) {
                throw new IOException("SSL connection ended abnormally " +
                                      "while reading record header");
            }

            headerBytesRead += b;
        }

        /*
         * This method could have returned last time after reading the
         * header but not all of the data, in that case dataLength
         * will not be 0.
         */
        if (dataLength == 0) {
            // Check record type and version
            if ((inputHeader[0] < CCS) || 
                (inputHeader[0] > APP) ||
                (inputHeader[1] != (byte) (ver >>> 4)) ||
                (inputHeader[2] != (byte) (ver & 0x0f))) {
                alert(FATAL, UNEXP_MSG);
                throw new IOException("Bad record type (" + inputHeader[0] + 
                                  ") or version (" + inputHeader[1] + "." +
                                  inputHeader[2] + ")");
            }
        
            dataLength = ((inputHeader[3] & 0xff) << 8) + 
                (inputHeader[4] & 0xff);
            inputData = new byte[dataLength];
        }

        while (dataBytesRead < dataLength) {
            if (!block && in.available() == 0) {
                return false;
            }

            b = in.read(inputData, dataBytesRead, dataLength - dataBytesRead);
            if (b == -1) {
                throw new IOException("SSL connection ended abnormally " +
                                      "after reading record byte " +
                                      (dataBytesRead + headerBytesRead));
            }

            dataBytesRead += b;
        }

        if (rActive == 1) {
            try {
                plainTextLength = decoder.decode(inputHeader, inputData);
            } catch (IOException e) {
                if (e.getMessage().compareTo("Bad MAC") == 0) {
                    alert(FATAL, BAD_MAC);
                } else {
                    throw e;
                }
            }
        } else {
            plainTextLength = dataBytesRead;
        }

        if (inputHeader[0] == CCS) {
            rActive = 1;
        }

        // start with a new record header next time
        headerBytesRead = 0;

        return true;
    }
            
    /**
     * Writes an SSL record to the underlying socket's output stream.
     * 
     * @param type record type (one of CCS, ALRT, HNDSHK or APP)
     * @param buf byte array containing the record body (i.e. everything
     * but the 5-byte header)
     * @param off starting offset of the record body inside buf
     * @param len length of the record body, the maximum is 2^14 +2048 as
     *            defined by RFC 2246
     *
     * @exception IOException if an I/O error occurs.
     */
    /*
     * REVISIT: We currently do not handle fragmentation and only 
     * increment sequence numbers when encoding/decoding are 
     * turned on. Is it necessary to maintain these counts for
     * handshake messages as well???
     */ 
    void wrRec(byte type, byte[] buf, int off, int len) throws IOException {
        byte[] rec;

        if (shutdown) {
            throw new IOException("Server has shutdown the connection");
        }

        /*
         * Create a new byte array with room for the header and
         * fill the record header with type, version and length
         */
        rec = new byte[len + 5];
        rec[0] = type;
        rec[1] = (byte) (ver >>> 4);
        rec[2] = (byte) (ver & 0x0f);
        rec[3] = (byte) (len >>> 8);
        rec[4] = (byte) (len & 0xff);
        // Fill the rest of the record
        System.arraycopy(buf, off, rec, 5, len);
        if (wActive == 1) {
            out.write(encoder.encode(rec));
        } else {
            out.write(rec);
        }
        if (type == CCS) wActive = 1;
    }       
            
    /**
     * Sends an alert message of the specified level and type to the SSL peer.
     * 
     * @param level one of WARNING or FATAL)
     * @param type one of CLOSE_NTFY, UNEXP_MSG, BAD_MAC, DECOMP_FAIL,
     *             HNDSHK_FAIL, NO_CERT, BAD_CERT, UNSUP_CERT, CERT_REVKD,
     *             CERT_EXPRD, CERT_UNKWN, BAD_PARAM
     */ 
    public void alert(byte level, byte type) {
        byte[] tmp = new byte[2];
        tmp[0] = level;
        tmp[1] = type;

        try {
            wrRec(ALRT, tmp, 0, 2);
        } catch (IOException e) {
            // ignore, we do not want to step on the real error
        }
    }

    /**
     * Send a close notify and shutdown the TCP connection if needed.
     */
    public void shutdownConnection() {
        if (shutdown) {
            return;
        }

        alert(Record.WARNING, Record.CLOSE_NTFY);
        shutdown = true;

        try {
            try {
                out.close();
            } finally {
                in.close();
            }
        } catch (IOException e) {
            // ignore
        }

    }
}

/**
 * All kinds of data related to cipher suite like keys,
 * digestes, etc.
 */
class CipherSuiteData {
    /** Client MAC secret. */
    private byte[] clientMACSecret = null;
    /** Server MAC secret. */       
    private byte[] serverMACSecret = null; 
    /** Client write key for bulk encryption. */
    private byte[] clientKey = null;
    /** Server write key for bulk encryption. */
    private byte[] serverKey = null;
    /** Client write IV for block encryption. */
    private byte[] clientIV = null;
    /** Server write IV for block encryption. */
    private byte[] serverIV = null;
    /** Clients bulk encryption secret key. */
    private SecretKey clientBulkKey = null;
    /** Servers bulk encryption secret key. */
    private SecretKey serverBulkKey = null;
    /** Length of the digest */
    private int digestLength = 0;
    /** Digest for encoding */
    private MessageDigest encodeDigest = null;
    /** Digest for decoding */
    private MessageDigest decodeDigest = null;
    /** Encode cipher */
    private Cipher encodeCipher = null;
    /** Decode cipher */
    private Cipher decodeCipher = null;
    /** Length of PAD1/PAD2 used in MACs. */
    private int padLength = 0;
    /** Cipher suite type */
    private byte suiteType = 0;
    /** Digest used for keys generation */
    private MessageDigest md = null;
    /** Digest used for keys generation */
    private MessageDigest sd = null;
    /** Block of generated keys */
    private byte[] keyBlock = null;

    /**
     * Constructs CipherSuiteData object
     * 
     * @param suite negotiated cipher suite
     *
     * @exception Exception if the negotiated cipher suite involves an 
     *                      unsupported hash or cipher algorithm
     */       
    CipherSuiteData(byte suite) throws Exception {
        suiteType = suite;
        
        /* 
         * The actual size of our computed key block is the closest 
         * 16-byte multiple and depends on the choice of hashing
         * and encryption algorithms.
         */
        int keyMaterial = 5;

        switch (suite) {
        case Handshake.ARCFOUR_128_MD5:
            keyMaterial = 16;
            // fall through
        case Handshake.ARCFOUR_40_MD5:
            padLength = 48;
            encodeDigest = MessageDigest.getInstance("MD5");
            break;
    
        case Handshake.ARCFOUR_128_SHA:
            keyMaterial = 16;
            padLength = 40;
            encodeDigest = MessageDigest.getInstance("SHA-1");
            break;

        default:
            throw new Exception("Unsupported suite");
        }

        decodeDigest = (MessageDigest)encodeDigest.clone();
        digestLength = encodeDigest.getDigestLength();

        encodeCipher = Cipher.getInstance("ARC4");
        decodeCipher = Cipher.getInstance("ARC4");
        
            
        /* 
         * Key block size is the closest 16-byte multiple larger than
         *       2x(hash_size + key_material + IV_size)
         * int keyMaterial = CipherSuite.cipherList[
         * CipherSuite.suiteList[suite][2]][2];
         * int ivSize = CipherSuite.cipherList[
         *          CipherSuite.suiteList[suite][2]][4];
         */

        int ivSize = 0; // stream ciphers do not use IVs
            
        int blockSize = (digestLength + keyMaterial + ivSize) << 1;
        blockSize = ((blockSize + 15) >>> 4) << 4;
        keyBlock = new byte[blockSize];

        clientMACSecret = new byte[digestLength];
        serverMACSecret = new byte[digestLength];
        clientKey = new byte[keyMaterial];
        serverKey = new byte[keyMaterial];
        clientIV = new byte[ivSize];
        serverIV = new byte[ivSize];

        md = MessageDigest.getInstance("MD5");
        sd = MessageDigest.getInstance("SHA-1");
    }

    /**
     * Chops up a master secret into the client and server MAC secrets, 
     * bulk encryption keys and IVs.
     * 
     * @param clientRand 32-byte random value chosen by the client
     * @param serverRand 32-byte random value chosen by the server
     * @param masterSecret master secret resulting from the key exchange
     *
     * @exception GeneralSecurityException thrown in case of failure
     */
    void generateKeys(byte[] clientRand, byte[] serverRand, 
                      byte[] masterSecret) 
            throws GeneralSecurityException {
        generateKeysBlock(clientRand, serverRand, masterSecret);
        chopKeysBlock(clientRand, serverRand, masterSecret);
        keyBlock = null;
        md = null;
        sd = null;
    }

    
    /**
     * Get client MAC secret
     * @return client MAC secret
     */
    byte[] getClientMACSecret() {
        return clientMACSecret;
    }
    
    /**
     * Get server MAC secret
     * @return server MAC secret
     */
    byte[] getServerMACSecret() {
        return serverMACSecret;
    }
        
    /**
     * Get client bulk key
     * @return client bulk key
     */
    SecretKey getClientBulkKey() {
        return clientBulkKey;
    }

    /**
     * Get server bulk key
     * @return server bulk key
     */
    SecretKey getServerBulkKey() {
        return serverBulkKey;
    }

    /**
     * Get digest used for encoding 
     * @return encode digest
     */
    MessageDigest getEncodeDigest() {
        return encodeDigest;
    }

    /**
     * Get digest used for decoding
     * @return decode digest
     */
    MessageDigest getDecodeDigest() {
        return decodeDigest;
    }    

    /**
     * Get cipher used for encoding
     * @return encode cipher
     */
    Cipher getEncodeCipher() {
        return encodeCipher;
    }

    /**
     * Get cipher used for decoding
     * @return decode cipher
     */
    Cipher getDecodeCipher() {
        return decodeCipher;
    }
        

    /**
     * Get pad length used for MAC computation 
     * @return pad length
     */
    int getPadLength() {
        return padLength;
    }    


    /**
     * Generates keys block
     * 
     * @param clientRand 32-byte random value chosen by the client
     * @param serverRand 32-byte random value chosen by the server
     * @param masterSecret master secret resulting from the key exchange
     *
     * @exception GeneralSecurityException thrown in case of failure
     */    
    private void generateKeysBlock(byte[] clientRand, byte[] serverRand, 
                                   byte[] masterSecret)
            throws GeneralSecurityException {
        /* 
         * The following should suffice to generate a total of
         * 16*7 = 112 bytes of key material. 3DES_SHA requires
         * 2*(20 + 24 + 8) = 104 bytes.
         */ 
        byte[] expansion[] = { 
                { 0x41 },                                     // 'A'
                { 0x42, 0x42 },                               // 'BB'
                { 0x43, 0x43, 0x43 },                         // 'CCC'
                { 0x44, 0x44, 0x44, 0x44 },                   // 'DDDD'
                { 0x45, 0x45, 0x45, 0x45, 0x45 },             // 'EEEEE'
                { 0x46, 0x46, 0x46, 0x46, 0x46, 0x46 },       // 'FFFFFF'
                { 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47 }, // 'GGGGGGG'
        };            

            
        /*
         * key_block =
         *    MD5(master + SHA('A' + master +
         *                     ServerHello.random + ClientHello.random)) +
         *    MD5(master + SHA('BB' + master +
         *                     ServerHello.random + ClientHello.random)) +
         *    MD5(master + SHA('CCC' + master +
         *                     ServerHello.random + ClientHello.random)) +
         *    [..]
         *
         * We set 
         *     subExp = master + ServerHello.random + ClientHello.random
         */ 
        byte[] blockSubExp = new byte[masterSecret.length + 
            serverRand.length + clientRand.length];
        int offset = 0;
        System.arraycopy(masterSecret, 0, blockSubExp, offset, 
                masterSecret.length);
        offset += masterSecret.length;
        System.arraycopy(serverRand, 0, blockSubExp, offset, 
                serverRand.length);
        offset += serverRand.length;
        System.arraycopy(clientRand, 0, blockSubExp, offset,
                clientRand.length);

        for (int i = 0; i < (keyBlock.length >>> 4); i++) {
            md.update(masterSecret, 0, masterSecret.length);
            sd.update(expansion[i], 0, expansion[i].length);
            byte[] res = new byte[20];
            sd.update(blockSubExp, 0, blockSubExp.length);
            sd.digest(res, 0, res.length);
            md.update(res, 0, 20);
            md.digest(keyBlock, i << 4, md.getDigestLength());
        }            
    }

    /**
     * Extract keys form keys block
     * 
     * @param clientRand 32-byte random value chosen by the client
     * @param serverRand 32-byte random value chosen by the server
     * @param masterSecret master secret resulting from the key exchange
     *
     * @exception GeneralSecurityException thrown in case of failure
     */
    private void chopKeysBlock(byte[] clientRand, byte[] serverRand,
                               byte[] masterSecret) 
            throws GeneralSecurityException {
        int offset = 0;
        
        System.arraycopy(keyBlock, 0, clientMACSecret, 
                0, clientMACSecret.length);
        offset += clientMACSecret.length;
    
        System.arraycopy(keyBlock, offset, serverMACSecret, 
                0, serverMACSecret.length);
        offset += serverMACSecret.length;
        
        System.arraycopy(keyBlock, offset, clientKey, 0, clientKey.length);
        offset += clientKey.length;
        
        System.arraycopy(keyBlock, offset, serverKey, 0, serverKey.length);
        offset += serverKey.length;

        if (suiteType == Handshake.ARCFOUR_128_MD5 ||
            suiteType == Handshake.ARCFOUR_128_SHA) {
        /*
         * NOTE: We know ivSze is always zero for ARCFOUR cipher suites
         * so this is commented out. It wasn't removed in case we need
         * to add support for DES or another block cipher.
         * if (ivSize != 0) {
         *     // bulk encryption uses a block cipher, so initialize IVs
         *     System.arraycopy(keyBlock, 
         *         2*(clientMACSecret.length + clientKey.length), 
         *         clientIV, 0, ivSize);
         *     System.arraycopy(keyBlock, 
         *         2*(clientMACSecret.length + clientKey.length) 
         *         + ivSize, serverIV, 0, ivSize);
         * }
         */ 
        } else {
        /* 
         * IMPL_NOTE: The only other option is ARCFOUR_40_MD5
         * 
         * Expand the keys for exportable cipher suites
         *  final_client_write_key = MD5(client_write_key +
         *                                ClientHello.random +
         *                               ServerHello.random);
         *  final_server_write_key = MD5(server_write_key +
         *                               ServerHello.random +
         *                               ClientHello.random);
         */
        byte[] res = new byte[16];
        md.update(clientKey, 0, clientKey.length);
        md.update(clientRand, 0, clientRand.length);
        md.update(serverRand, 0, serverRand.length);
        md.digest(res, 0, res.length);
            
        /* 
         * NOTE: For both ARCFOUR_128_MD5 and ARCFOUR_40_MD5, 
         * expanded key is 16
         */ 
        byte[] fcKey = new byte[16]; 
        System.arraycopy(res, 0, fcKey, 0, fcKey.length);
                
        md.update(serverKey, 0, serverKey.length);
        md.update(serverRand, 0, serverRand.length);
        md.update(clientRand, 0, clientRand.length);
        md.digest(res, 0, res.length);
        byte[] fserverKey = new byte[fcKey.length];
        System.arraycopy(res, 0, fserverKey, 0, fserverKey.length);
            
        clientKey = fcKey;
        serverKey = fserverKey;
            
        /* 
         * ... and compute IVs in a special way
         * client_write_IV = MD5(ClientHello.random + ServerHello.random)
         * server_write_IV = MD5(ServerHello.random + ClientHello.random)
         * 
         * NOTE: We know for the chosen ciphersuites, ivSize is zero
         * so this code is commented out for now
            if (ivSize != 0) {
                md.update(clientRand, (short) 0, 
                    (short) clientRand.length);
                md.doFinal(serverRand, (short) 0, 
                    (short) serverRand.length, res, (short) 0);
                System.arraycopy(res, 0, clientIV, 0, ivSize);
                
                md.update(serverRand, (short) 0, 
                    (short) serverRand.length);
                md.doFinal(clientRand, (short) 0, 
                    (short) clientRand.length, res, (short) 0);
                System.arraycopy(res, 0, serverIV, 0, ivSize);
        }
        */ 
        }

        /*
         * Now initialize the ciphers and keys. FOr now this is always
         * ARCFOUR and we comment out support for other ciphers.
         */ 
        clientBulkKey = new SecretKey(clientKey, 0, clientKey.length, "ARC4");
        serverBulkKey = new SecretKey(serverKey, 0, serverKey.length, "ARC4");
    }
}

/**
 * Implements MAC computation
 */
class MAC {
    /** 
     * PAD1 is a 48-byte array filled with 0x36
     */
    static final byte[] PAD1 = {    
        0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
        0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
        0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
        0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
        0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
        0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36
    };
        
    /** 
     * PAD2 is a 48-byte array filled with 0x5c
     */
    static final byte[] PAD2 = {
        0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c,
        0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c,
        0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c,
        0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c,
        0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c,
        0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c
    };
    
    /**
     * Data used for MAC computation
     */
    /** MAC secret */
    protected byte[] macSecret = null;
    /** Message digest */
    protected MessageDigest digest = null;
    /** Message digest length */
    protected int digestLength = 0;
    /** pad length */
    protected int padLength = 0;
    /** Write sequence number */
    private long sequenceNumber = 0;
        
        
    /** 
     * Computes the MAC for an SSLCompressed structure.
     * 
     * @param type SSL record type of the SSLCompressed structure
     * @param buf byte array containing the SSLCompressed fragment
     * @param offset starting offset of the fragment in buf
     * @param length length of the fragment
     *
     * @return a byte array containing the MAC
     */
    byte[] getMAC(byte type, byte[] buf, int offset, int length) {
        /* 
         * MAC = hash(MAC_secret + PAD2 +
         *    hash(MAC_secret + PAD1 + seq_num + type + len +
         *         compressed_fragment));
         */ 

        // Compute the inner hash first
        byte[] byteArray = null;
        digest.update(macSecret, 0, macSecret.length);
        digest.update(PAD1, 0, padLength);
        byteArray = Utils.longToBytes(sequenceNumber);
        digest.update(byteArray, 0, byteArray.length);
        
        byteArray = new byte[3];
        byteArray[0] = type;
        byteArray[1] = (byte) (length >>> 8);
        byteArray[2] = (byte) (length & 0xff);
        digest.update(byteArray, 0, byteArray.length);
        byte[] innerHash = new byte[digest.getDigestLength()];
        digest.update(buf, offset, length);
        try {
            digest.digest(innerHash, 0, innerHash.length);
        } catch (DigestException e) {
            // Ignore this exception, it should never happen
        }
            
        // Now, the outer hash
        digest.update(macSecret, 0, macSecret.length);
        digest.update(PAD2, 0, padLength);
        byte[] mac = new byte[innerHash.length];
        digest.update(innerHash, 0, innerHash.length);
        try {
            digest.digest(mac, 0, mac.length);
        } catch (DigestException e) {
            // Ignore this exception, it should never happen
        }
            
            return mac;
    }
        
    /**
     * Increments write sequence number
     * @exception IOException if the sequence numbers rolls around
     */ 
    void incrementSequenceNumber() throws IOException {
        if (++sequenceNumber == (long) 0) 
            throw new IOException("Sequence number rolled over");
    }
}    

/**
 * Implements record's encoding
 */
class RecordEncoder extends MAC {
    /** Cipher used for encryption */
    private Cipher cipher;

    /**
     * Constructs RecordEncoder object
     * 
     * @param dgst digest for MAC computation
     * @param secret MAC secret
     * @param padLen padding length
     * @param cphr cipher used for encoding
     */
    RecordEncoder(MessageDigest dgst, byte[] secret, int padLen, Cipher cphr) {
        macSecret = secret;
        digest = dgst;
        digestLength = digest.getDigestLength();
        padLength = padLen;
        cipher = cphr;
    }

    /**
     * Converts a byte array containing an SSLPlaintext structure
     * to the corresponding SSLCiphertext structure. The process 
     * typically involves the addition of a MAC followed by 
     * encryption.
     * 
     * @param plainText byte array containing SSLPlaintext
     * @return the number of bytes written to the OutputStream
     *
     * @exception IOException if a problem is encountered during
     * encryption
     */ 
    byte[] encode(byte[] plainText) throws IOException {
        /*
         * Since we only support NULL compression, SSLPlaintext
         * the same as SSLCompressed.
         */ 
        byte[] fragAndMAC = null; // fragment plus MAC
        
        if (digest != null) {
            fragAndMAC = new byte[plainText.length - 5 + digestLength];
            System.arraycopy(plainText, 5, fragAndMAC, 
                    0, plainText.length - 5);
            byte[] mac = getMAC(plainText[0], 
                    plainText, 5, plainText.length - 5);
            System.arraycopy(mac, 0, fragAndMAC, 
                    (plainText.length - 5), digestLength);
            } else {
                fragAndMAC = new byte[plainText.length - 5];
                System.arraycopy(plainText, 5, fragAndMAC, 
                        0, plainText.length - 5);
        }
        
        // ... now we need to encrypt fragAndMAC and possibly update IVs
        byte[] efragAndMAC = null; 
        if (cipher != null) {
            try {
                /*
                * NOTE: For now, we always have a stream cipher so this
                * is commented out.
                */ 
                // We have a stream cipher
                efragAndMAC = fragAndMAC;
                cipher.update(fragAndMAC, 0, 
                        fragAndMAC.length, efragAndMAC, 0);
            } catch (Exception e) {
                throw new IOException("Encode caught " + e);
            }
        } else {
            // Cipher algorithm is NULL
            efragAndMAC = fragAndMAC;
        }
        
    	if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
	        Logging.report(Logging.INFORMATION, LogChannels.LC_SECURITY,
			   "efragAndMAC: " + Utils.hexEncode(efragAndMAC));
	    }
        
        byte[] record = new byte[efragAndMAC.length + 5];
        System.arraycopy(plainText, 0, record, 0, 3);
        record[3] = (byte) (efragAndMAC.length >>> 8);
        record[4] = (byte) (efragAndMAC.length & 0xff);
        System.arraycopy(efragAndMAC, 0, record, 5, efragAndMAC.length);
        
        // We have encoded one more record, increment seq number
        incrementSequenceNumber();
        
        return record;
    }
}

/**
 * Implements record's decoding
 */
class RecordDecoder extends MAC {
    /** Cipher used for decryption */
    private Cipher cipher;

    /**
     * Constructs RecordDecoder object
     * 
     * @param dgst digest for MAC computation
     * @param secret MAC secret
     * @param padLen padding length
     * @param cphr cipher used for decoding
     */
    RecordDecoder(MessageDigest dgst, byte[] secret, int padLen, Cipher cphr) {
        macSecret = secret;
        digest = dgst;
        digestLength = digest.getDigestLength();
        padLength = padLen;
        cipher = cphr;
    }    
    

    /**
     * Converts a byte array containing an SSLCiphertext structure
     * to the corresponding SSLPlaintext structure. The process
     * typically involves decryption followed by MAC verification
     * and MAC stripping.
     * @param recordHeader record header
     * @param recordData record data
     * @return Length of the decrypted data in the input buffer.
     * 
     * @exception IOException if a problem is encountered during decryption
     *                        or MAC verification
     */ 
    int decode(byte[] recordHeader, byte[] recordData) 
               throws IOException {
        if (cipher != null) {
            // Cipher algorithm is not NULL (ctxt needs to be decrypted)

            try {
                /*
                 * NOTE: For now, we only have ARCFOUR, a stream cipher
                 * so there is no IV or padding that a block cipher has.
                 *
                 * Otherwise we would have to find the plaintext offset
                 * after we decrypt.
                 */

                // We have a stream cipher (NOTE: assuming CLIENT role)

                // We can decode in place w/o using additional memory
                cipher.update(recordData, 0, recordData.length, recordData, 0);
            } catch (Exception e) {
                throw new IOException("Decode caught " + e);
            }
        }

        int length = recordData.length - digestLength;
        if (digest != null) {
            byte[] expMAC = null;  // expected MAC
            expMAC = getMAC(recordHeader[0], recordData, 0, length);
            if (!Utils.byteMatch(expMAC, 0, recordData, length, 
                        digestLength)) {
                throw new IOException("Bad MAC");
            }
        }
        
        incrementSequenceNumber();

        return length;
    }
}

