/**
 * TLSSecurityParameters
 * 
 * This class encapsulates all the security parameters that get negotiated
 * during the TLS handshake. It also holds all the key derivation methods.
 * Copyright (c) 2007 Henri Torgemane
 * 
 * Patched by Bobby Parker (sh0rtwave@gmail.com)
 * 
 * See LICENSE.txt for full license information.
 */
package com.hurlant.crypto.tls {
    import com.hurlant.crypto.hash.MD5;
    import com.hurlant.crypto.hash.SHA1;
    import com.hurlant.crypto.prng.TLSPRF;
    import com.hurlant.util.Hex;
    
    import flash.utils.ByteArray;
    import com.hurlant.crypto.rsa.RSAKey;
    
    public class TLSSecurityParameters implements ISecurityParameters {
        
        // COMPRESSION
        public static const COMPRESSION_NULL:uint = 0;
        
        // This is probably not smart. Revise this to use all settings from TLSConfig, since this shouldn't really know about
        // user settings, those are best handled from the engine at a session level.
        public static var IGNORE_CN_MISMATCH:Boolean = true;
        public static var ENABLE_USER_CLIENT_CERTIFICATE:Boolean = false;
        public static var USER_CERTIFICATE:String;
        
        
        private var cert:ByteArray; // Local Cert
        private var key:RSAKey; // local key
        private var entity:uint; // SERVER | CLIENT
        private var bulkCipher:uint; // BULK_CIPHER_*
        private var cipherType:uint; // STREAM_CIPHER | BLOCK_CIPHER
        private var keySize:uint;
        private var keyMaterialLength:uint;
        private var IVSize:uint;
        private var macAlgorithm:uint; // MAC_*
        private var hashSize:uint;
        private var compression:uint; // COMPRESSION_NULL
        private var masterSecret:ByteArray; // 48 bytes
        private var clientRandom:ByteArray; // 32 bytes
        private var serverRandom:ByteArray; // 32 bytes
        private var ignoreCNMismatch:Boolean = true;
        private var trustAllCerts:Boolean = false;
        private var trustSelfSigned:Boolean = false;
        public static const PROTOCOL_VERSION:uint = 0x0301; 
        private var tlsDebug:Boolean = false;

        
        // not strictly speaking part of this, but yeah.
        public var keyExchange:uint;
        public function TLSSecurityParameters(entity:uint, localCert:ByteArray = null, localKey:RSAKey = null) {
            this.entity = entity;
            reset();
            key = localKey;
            cert = localCert;
        }
        
        public function get version() : uint {
            return PROTOCOL_VERSION;
        }
        
        public function reset():void {
            bulkCipher = BulkCiphers.NULL;
            cipherType = BulkCiphers.BLOCK_CIPHER;
            macAlgorithm = MACs.NULL;
            compression = COMPRESSION_NULL;
            masterSecret = null;
        }
        
        public function getBulkCipher():uint {
            return bulkCipher;
        }
        public function getCipherType():uint {
            return cipherType;
        }
        public function getMacAlgorithm():uint {
            return macAlgorithm;
        }
        
        public function setCipher(cipher:uint):void {
            bulkCipher = CipherSuites.getBulkCipher(cipher);
            cipherType = BulkCiphers.getType(bulkCipher);
            keySize = BulkCiphers.getExpandedKeyBytes(bulkCipher);   // 8
            keyMaterialLength = BulkCiphers.getKeyBytes(bulkCipher); // 5
            IVSize = BulkCiphers.getIVSize(bulkCipher);
            
            keyExchange = CipherSuites.getKeyExchange(cipher);
            
            macAlgorithm = CipherSuites.getMac(cipher);
            hashSize = MACs.getHashSize(macAlgorithm);
        }
        public function setCompression(algo:uint):void {
            compression = algo;
        }
        public function setPreMasterSecret(secret:ByteArray):void {
            // compute master_secret
            var seed:ByteArray = new ByteArray;
            seed.writeBytes(clientRandom, 0, clientRandom.length);
            seed.writeBytes(serverRandom, 0, serverRandom.length);
            var prf:TLSPRF = new TLSPRF(secret, "master secret", seed);
            masterSecret = new ByteArray;
            prf.nextBytes(masterSecret, 48);
            if (tlsDebug)
                trace("Master Secret: " + Hex.fromArray( masterSecret, true ));
        }
        public function setClientRandom(secret:ByteArray):void {
            clientRandom = secret;
        }
        public function setServerRandom(secret:ByteArray):void { 
            serverRandom = secret;
        }
        
        public function get useRSA():Boolean {
            return KeyExchanges.useRSA(keyExchange);
        }
        
        public function computeVerifyData(side:uint, handshakeMessages:ByteArray):ByteArray {
            var seed:ByteArray = new ByteArray;
            var md5:MD5 = new MD5;
            if (tlsDebug)
                trace("Handshake value: " + Hex.fromArray(handshakeMessages, true ));
            seed.writeBytes(md5.hash(handshakeMessages),0,md5.getHashSize());
            var sha:SHA1 = new SHA1;
            seed.writeBytes(sha.hash(handshakeMessages),0,sha.getHashSize());
            if (tlsDebug)
                trace("Seed in: " + Hex.fromArray(seed, true ));
            var prf:TLSPRF = new TLSPRF(masterSecret, (side==TLSEngine.CLIENT) ? "client finished" : "server finished", seed);
            var out:ByteArray = new ByteArray;
            prf.nextBytes(out, 12);
            if (tlsDebug)
                trace("Finished out: " + Hex.fromArray(out, true ));
            out.position = 0;
            return out;
        }
        
        // client side certficate check - This is probably incorrect somehow
        public function computeCertificateVerify( side:uint, handshakeMessages:ByteArray ):ByteArray {
            var seed:ByteArray = new ByteArray;
            var md5:MD5 = new MD5;
            seed.writeBytes(md5.hash(handshakeMessages),0,md5.getHashSize());
            var sha:SHA1 = new SHA1;
            seed.writeBytes(sha.hash(handshakeMessages),0,sha.getHashSize());
            
            // Now that I have my hashes of existing handshake messages (which I'm not sure about the length of yet) then 
            // Sign that with my private key
            seed.position = 0;
            var out:ByteArray = new ByteArray();
            key.sign( seed, out, seed.bytesAvailable);
            out.position = 0;
            return out;    
        }
        
        public function getConnectionStates():Object {
            if (masterSecret != null) {
                var seed:ByteArray = new ByteArray;
                seed.writeBytes(serverRandom, 0, serverRandom.length);
                seed.writeBytes(clientRandom, 0, clientRandom.length);
                var prf:TLSPRF = new TLSPRF(masterSecret, "key expansion", seed);
                
                var client_write_MAC:ByteArray = new ByteArray;
                prf.nextBytes(client_write_MAC, hashSize);
                var server_write_MAC:ByteArray = new ByteArray;
                prf.nextBytes(server_write_MAC, hashSize);
                var client_write_key:ByteArray = new ByteArray;
                prf.nextBytes(client_write_key, keyMaterialLength);
                var server_write_key:ByteArray = new ByteArray;
                prf.nextBytes(server_write_key, keyMaterialLength);
                var client_write_IV:ByteArray = new ByteArray;
                prf.nextBytes(client_write_IV, IVSize);
                var server_write_IV:ByteArray = new ByteArray;
                prf.nextBytes(server_write_IV, IVSize);

                var client_write:TLSConnectionState = new TLSConnectionState(
                        bulkCipher, cipherType, macAlgorithm,
                        client_write_MAC, client_write_key, client_write_IV);
                var server_write:TLSConnectionState = new TLSConnectionState(
                        bulkCipher, cipherType, macAlgorithm,
                        server_write_MAC, server_write_key, server_write_IV);
                
                if (entity == TLSEngine.CLIENT) {
                    return {read:server_write, write:client_write};
                } else {
                    return {read:client_write, write:server_write};
                }

            } else {
                return {read:new TLSConnectionState, write:new TLSConnectionState};
            }
        }
        
    }
}