Source: ptolemy/actor/lib/jjs/modules/iotAuth/iotAuth.js

// Copyright (c) 2016-2017 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//
//

/**
 * Module supporting authorization (Auth) service for the Internet of Things (IoT).
 * This module includes common functions for communication with IoT Auth service
 * and communication with other registered entities.
 *
 * The authorization service is provided by a local authorization entity, Auth,
 * whose open-source Java implementation can be found on a GitHub repository
 * (https://github.com/iotauth/iotauth).
 *
 * @module iotAuth
 * @author Hokeun Kim
 * @version $$Id$$
 */

// Stop extra messages from jslint.  Note that there should be no
// space between the / and the * and global.
/*globals console, error, exports, require */
/*jshint globalstrict: true*/
"use strict";

var buffer = require('buffer');
var crypto = require('crypto');
var socket = require('socket');

// Message types
var msgType = {
    AUTH_HELLO: 0,
    AUTH_SESSION_KEY_REQ: 10,
    AUTH_SESSION_KEY_RESP: 11,
    SESSION_KEY_REQ_IN_PUB_ENC: 20,
    /** Includes distribution message (session keys) */
    SESSION_KEY_RESP_WITH_DIST_KEY: 21,
    /** Distribution message */
    SESSION_KEY_REQ: 22,
    /** Distribution message */
    SESSION_KEY_RESP: 23,
    SKEY_HANDSHAKE_1: 30,
    SKEY_HANDSHAKE_2: 31,
    SKEY_HANDSHAKE_3: 32,
    SECURE_COMM_MSG: 33,
    FIN_SECURE_COMM: 34,
    SECURE_PUB: 40,
    AUTH_ALERT: 100
};

var authAlertCode = {
    INVALID_DISTRIBUTION_KEY: 0,
    INVALID_SESSION_KEY_REQ_TARGET: 1
};

exports.msgType = msgType;

var AUTH_NONCE_SIZE = 8; // used in parseAuthHello
var SESSION_KEY_ID_SIZE = 8;
var DIST_KEY_EXPIRATION_TIME_SIZE = 6;
var SESSION_KEY_EXPIRATION_TIME_SIZE = 6;
var REL_VALIDITY_SIZE = 6;
var DIST_CIPHER_KEY_SIZE = 16; // 256 bit key = 32 bytes
var SESSION_CIPHER_KEY_SIZE = 16; // 128 bit key = 16 bytes
var AUTH_ALERT_CODE_SIZE = 1;


var HANDSHAKE_NONCE_SIZE = 8; // handshake nonce size
var SEQ_NUM_SIZE = 8;

exports.SESSION_KEY_ID_SIZE = SESSION_KEY_ID_SIZE;
exports.HANDSHAKE_NONCE_SIZE = HANDSHAKE_NONCE_SIZE;

function numToVarLenInt(num) {
    var extraBuf;
    var buf = new buffer.Buffer(0);
    while (num > 127) {
        extraBuf = new buffer.Buffer(1);
        extraBuf.writeUInt8(128 | num & 127);
        buf = buffer.concat([buf, extraBuf]);
        num >>= 7;
    }
    extraBuf = new buffer.Buffer(1);
    extraBuf.writeUInt8(num);
    buf = buffer.concat([buf, extraBuf]);
    return buf;
}

function varLenIntToNum(buf, offset) {
    var num = 0;
    for (var i = 0; i < buf.length && i < 5; i++) {
        num |= (buf.get(offset + i) & 127) << (7 * i);
        if ((buf.get(offset + i) & 128) === 0) {
            return {
                num: num,
                bufLen: i + 1
            };
            //break;
        }
    }
    return null;
}

function serializeStringParam(stringParam) {
    var result;
    if (stringParam === null) {
        result = numToVarLenInt(0);
    } else {
        var strLenBuf = numToVarLenInt(stringParam.length);
        var strBuf = new buffer.Buffer(stringParam);
        result = buffer.concat([strLenBuf, strBuf]);
    }
    return result;
}

function parseStringParam(buf, offset) {
    var ret = varLenIntToNum(buf, offset);
    if (ret.bufLen === 0) {
        return {
            len: 1,
            str: null
        };
    }
    var strLen = ret.num;
    var str = buf.toString(offset + ret.bufLen, offset + ret.bufLen + strLen);
    return {
        len: ret.bufLen + strLen,
        str: str
    };
}

/*
    IoTSP (IoT Secure Protocol) Message
    {
        msgType: /UInt8/,
        payloadLen: /variable-length integer encoding/
        payload: /Buffer/
    }
*/
exports.serializeIoTSP = function (obj) {
    if (typeof obj.msgType === 'undefined' || typeof obj.payload === 'undefined') {
        console.log('Error: IoTSP msgType or payload is missing.');
        return;
    }
    var msgTypeBuf = new buffer.Buffer(1);
    msgTypeBuf.writeUInt8(obj.msgType, 0);
    var payLoadLenBuf = numToVarLenInt(obj.payload.length);
    return buffer.concat([msgTypeBuf, payLoadLenBuf, obj.payload]);
};

exports.parseIoTSP = function (buf) {
    var msgTypeVal = buf.readUInt8(0);
    var ret = varLenIntToNum(buf, 1);
    var payloadVal = buf.slice(1 + ret.bufLen);
    return {
        msgType: msgTypeVal,
        payloadLen: ret.num,
        payload: payloadVal
    };
};


///////////////////////////////////////////////////////////////////
////            Functions for Auth packet handling             ////

var parseAuthHello = function (buf) {
    var authId = buf.readUInt32BE(0);
    var nonce = buf.slice(4, 4 + AUTH_NONCE_SIZE);
    return {
        authId: authId,
        nonce: nonce
    };
};

var serializeSessionKeyReq = function (obj) {
    if (typeof obj.nonce === 'undefined' || typeof obj.replyNonce === 'undefined' ||
        typeof obj.sender === 'undefined' || typeof obj.purpose === 'undefined' ||
        typeof obj.numKeysPerRequest === 'undefined') {
        console.log('Error: SessionKeyReq nonce or replyNonce ' +
            'or purpose or numKeysPerRequest is missing.');
        return;
    }
    var buf = new buffer.Buffer(AUTH_NONCE_SIZE * 2 + 4);
    obj.nonce.copy(buf, 0);
    obj.replyNonce.copy(buf, AUTH_NONCE_SIZE);
    buf.writeUInt32BE(obj.numKeysPerRequest, AUTH_NONCE_SIZE * 2);

    var senderBuf = serializeStringParam(obj.sender);
    var purposeBuf = serializeStringParam(JSON.stringify(obj.purpose));
    return buffer.concat([buf, senderBuf, purposeBuf]);
};

var serializeSessionKeyReqWithDistributionKey = function (senderName,
    encryptedSessionKeyReqBuf) {
    var senderBuf = new buffer.Buffer(senderName);
    var lengthBuf = new buffer.Buffer(1);
    lengthBuf.writeUInt8(senderBuf.length);
    return buffer.concat([lengthBuf, senderBuf, encryptedSessionKeyReqBuf]);
};

var parseDistributionKey = function (buf) {
    var absValidity = new Date(buf.readUIntBE(0, DIST_KEY_EXPIRATION_TIME_SIZE));
    var curIndex = DIST_KEY_EXPIRATION_TIME_SIZE;
    var cipherKeySize = buf.readUInt8(curIndex);
    curIndex += 1;
    var cipherKeyVal = buf.slice(curIndex, curIndex + cipherKeySize);
    curIndex += cipherKeySize;
    var macKeySize = buf.readUInt8(curIndex);
    curIndex += 1;
    var macKeyVal = buf.slice(curIndex, curIndex + macKeySize);
    return {
        cipherKeyVal: cipherKeyVal,
        macKeyVal: macKeyVal,
        absValidity: absValidity
    };
};

var parseSessionKey = function (buf) {
    var keyId = buf.readUIntBE(0, SESSION_KEY_ID_SIZE);
    var curIndex = SESSION_KEY_ID_SIZE;
    var absValidityValue = buf.readUIntBE(curIndex, SESSION_KEY_EXPIRATION_TIME_SIZE);
    curIndex += SESSION_KEY_EXPIRATION_TIME_SIZE;
    var absValidity = new Date(absValidityValue);
    var relValidity = buf.readUIntBE(curIndex, REL_VALIDITY_SIZE);
    curIndex += REL_VALIDITY_SIZE;
    var cipherKeySize = buf.readUInt8(curIndex);
    curIndex += 1;
    var cipherKeyVal = buf.slice(curIndex, curIndex + cipherKeySize);
    curIndex += cipherKeySize;
    var macKeySize = buf.readUInt8(curIndex);
    curIndex += 1;
    var macKeyVal = buf.slice(curIndex, curIndex + macKeySize);
    curIndex += macKeySize;
    var sessionKey = {
        id: keyId,
        cipherKeyVal: cipherKeyVal,
        macKeyVal: macKeyVal,
        absValidity: absValidity,
        relValidity: relValidity
    };
    return {
        sessionKey: sessionKey,
        totalLen: curIndex
    };
};

var parseSessionKeyResp = function (buf) {
    // Do not change the reading order from serialized buffer
    // Must follow the order written in buffer:
    // replyNonce, cryptoSpecStr, sessionKeyCount, session keys
    var curIndex = 0;
    var replyNonce = buf.slice(curIndex, AUTH_NONCE_SIZE);
    curIndex += AUTH_NONCE_SIZE;

    var ret = parseStringParam(buf, curIndex);
    curIndex += ret.len;
    var cryptoSpecStr = ret.str;

    var sessionKeyCount = buf.readUInt32BE(curIndex);
    curIndex += 4;

    var sessionKeyList = [];
    var i;
    for (i = 0; i < sessionKeyCount; i++) {
        ret = parseSessionKey(buf.slice(curIndex));
        var sessionKey = ret.sessionKey;
        sessionKeyList.push(sessionKey);
        curIndex += ret.totalLen;
    }
    return {
        replyNonce: replyNonce,
        sessionKeyList: sessionKeyList
    };
};

var parseAuthAlert = function (buf) {
    var code = buf.readUIntBE(0, AUTH_ALERT_CODE_SIZE);
    return {
        code: code
    };
};

///////////////////////////////////////////////////////////////////
////           Functions for Entity packet handling            ////
/*
    Handshake Format
    {
        nonce: /Buffer/, // encrypted, may be undefined
        replyNonce: /Buffer/, // encrypted, may be undefined
    }
*/
var serializeHandshake = function (obj) {
    if (typeof obj.nonce === 'undefined' && typeof obj.replyNonce === 'undefined') {
        console.log('Error: handshake should include at least on nonce.');
        return;
    }
    var buf = new buffer.Buffer(1 + HANDSHAKE_NONCE_SIZE * 2);

    // indicates existance of nonces
    var indicator = 0;
    if (obj.nonce !== undefined) {
        indicator += 1;
        obj.nonce.copy(buf, 1);
    }
    if (obj.replyNonce !== undefined) {
        indicator += 2;
        obj.replyNonce.copy(buf, 1 + HANDSHAKE_NONCE_SIZE);
    }
    buf.writeUInt8(indicator, 0);

    return buf;
};

// buf should be just the unencrypted part
var parseHandshake = function (buf) {
    var obj = {};
    var indicator = buf.readUInt8(0);
    if ((indicator & 1) !== 0) {
        // nonce exists
        obj.nonce = buf.slice(1, 1 + HANDSHAKE_NONCE_SIZE);
    }
    if ((indicator & 2) !== 0) {
        // replayNonce exists
        obj.replyNonce = buf.slice(1 + HANDSHAKE_NONCE_SIZE, 1 + HANDSHAKE_NONCE_SIZE * 2);
    }
    return obj;
};

/*
    SecureSessionMessage Format
    {
        SeqNum: /Buffer/, // UIntBE, SEQ_NUM_SIZE Bytes
        data: /Buffer/,
    }
*/
var serializeSessionMessage = function (obj) {
    if (typeof obj.seqNum === 'undefined' || typeof obj.data === 'undefined') {
        console.log('Error: Secure session message seqNum or data is missing.');
        return;
    }
    var seqNumBuf = new buffer.Buffer(SEQ_NUM_SIZE);
    seqNumBuf.writeUIntBE(obj.seqNum, 0, SEQ_NUM_SIZE);
    return buffer.concat([seqNumBuf, obj.data]);
};
var parseSessionMessage = function (buf) {
    var seqNum = buf.readUIntBE(0, SEQ_NUM_SIZE);
    var data = buf.slice(SEQ_NUM_SIZE);
    return {
        seqNum: seqNum,
        data: data
    };
};

///////////////////////////////////////////////////////////////////
////            Wrapper functions for crypto module            ////

exports.loadPublicKey = function (path) {
    return crypto.loadPublicKey(path);
};

exports.loadPrivateKey = function (path) {
    return crypto.loadPrivateKey(path);
};

function symmetricEncryptAuthenticate(buf, symmetricKey, cryptoSpec) {
    cryptoSpec = crypto.parseSymmetricCryptoSpec(cryptoSpec);
    return crypto.symmetricEncryptWithHash(buf.getArray(),
        symmetricKey.cipherKeyVal.getArray(), symmetricKey.macKeyVal.getArray(),
        cryptoSpec.cipher, cryptoSpec.mac);
}

function symmetricDecryptVerify(buf, symmetricKey, cryptoSpec) {
    cryptoSpec = crypto.parseSymmetricCryptoSpec(cryptoSpec);
    return crypto.symmetricDecryptWithHash(buf.getArray(),
        symmetricKey.cipherKeyVal.getArray(), symmetricKey.macKeyVal.getArray(),
        cryptoSpec.cipher, cryptoSpec.mac);

}

///////////////////////////////////////////////////////////////////
////           Functions for accessing Auth service            ////

/*
options = {
    authHost,
    authPort,
    entityName,
    numKeysPerRequest,
    purpose,
    distributionKey = {val, absValidity},
    distributionCryptoSpec,
    publicKeyCryptoSpec,
    authPublicKey,
    entityPrivateKey
}
*/
// Helper function for common code in handing session key response
function processSessionKeyResponse(options, sessionKeyRespBuf, distributionKey, myNonce) {
    var ret = symmetricDecryptVerify(sessionKeyRespBuf, distributionKey, options.distributionCryptoSpec);
    if (!ret.hashOk) {
        return {
            error: 'Received hash for session key resp is NOT ok'
        };
    }
    console.log('Received hash for session key resp is ok');
    sessionKeyRespBuf = new buffer.Buffer(ret.data);
    var sessionKeyResp = parseSessionKeyResp(sessionKeyRespBuf);
    if (!sessionKeyResp.replyNonce.equals(myNonce)) {
        return {
            error: 'Auth nonce NOT verified'
        };
    }
    console.log('Auth nonce verified');
    return {
        sessionKeyList: sessionKeyResp.sessionKeyList
    };
}

function handleSessionKeyResponse(options, obj, myNonce, sessionKeyResponseCallback, callbackParameters) {
    var ret;
    if (obj.msgType == msgType.SESSION_KEY_RESP_WITH_DIST_KEY) {
        console.log('received session key response with distribution key attached!');
        var distKeyBuf = obj.payload.slice(0, 512);
        var sessionKeyRespBuf = obj.payload.slice(512);
        var pubEncData = distKeyBuf.slice(0, 256).getArray();
        var signature = distKeyBuf.slice(256).getArray();
        var verified = crypto.verifySignature(pubEncData, signature, options.authPublicKey, options.publicKeyCryptoSpec.sign);
        if (!verified) {
            sessionKeyResponseCallback({
                error: 'Auth signature NOT verified'
            });
            return;
        }
        console.log('Auth signature verified');
        distKeyBuf = new buffer.Buffer(
            crypto.privateDecrypt(pubEncData, options.entityPrivateKey, options.publicKeyCryptoSpec.cipher));
        var receivedDistKey = parseDistributionKey(distKeyBuf);
        ret = processSessionKeyResponse(options, sessionKeyRespBuf, receivedDistKey, myNonce);
        if (ret.error) {
            sessionKeyResponseCallback({
                error: ret.error
            });
            return;
        }
        sessionKeyResponseCallback({
            success: true
        }, receivedDistKey, ret.sessionKeyList, callbackParameters);
    } else if (obj.msgType == msgType.SESSION_KEY_RESP) {
        console.log('Received session key response encrypted with distribution key');
        ret = processSessionKeyResponse(options, obj.payload, options.distributionKey, myNonce);
        if (ret.error) {
            sessionKeyResponseCallback({
                error: ret.error
            });
            return;
        }
        sessionKeyResponseCallback({
            success: true
        }, null, ret.sessionKeyList, callbackParameters);
    } else if (obj.msgType == msgType.AUTH_ALERT) {
        console.log('Received Auth alert!');
        var authAlert = parseAuthAlert(obj.payload);
        if (authAlert.code == authAlertCode.INVALID_DISTRIBUTION_KEY) {
            sessionKeyResponseCallback({
                error: 'Received Auth alert due to invalid distribution key'
            });
            return;
        } else if (authAlert.code == authAlertCode.INVALID_SESSION_KEY_REQ_TARGET) {
            sessionKeyResponseCallback({
                error: 'Received Auth alert due to invalid session key request target'
            });
            return;
        } else {
            sessionKeyResponseCallback({
                error: 'Received Auth alert with unknown code :' + authAlert.code
            });
            return;
        }
    } else {
        sessionKeyResponseCallback({
            error: 'Unknown message type :' + obj.msgType
        });
        return;
    }
}

exports.sendSessionKeyRequest = function (options, sessionKeyResponseCallback, callbackParameters) {
    options.publicKeyCryptoSpec = crypto.parsePublicKeyCryptoSpec(options.publicKeyCryptoSpec);
    var authClientSocket = new socket.SocketClient(options.authPort, options.authHost, {
        //'connectTimeout' : this.getParameter('connectTimeout'),
        'discardMessagesBeforeOpen': false,
        'emitBatchDataAsAvailable': true,
        //'idleTimeout' : this.getParameter('idleTimeout'),
        'keepAlive': false,
        //'maxUnsentMessages' : this.getParameter('maxUnsentMessages'),
        //'noDelay' : this.getParameter('noDelay'),
        //'pfxKeyCertPassword' : this.getParameter('pfxKeyCertPassword'),
        //'pfxKeyCertPath' : this.getParameter('pfxKeyCertPath'),
        'rawBytes': true,
        //'receiveBufferSize' : this.getParameter('receiveBufferSize'),
        'receiveType': 'byte',
        //'reconnectAttempts' : this.getParameter('reconnectAttempts'),
        //'reconnectInterval' : this.getParameter('reconnectInterval'),
        //'sendBufferSize' : this.getParameter('sendBufferSize'),
        'sendType': 'byte',
        //'sslTls' : this.getParameter('sslTls'),
        //'trustAll' : this.getParameter('trustAll'),
        //'trustedCACertPath' : this.getParameter('trustedCACertPath')
    });
    authClientSocket.on('open', function () {
        console.log('connected to auth');
    });
    var myNonce;
    var expectingMoreData = false;
    var obj;
    authClientSocket.on('data', function (data) {
        console.log('data received from auth');
        var buf = new buffer.Buffer(data);
        if (!expectingMoreData) {
            obj = exports.parseIoTSP(buf);
            if (obj.payload.length < obj.payloadLen) {
                expectingMoreData = true;
                console.log('more data will come. current: ' + obj.payload.length +
                    ' expected: ' + obj.payloadLen);
            }
        } else {
            obj.payload = buffer.concat([obj.payload, new buffer.Buffer(data)]);
            if (obj.payload.length == obj.payloadLen) {
                expectingMoreData = false;
            } else {
                console.log('more data will come. current: ' + obj.payload.length +
                    ' expected: ' + obj.payloadLen);
            }
        }

        if (expectingMoreData) {
            // do not process the packet yet
            return;
        } else if (obj.msgType == msgType.AUTH_HELLO) {
            var authHello = parseAuthHello(obj.payload);
            myNonce = new buffer.Buffer(crypto.randomBytes(AUTH_NONCE_SIZE));

            var sessionKeyReq = {
                nonce: myNonce,
                replyNonce: authHello.nonce,
                numKeysPerRequest: options.numKeysPerRequest,
                sender: options.entityName,
                purpose: options.purpose
            };
            var reqMsgType;
            var reqPayload;
            var sessionKeyReqBuf;
            if (options.distributionKey === null || options.distributionKey.absValidity < new Date()) {
                if (options.distributionKey !== null) {
                    console.log('Distribution key expired, requesting new distribution key as well...');
                } else {
                    console.log('No distribution key yet, requesting new distribution key as well...');
                }
                reqMsgType = msgType.SESSION_KEY_REQ_IN_PUB_ENC;
                sessionKeyReqBuf = serializeSessionKeyReq(sessionKeyReq);
                reqPayload = new buffer.Buffer(
                    crypto.publicEncryptAndSign(sessionKeyReqBuf.getArray(),
                        options.authPublicKey, options.entityPrivateKey,
                        options.publicKeyCryptoSpec.cipher, options.publicKeyCryptoSpec.sign));
            } else {
                console.log('distribution key available! ');
                reqMsgType = msgType.SESSION_KEY_REQ;
                sessionKeyReqBuf = serializeSessionKeyReq(sessionKeyReq);
                var encryptedSessionKeyReqBuf = new buffer.Buffer(
                    symmetricEncryptAuthenticate(sessionKeyReqBuf, options.distributionKey, options.distributionCryptoSpec));
                reqPayload = serializeSessionKeyReqWithDistributionKey(options.entityName,
                    encryptedSessionKeyReqBuf);
            }
            var toSend = exports.serializeIoTSP({
                msgType: reqMsgType,
                payload: reqPayload
            }).getArray();
            authClientSocket.send(toSend);
        } else {
            console.log('Session key response arrived.');
            handleSessionKeyResponse(options, obj, myNonce, sessionKeyResponseCallback, callbackParameters);
            authClientSocket.close();
        }
    });
    authClientSocket.on('close', function () {
        console.log('disconnected from auth');
    });
    authClientSocket.on('error', function (message) {
        sessionKeyResponseCallback({
            error: 'an error occurred in socket during session key request, details: ' + message
        });
        authClientSocket.close();
    });
    authClientSocket.open();
};

///////////////////////////////////////////////////////////////////
////       List of available cryptography specifications       ////
exports.symmetricCryptoSpecs = ['AES-128-CBC:HmacSHA256',
    'AES-192-CBC:HmacSHA256',
    'AES-128-GCM'
];

exports.publicKeyCryptoSpecs = ['RSA/ECB/PKCS1PADDING:SHA256withRSA'];


///////////////////////////////////////////////////////////////////
////     Common socket class for entity server and client      ////

var IoTSecureSocket = function (socket, sessionKey, sessionCryptoSpec) {
    this.socket = socket;
    this.sessionKey = sessionKey;
    this.sessionCryptoSpec = sessionCryptoSpec;
    this.writeSeqNum = 0;
    this.readSeqNum = 0;
};

IoTSecureSocket.prototype.close = function () {
    if (this.socket) {
        this.socket.close();
    }
    this.socket = null;
};

IoTSecureSocket.prototype.checkSessionKeyValidity = function () {
    if (this.sessionKey.absValidity > new Date()) {
        return true;
    }
    return false;
};

// to be called from outside of iotAuth module
IoTSecureSocket.prototype.send = function (data) {
    if (!this.socket) {
        console.log('Internal socket is not available');
        return false;
    }
    if (!this.checkSessionKeyValidity()) {
        console.log('Session key expired!');
        return false;
    }
    var buf = serializeSessionMessage({
        seqNum: this.writeSeqNum,
        data: new buffer.Buffer(data)
    });
    var encBuf = new buffer.Buffer(symmetricEncryptAuthenticate(buf, this.sessionKey, this.sessionCryptoSpec));
    this.writeSeqNum++;
    var msg = {
        msgType: msgType.SECURE_COMM_MSG,
        payload: encBuf
    };
    var toSend = exports.serializeIoTSP(msg).getArray();
    this.socket.send(toSend);
    return true;
};

// to be called inside of iotAuth module
IoTSecureSocket.prototype.receive = function (payload) {
    if (!this.socket) {
        return {
            success: false,
            error: 'Internal socket is not available'
        };
    }
    if (!this.checkSessionKeyValidity()) {
        return {
            success: false,
            error: 'Session key expired!'
        };
    }
    var ret = symmetricDecryptVerify(payload, this.sessionKey, this.sessionCryptoSpec);
    if (!ret.hashOk) {
        return {
            success: false,
            error: 'Received hash for secure comm msg is NOT ok'
        };
    }
    console.log('Received hash for secure comm msg is ok');
    var buf = new buffer.Buffer(ret.data);
    ret = parseSessionMessage(buf);

    if (ret.seqNum != this.readSeqNum) {
        return {
            success: false,
            error: 'seqNum does not match! expected: ' + this.readSeqNum + ' received: ' + ret.seqNum
        };
    }
    this.readSeqNum++;
    console.log('Received seqNum: ' + ret.seqNum);
    return {
        success: true,
        data: ret.data
    };
};

IoTSecureSocket.prototype.inspect = function () {
    var ret = 'sessionKey: ' + this.sessionKey.toString() +
        ' writeSeqNum: ' + this.writeSeqNum +
        ' readSeqNum: ' + this.readSeqNum;
    return ret;
};

///////////////////////////////////////////////////////////////////
////                Functions for entity client                ////

/*
options = {
    serverHost,
    serverPort,
    sessionKey = {id, val, absValidity},
    sessionCryptoSpec
}
*/
/*
eventHandlers = {
    onClose,
    onError,
    onData,
    onConnection
}
*/
exports.initializeSecureCommunication = function (options, eventHandlers) {
    if (options.sessionKey === null) {
        eventHandlers.onError('Comm init failed: No available key');
        return;
    }
    // client communication state
    var entityClientCommState = {
        IDLE: 0,
        HANDSHAKE_1_SENT: 10,
        IN_COMM: 30 // Session message
    };
    var entityClientState = entityClientCommState.IDLE;
    var entityClientSocket = new socket.SocketClient(options.serverPort, options.serverHost, {
        //'connectTimeout' : this.getParameter('connectTimeout'),
        'discardMessagesBeforeOpen': false,
        'emitBatchDataAsAvailable': true,
        //'idleTimeout' : this.getParameter('idleTimeout'),
        //'keepAlive' : false,
        //'maxUnsentMessages' : this.getParameter('maxUnsentMessages'),
        //'noDelay' : this.getParameter('noDelay'),
        //'pfxKeyCertPassword' : this.getParameter('pfxKeyCertPassword'),
        //'pfxKeyCertPath' : this.getParameter('pfxKeyCertPath'),
        'rawBytes': true,
        //'receiveBufferSize' : this.getParameter('receiveBufferSize'),
        'receiveType': 'byte',
        //'reconnectAttempts' : this.getParameter('reconnectAttempts'),
        //'reconnectInterval' : this.getParameter('reconnectInterval'),
        //'sendBufferSize' : this.getParameter('sendBufferSize'),
        'sendType': 'byte',
        //'sslTls' : this.getParameter('sslTls'),
        //'trustAll' : this.getParameter('trustAll'),
        //'trustedCACertPath' : this.getParameter('trustedCACertPath')
    });

    var myNonce;
    entityClientSocket.on('open', function () {
        console.log('connected to server');
        myNonce = new buffer.Buffer(crypto.randomBytes(HANDSHAKE_NONCE_SIZE));
        var handshake1 = {
            nonce: myNonce
        };
        var buf = serializeHandshake(handshake1);
        var encBuf = new buffer.Buffer(symmetricEncryptAuthenticate(buf, options.sessionKey, options.sessionCryptoSpec));

        var keyIdBuf = new buffer.Buffer(SESSION_KEY_ID_SIZE);
        keyIdBuf.writeUIntBE(options.sessionKey.id, 0, SESSION_KEY_ID_SIZE);
        var msg = {
            msgType: msgType.SKEY_HANDSHAKE_1,
            payload: buffer.concat([keyIdBuf, encBuf])
        };
        var toSend = exports.serializeIoTSP(msg).getArray();
        entityClientSocket.send(toSend);
        console.log('switching to HANDSHAKE_1_SENT');
        entityClientState = entityClientCommState.HANDSHAKE_1_SENT;
    });
    var expectingMoreData = false;
    var obj;
    var iotSecureSocket = null;
    entityClientSocket.on('data', function (data) {
        console.log('data received from server');
        var buf = new buffer.Buffer(data);
        var ret;
        if (!expectingMoreData) {
            obj = exports.parseIoTSP(buf);
            if (obj.payload.length < obj.payloadLen) {
                expectingMoreData = true;
                console.log('more data will come. current: ' + obj.payload.length +
                    ' expected: ' + obj.payloadLen);
            }
        } else {
            obj.payload = buffer.concat([obj.payload, new buffer.Buffer(data)]);
            if (obj.payload.length == obj.payloadLen) {
                expectingMoreData = false;
            } else {
                console.log('more data will come. current: ' + obj.payload.length +
                    ' expected: ' + obj.payloadLen);
            }
        }

        if (expectingMoreData) {
            // do not process the packet yet
            return;
        } else if (obj.msgType == msgType.SKEY_HANDSHAKE_2) {
            console.log('received session key handshake2!');
            if (entityClientState != entityClientCommState.HANDSHAKE_1_SENT) {
                eventHandlers.onError('Comm init failed: Wrong sequence of handshake, disconnecting...');
                entityClientSocket.close();
                return;
            }
            ret = symmetricDecryptVerify(obj.payload, options.sessionKey, options.sessionCryptoSpec);
            if (!ret.hashOk) {
                eventHandlers.onError('Comm init failed: Received hash for handshake2 is NOT ok');
                entityClientSocket.close();
                return;
            }
            console.log('Received hash for handshake2 is ok');
            buf = new buffer.Buffer(ret.data);
            var handshake2 = parseHandshake(buf);
            if (!handshake2.replyNonce.equals(myNonce)) {
                eventHandlers.onError('Comm init failed: Server nonce NOT verified');
                return;
            }
            console.log('Server nonce verified');
            var theirNonce = handshake2.nonce;
            var handshake3 = {
                replyNonce: theirNonce
            };
            buf = serializeHandshake(handshake3);
            var encBuf = new buffer.Buffer(symmetricEncryptAuthenticate(buf, options.sessionKey, options.sessionCryptoSpec));
            var msg = {
                msgType: msgType.SKEY_HANDSHAKE_3,
                payload: encBuf
            };
            entityClientSocket.send(exports.serializeIoTSP(msg).getArray());

            console.log('switching to IN_COMM');
            entityClientState = entityClientCommState.IN_COMM;
            // socket, sessionKey, cipherAlgorithm, hashAlgorithm
            iotSecureSocket = new IoTSecureSocket(entityClientSocket, options.sessionKey,
                options.sessionCryptoSpec);
            eventHandlers.onConnection(iotSecureSocket);
        } else if (obj.msgType == msgType.SECURE_COMM_MSG) {
            console.log('received secure communication message!');
            if (entityClientState == entityClientCommState.IN_COMM) {
                ret = iotSecureSocket.receive(obj.payload);
                if (!ret.success) {
                    eventHandlers.onError(ret.error);
                    return;
                }
                eventHandlers.onData(ret.data);
                return;
            } else {
                eventHandlers.onError('Comm init failed: it is not in IN_COMM state, disconnecting...');
                entityClientSocket.close();
                return;
            }
        }
    });
    entityClientSocket.on('close', function () {
        if (entityClientState == entityClientCommState.IN_COMM) {
            eventHandlers.onClose();
            return;
        }
        eventHandlers.onError('Comm init failed: disconnected from server during communication initialization.');
    });
    entityClientSocket.on('error', function (message) {
        if (entityClientState == entityClientCommState.IN_COMM) {
            eventHandlers.onError(message);
            return;
        }
        eventHandlers.onError('Comm init failed: Error in comm init - details: ' + message);
        entityClientSocket.close();
        return;
    });
    entityClientSocket.open();
};

///////////////////////////////////////////////////////////////////
////                Functions for entity server                ////

/*
options = {
    serverPort,
    sessionCryptoSpec
}
*/
/*
eventHandlers = {
    onServerError,      // for server
    onServerListening,
    onClientRequest,    // for client's communication initialization request

    onClose,            // for individual sockets
    onError,
    onData,
    onConnection
}
*/
exports.initializeSecureServer = function (options, eventHandlers) {
    var entityServer;

    entityServer = new socket.SocketServer({
        //'clientAuth' : this.getParameter('clientAuth'),
        'emitBatchDataAsAvailable': true,
        //'hostInterface' : this.getParameter('hostInterface'),
        //'idleTimeout' : this.getParameter('idleTimeout'),
        //'keepAlive' : false,
        //'noDelay' : this.getParameter('noDelay'),
        //'pfxKeyCertPassword' : this.getParameter('pfxKeyCertPassword'),
        //'pfxKeyCertPath' : this.getParameter('pfxKeyCertPath'),
        'port': options.serverPort,
        'rawBytes': true,
        //'receiveBufferSize' : this.getParameter('receiveBufferSize'),
        'receiveType': 'byte',
        //'sendBufferSize' : this.getParameter('sendBufferSize'),
        'sendType': 'byte',
        //'sslTls' : this.getParameter('sslTls'),
        //'trustedCACertPath' : this.getParameter('trustedCACertPath')
    });

    entityServer.on('error', function (message) {
        eventHandlers.onServerError(message);
    });

    entityServer.on('listening', function (listeningPort) {
        eventHandlers.onServerListening(listeningPort);
    });

    // server communication state
    var entityServerCommState = {
        IDLE: 0,
        WAITING_SESSION_KEY: 20,
        HANDSHAKE_1_RECEIVED: 21,
        HANDSHAKE_2_SENT: 22,
        IN_COMM: 30 // Session message
    };
    var connectionCount = 0;
    entityServer.on('connection', function (entityServerSocket) {
        console.log('client connected');
        // entityServerSocket is an instance of the Socket class defined
        // in the socket module.
        var entityServerState = entityServerCommState.IDLE;
        var myNonce;
        var entityServerSessionKey;
        var socketID = -1;

        function sendHandshake2(handshake1Payload, serverSocket, sessionKey) {
            if (entityServerState != entityServerCommState.HANDSHAKE_1_RECEIVED) {
                eventHandlers.onServerError('Error during comm init - in wrong state, expected: HANDSHAKE_1_RECEIVED, disconnecting...');
                serverSocket.close();
                return;
            }
            var enc = handshake1Payload.slice(SESSION_KEY_ID_SIZE);
            entityServerSessionKey = sessionKey;
            var ret = symmetricDecryptVerify(enc, entityServerSessionKey, options.sessionCryptoSpec);
            if (!ret.hashOk) {
                eventHandlers.onServerError('Error during comm init - received hash for handshake 1 is NOT ok');
                serverSocket.close();
                return;
            }
            console.log('received hash for handshake 1 is ok');
            var buf = new buffer.Buffer(ret.data);

            var handshake1 = parseHandshake(buf);

            myNonce = new buffer.Buffer(crypto.randomBytes(HANDSHAKE_NONCE_SIZE));

            var theirNonce = handshake1.nonce;

            var handshake2 = {
                nonce: myNonce,
                replyNonce: theirNonce
            };

            var encBuf = symmetricEncryptAuthenticate(serializeHandshake(handshake2),
                entityServerSessionKey, options.sessionCryptoSpec);
            var msg = {
                msgType: msgType.SKEY_HANDSHAKE_2,
                payload: new buffer.Buffer(encBuf)
            };
            var toSend = exports.serializeIoTSP(msg).getArray();

            console.log('switching to HANDSHAKE_2_SENT state.');
            entityServerState = entityServerCommState.HANDSHAKE_2_SENT;

            serverSocket.send(toSend);
        }

        console.log('A client connected from ' +
            entityServerSocket.remoteHost() + ':' + entityServerSocket.remotePort());

        entityServerSocket.on('close', function () {
            if (entityServerState == entityServerCommState.IN_COMM) {
                eventHandlers.onClose(socketID);
                return;
            } else {
                eventHandlers.onServerError('Closed during comm init from ' +
                    entityServerSocket.remoteHost() + ':' + entityServerSocket.remotePort());
                entityServerSocket.close();
                return;
            }
        });

        entityServerSocket.on('error', function (message) {
            if (entityServerState == entityServerCommState.IN_COMM) {
                eventHandlers.onError(message, socketID);
                return;
            } else {
                eventHandlers.onServerError('Error during comm init, details: ' + message);
                entityServerSocket.close();
                return;
            }
        });

        var expectingMoreData = false;
        var obj;
        var iotSecureSocket = null;
        entityServerSocket.on('data', function (data) {
            console.log('received data from client');
            var ret;
            var buf = new buffer.Buffer(data);
            if (!expectingMoreData) {
                obj = exports.parseIoTSP(buf);
                if (obj.payload.length < obj.payloadLen) {
                    expectingMoreData = true;
                    console.log('more data will come. current: ' + obj.payload.length +
                        ' expected: ' + obj.payloadLen);
                }
            } else {
                obj.payload = buffer.concat([obj.payload, new buffer.Buffer(data)]);
                if (obj.payload.length == obj.payloadLen) {
                    expectingMoreData = false;
                } else {
                    console.log('more data will come. current: ' + obj.payload.length +
                        ' expected: ' + obj.payloadLen);
                }
            }

            if (expectingMoreData) {
                // do not process the packet yet
                return;
            } else if (obj.msgType == msgType.SKEY_HANDSHAKE_1) {
                console.log('received session key handshake1');
                if (entityServerState != entityServerCommState.IDLE) {
                    eventHandlers.onServerError('Error during comm init - in wrong state, expected: IDLE, disconnecting...');
                    entityServerSocket.close();
                    return;
                }
                console.log('switching to HANDSHAKE_1_RECEIVED state.');
                entityServerState = entityServerCommState.HANDSHAKE_1_RECEIVED;
                eventHandlers.onClientRequest(obj.payload, entityServerSocket, sendHandshake2);

            } else if (obj.msgType == msgType.SKEY_HANDSHAKE_3) {
                console.log('received session key handshake3');
                if (entityServerState != entityServerCommState.HANDSHAKE_2_SENT) {
                    eventHandlers.onServerError('Error during comm init - in wrong state, expected: HANDSHAKE_2_SENT, disconnecting...');
                    entityServerSocket.close();
                    return;
                }
                ret = symmetricDecryptVerify(obj.payload, entityServerSessionKey, options.sessionCryptoSpec);
                if (!ret.hashOk) {
                    eventHandlers.onServerError('Error during comm init - received hash for handshake 3 is NOT ok, disconnecting...');
                    entityServerSocket.close();
                    return;
                }
                console.log('received hash for handshake 3 is ok');
                buf = new buffer.Buffer(ret.data);
                var handshake3 = parseHandshake(buf);
                if (!handshake3.replyNonce.equals(myNonce)) {
                    eventHandlers.onServerError('Error during comm init - client nonce NOT verified');
                    entityServerSocket.close();
                    return;
                }
                console.log('client nonce verified');

                console.log('switching to IN_COMM state.');
                entityServerState = entityServerCommState.IN_COMM;
                iotSecureSocket = new IoTSecureSocket(entityServerSocket, entityServerSessionKey,
                    options.sessionCryptoSpec);

                socketID = connectionCount++;
                var socketInstance = {
                    'id': socketID,
                    'remoteHost': entityServerSocket.remoteHost(),
                    'remotePort': entityServerSocket.remotePort(),
                    'status': 'open'
                };
                eventHandlers.onConnection(socketInstance, iotSecureSocket);
            } else if (obj.msgType == msgType.SECURE_COMM_MSG) {
                if (entityServerState == entityServerCommState.IN_COMM) {
                    ret = iotSecureSocket.receive(obj.payload);
                    if (!ret.success) {
                        eventHandlers.onError(ret.error, socketID);
                        return;
                    }
                    eventHandlers.onData(ret.data, socketID);
                    return;
                } else {
                    eventHandlers.onServerError('Error: it is not in IN_COMM state, disconnecting...');
                    entityServerSocket.close();
                    return;
                }
            }
        });
    });

    // Open the server after setting up all the handlers.
    entityServer.start();
    return entityServer;
};


///////////////////////////////////////////////////////////////////
////          Functions for secure publish-subscribe           ////

/*
    SecurePublishedMessage Format
    {
        keyId: /UIntBE/, // S_KEY_ID_SIZE Bytes - in plain text
        seqNum: /UIntBE/, SEQ_NUM_SIZE Bytes - encrypted
        data: /Buffer/, // data - encrypted
    }
*/
/*
    cryptoSpec          // 'cipher:hash'
*/
/*
    message = {
        sequenceNum,    // Integer
        data            // Buffer
    }
*/
exports.encryptSecureMessageToPublish = function (message, cryptoSpec, sessionKey) {
    var buf = serializeSessionMessage({
        seqNum: message.sequenceNum,
        data: new buffer.Buffer(message.data)
    });
    var encBuf = new buffer.Buffer(symmetricEncryptAuthenticate(buf, sessionKey, cryptoSpec));

    var keyIdBuf = new buffer.Buffer(SESSION_KEY_ID_SIZE);
    keyIdBuf.writeUIntBE(sessionKey.id, 0, SESSION_KEY_ID_SIZE);
    var msg = {
        msgType: msgType.SECURE_PUB,
        payload: buffer.concat([keyIdBuf, encBuf])
    };
    return exports.serializeIoTSP(msg).getArray();
};
/*
    ret = {
        success,
        error,           // only when success == false
        keyId,
        encryptedMessage // Buffer
    }
*/
exports.getKeyIdOfSecurePublishedMessage = function (rawData) {
    var buf = new buffer.Buffer(rawData);

    var obj = exports.parseIoTSP(buf);
    if (obj.msgType != msgType.SECURE_PUB) {
        return {
            success: false,
            error: 'Not a secure published message'
        };
    }
    var keyId = obj.payload.readUIntBE(0, SESSION_KEY_ID_SIZE);
    return {
        success: true,
        keyId: keyId,
        encryptedMessage: obj.payload.slice(SESSION_KEY_ID_SIZE)
    };
};
/*
    ret = {
        success,
        error,           // only when success == false
        sequenceNum,
        message         // JavaScript array
    }
*/
exports.decryptSecurePublishedMessage = function (encryptedMessage, cryptoSpec, sessionKey) {
    var ret = symmetricDecryptVerify(encryptedMessage, sessionKey, cryptoSpec);
    if (!ret.hashOk) {
        return {
            success: false,
            error: 'Received hash for secure comm msg is NOT ok'
        };
    }
    console.log('Received hash for secure published message is ok');
    var buf = new buffer.Buffer(ret.data);
    ret = parseSessionMessage(buf);
    return {
        success: true,
        sequenceNum: ret.seqNum,
        message: ret.data.getArray()
    };
};