/** * TLSEngine * * A TLS protocol implementation. * See comment below for some details. * Copyright (c) 2007 Henri Torgemane * * Patched(heavily) by Bobby Parker (shortwave@gmail.com) * * See LICENSE.txt for full license information. */ package com.hurlant.crypto.tls { import com.hurlant.crypto.cert.X509Certificate; import com.hurlant.crypto.cert.X509CertificateCollection; import com.hurlant.crypto.prng.Random; import com.hurlant.util.ArrayUtil; import com.hurlant.util.Hex; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.ProgressEvent; import flash.utils.ByteArray; import flash.utils.IDataInput; import flash.utils.IDataOutput; import flash.utils.clearTimeout; import flash.utils.setTimeout; import com.hurlant.crypto.prng.ARC4; [Event(name="close", type="flash.events.Event")] [Event(name="socketData", type="flash.events.ProgressEvent")] [Event(name="ready", type="com.hurlant.crypto.tls.TLSEvent")] [Event(name="data", type="com.hurlant.crypto.tls.TLSEvent")] /** * The heart of the TLS protocol. * This class can work in server or client mode. * * This doesn't fully implement the TLS protocol. * * Things missing that I'd like to add: * - support for client-side certificates * - general code clean-up to make sure we don't have gaping securite holes * * Things that aren't there that I won't add: * - support for "export" cypher suites (deprecated in later TLS versions) * - support for "anon" cypher suites (deprecated in later TLS versions) * * Things that I'm unsure about adding later: * - compression. Compressing encrypted streams is barely worth the CPU cycles. * - diffie-hellman based key exchange mechanisms. Nifty, but would we miss it? * * @author henri * */ public class TLSEngine extends EventDispatcher { public static const SERVER:uint = 0; public static const CLIENT:uint = 1; public var protocol_version:uint; private static const PROTOCOL_HANDSHAKE:uint = 22; private static const PROTOCOL_ALERT:uint = 21; private static const PROTOCOL_CHANGE_CIPHER_SPEC:uint = 20; private static const PROTOCOL_APPLICATION_DATA:uint = 23; private static const STATE_NEW:uint = 0; // brand new. nothing happened yet private static const STATE_NEGOTIATING:uint = 1; // we're figuring out what to use private static const STATE_READY:uint = 2; // we're ready for AppData stuff to go over us. private static const STATE_CLOSED:uint = 3; // we're done done. private var _entity:uint; // SERVER | CLIENT private var _config:TLSConfig; private var _state:uint; private var _securityParameters:ISecurityParameters; private var _currentReadState:IConnectionState; private var _currentWriteState:IConnectionState; private var _pendingReadState:IConnectionState; private var _pendingWriteState:IConnectionState; private var _handshakePayloads:ByteArray; private var _handshakeRecords:ByteArray; // For client-side certificate verify private var _iStream:IDataInput; private var _oStream:IDataOutput; // temporary store for X509 certs received by this engine. private var _store:X509CertificateCollection; // the main certificate received from the other side. private var _otherCertificate:X509Certificate; public function get peerCertificate() : X509Certificate { return _otherCertificate; } // If this isn't null, we expect this identity to be found in the Cert's Subject CN. private var _otherIdentity:String; // The client-side cert private var _myCertficate:X509Certificate; // My Identity private var _myIdentity:String; /** * * @param config A TLSConfig instance describing how we're supposed to work * @param iStream An input stream to read TLS data from * @param oStream An output stream to write TLS data to * @param otherIdentity An optional identifier. If set, this will be checked against the Subject CN of the other side's certificate. * */ function TLSEngine(config:TLSConfig, iStream:IDataInput, oStream:IDataOutput, otherIdentity:String = null) { _entity = config.entity; _config = config; _iStream = iStream; _oStream = oStream; _otherIdentity = otherIdentity; _state = STATE_NEW; // Pick the right set of callbacks _entityHandshakeHandlers = _entity == CLIENT ? handshakeHandlersClient : handshakeHandlersServer; // setting up new security parameters needs to be controlled by...something. if (_config.version == SSLSecurityParameters.PROTOCOL_VERSION) { _securityParameters = new SSLSecurityParameters(_entity); } else { _securityParameters = new TLSSecurityParameters(_entity, _config.certificate, _config.privateKey); } protocol_version = _config.version; // So this...why is it here, other than to preclude a possible null pointer situation? var states:Object = _securityParameters.getConnectionStates(); _currentReadState = states.read; _currentWriteState = states.write; _handshakePayloads = new ByteArray; _store = new X509CertificateCollection; } /** * This starts the TLS negotiation for a TLS Client. * * This is a no-op for a TLS Server. * */ public function start():void { if (_entity == CLIENT) { try { startHandshake(); } catch (e:TLSError) { handleTLSError(e); } } } public function dataAvailable(e:* = null):void { if (_state == STATE_CLOSED) return; // ignore try { parseRecord(_iStream); } catch (e:TLSError) { handleTLSError(e); } } public function close(e:TLSError = null):void { if (_state == STATE_CLOSED) return; // ignore // ok. send an Alert to let the peer know var rec:ByteArray = new ByteArray; if (e==null && _state != STATE_READY) { // use canceled while handshaking. be nice about it rec[0] = 1; rec[1] = TLSError.user_canceled; sendRecord(PROTOCOL_ALERT, rec); } rec[0] = 2; if (e == null) { rec[1] = TLSError.close_notify; } else { rec[1] = e.errorID; trace("TLSEngine shutdown triggered by "+e); } sendRecord(PROTOCOL_ALERT, rec); _state = STATE_CLOSED; dispatchEvent(new Event(Event.CLOSE)); } private var _packetQueue:Array = []; private function parseRecord(stream:IDataInput):void { var p:ByteArray; while(_state!=STATE_CLOSED && stream.bytesAvailable>4) { if (_packetQueue.length>0) { var packet:Object = _packetQueue.shift(); p = packet.data; if (stream.bytesAvailable+p.length>=packet.length) { // we have a whole packet. put together. stream.readBytes(p, p.length, packet.length-p.length); parseOneRecord(packet.type, packet.length, p); // do another loop to parse any leftover record continue; } else { // not enough. grab the data and park it. stream.readBytes(p, p.length, stream.bytesAvailable); _packetQueue.push(packet); continue; } } var type:uint = stream.readByte(); var ver:uint = stream.readShort(); var length:uint = stream.readShort(); if (length>16384+2048) { // support compression and encryption overhead. throw new TLSError("Excessive TLS Record length: "+length, TLSError.record_overflow); } // Can pretty much assume that if I'm here, I've got a default config, so let's use it. if (ver != _securityParameters.version ) { throw new TLSError("Unsupported TLS version: "+ver.toString(16), TLSError.protocol_version); } p = new ByteArray; var actualLength:uint = Math.min(stream.bytesAvailable, length); stream.readBytes(p, 0, actualLength); if (actualLength == length) { parseOneRecord(type, length, p); } else { _packetQueue.push({type:type, length:length, data:p}); } } } // Protocol handler map, provides a mapping of protocol types to individual packet handlers private var protocolHandlers:Object = { 23 : parseApplicationData, // PROTOCOL_APPLICATION_DATA 22 : parseHandshake, // PROTOCOL_HANDSHAKE 21 : parseAlert, // PROTOCOL_ALERT 20 : parseChangeCipherSpec }; // PROTOCOL_CHANGE_CIPHER_SPEC /** * Modified to support the notion of a handler map(see above ), since it makes for better clarity (IMHO of course). */ private function parseOneRecord(type:uint, length:uint, p:ByteArray):void { p = _currentReadState.decrypt(type, length, p); if (p.length>16384) { throw new TLSError("Excessive Decrypted TLS Record length: "+p.length, TLSError.record_overflow); } if (protocolHandlers.hasOwnProperty( type )) { while( p != null) p = protocolHandlers[ type ]( p ); } else { throw new TLSError("Unsupported TLS Record Content Type: "+type.toString( 16 ), TLSError.unexpected_message); } } ///////// handshake handling // session identifier // peer certificate // compression method // cipher spec // master secret // is resumable private static const HANDSHAKE_HELLO_REQUEST:uint = 0; private static const HANDSHAKE_CLIENT_HELLO:uint = 1; private static const HANDSHAKE_SERVER_HELLO:uint = 2; private static const HANDSHAKE_CERTIFICATE:uint = 11; private static const HANDSHAKE_SERVER_KEY_EXCHANGE:uint = 12; private static const HANDSHAKE_CERTIFICATE_REQUEST:uint = 13; private static const HANDSHAKE_HELLO_DONE:uint = 14; private static const HANDSHAKE_CERTIFICATE_VERIFY:uint = 15; private static const HANDSHAKE_CLIENT_KEY_EXCHANGE:uint = 16; private static const HANDSHAKE_FINISHED:uint = 20; // Server handshake handler map private var handshakeHandlersServer:Object = { 0 : notifyStateError, // HANDSHAKE_HELLO_REQUEST 1 : parseHandshakeClientHello, // HANDSHAKE_CLIENT_HELLO 2 : notifyStateError, // HANDSHAKE_SERVER_HELLO 11 : loadCertificates, // HANDSHAKE_CERTIFICATE 12 : notifyStateError, // HANDSHAKE_SERVER_KEY_EXCHANGE 13 : notifyStateError, // HANDSHAKE_CERTIFICATE_REQUEST 14 : notifyStateError, // HANDSHAKE_HELLO_DONE 15 : notifyStateError, // HANDSHAKE_CERTIFICATE_VERIFY 16 : parseHandshakeClientKeyExchange, // HANDSHAKE_CLIENT_KEY_EXCHANGE 20 : verifyHandshake // HANDSHAKE_FINISHED }; // Client handshake handler map private var handshakeHandlersClient:Object = { 0 : parseHandshakeHello, // HANDSHAKE_HELLO_REQUEST 1 : notifyStateError, // HANDSHAKE_CLIENT_HELLO 2 : parseHandshakeServerHello, // HANDSHAKE_SERVER_HELLO 11 : loadCertificates, // HANDSHAKE_CERTIFICATE 12 : parseServerKeyExchange, // HANDSHAKE_SERVER_KEY_EXCHANGE 13 : setStateRespondWithCertificate, // HANDSHAKE_CERTIFICATE 14 : sendClientAck, // HANDSHAKE_HELLO_DONE 15 : notifyStateError, // HANDSHAKE_CERTIFICATE_VERIFY 16 : notifyStateError, // HANDSHAKE_CLIENT_KEY_EXCHANGE 20 : verifyHandshake // HANDSHAKE_FINISHED }; private var _entityHandshakeHandlers:Object; private var _handshakeCanContinue:Boolean = true; // For handling cases where I might need to pause processing during a handshake (cert issues, etc.). private var _handshakeQueue:Array = []; /** * The handshake is always started by the client. */ private function startHandshake():void { _state = STATE_NEGOTIATING; // reset some other handshake state. XXX sendClientHello(); } /** * Handle the incoming handshake packet. * */ private function parseHandshake(p:ByteArray):ByteArray { if (p.length<4) { trace("Handshake packet is way too short. bailing."); return null; } p.position = 0; var rec:ByteArray = p; var type:uint = rec.readUnsignedByte(); var tmp:uint = rec.readUnsignedByte(); var length:uint = (tmp<<16) | rec.readUnsignedShort(); if (length+4>p.length) { // partial read. trace("Handshake packet is incomplete. bailing."); return null; } // we need to copy the record, to have a valid FINISHED exchange. if (type!=HANDSHAKE_FINISHED) { _handshakePayloads.writeBytes(p, 0, length+4); } // Surf the handler map and find the right handler for this handshake packet type. // I modified the individual handlers so they encapsulate all possible knowledge // about the incoming packet type, so no previous handling or massaging of the data // is required, as was the case using the switch statement. BP if (_entityHandshakeHandlers.hasOwnProperty( type )) { if (_entityHandshakeHandlers[ type ] is Function) _entityHandshakeHandlers[ type ]( rec ); } else { throw new TLSError( "Unimplemented or unknown handshake type!", TLSError.internal_error ); } // Get set up for the next packet. if (length+4