// Copyright (c) 2016 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.
/** This accessor is used for accessing authorization service provided by
* a local authorization entity, Auth (https://github.com/iotauth/iotauth),
* and for secure communication with a SecureCommserver.
*
* Specifically, this accessor establishes a secure communication with server
* using session keys (symmetric cryptographic keys) and sends/receives
* messages to/from the server. To obtain session keys, this accessor also
* communicates with the local authorization entity, Auth.
*
* This accessor internally manages the credentials (cryptographic keys)
* for communication with remote Auth and remote server.
* All the messages to/from remote Auth and server are protected using
* the credentials, while input/output data of this accessor is in plain text.
*
* This accessor requires the 'iotAuth', and 'dataConverter' modules.
*
* @accessor net/SecureCommClient
*
* @input serverHostPort Information of the destination server. This input triggers
* a secure connection with a server (possibly using the SecureCommServer accessor).
* This input is specified as a JSON with two properties, 'host' and 'port'. The property
* 'host' specifies the IP address or domain name of server in string and 'port' specifies
* the port number in integer. If a session key for communicating with a server is not
* available, the SecureCommClient communicates with Auth to request the session key(s)
* for secure communication, before establishing a secure connection with the server.
* @input toSend The data to be sent over the secure connection with the server.
*
* @output {boolean} connected Output `true` on connected and `false` on disconnected with
* the server over a secure connection.
* @output received The data received from the server over a secure connection.
*
* @parameter {string} clientName The client's unique name in string.
* @parameter {string} authHost Auth's IP address or domain name.
* @parameter {int} authPort Auth's port number.
*
* @parameter {string} authCertPath The path for the X.509 certificate file (in pem format)
* of Auth with which the client is registered.
* @parameter {string} clientPrivateKeyPath The path for the pem format private key of
* the client.
*
* @parameter {string} publicKeyCryptoSpec The specification for the public cryptography
* algorithms to be used for communication with Auth
* @parameter {string} distributionCryptoSpec The specification for the symmetric cryptography
* algorithms to be used for communication with Auth
* @parameter {string} sessionCryptoSpec The specification for the symmetric cryptography
* algorithms to be used for communication with the server
*
* @parameter {int} numKeysPerRequest The number of session keys to be requested per
* session key request to Auth
* @parameter {string} targetServerGroup The communication policy group to which the
* target server belong.
*
* @parameter {string} receiveType Data type of the received data from server.
* @parameter {string} sendType Data type of the sent data to server.
*
* @author Hokeun Kim
* @version $$Id$$
*/
// Stop extra messages from jslint. Note that there should be no
// space between the / and the * and global.
/*global console, exports, require */
/*jshint globalstrict: true */
"use strict";
var iotAuth = require('@accessors-modules/iot-auth');
//var dataConverter = require('dataConverter');
var msgType = iotAuth.msgType;
exports.setup = function () {
// Inputs and outputs
this.input('serverHostPort', {
type: 'JSON',
value: {host: 'localhost', port: 21200}
});
this.input('toSend');
this.output('connected', {
type: 'boolean',
spontaneous: true
});
this.output('received', {
type: 'string'
});
// Client information
this.parameter('clientName', {
value: '',
type: 'string'
});
// For communication with Auth
this.parameter('authHost', {
type: 'string',
value: 'localhost'
});
this.parameter('authPort', {
value: -1,
type: 'int'
});
this.parameter('authCertPath', {
value: '',
type: 'string'
});
this.parameter('clientPrivateKeyPath', {
value: '',
type: 'string'
});
// Spec for communication with Auth
this.parameter('publicKeyCryptoSpec', {
type: 'string',
options: iotAuth.publicKeyCryptoSpecs
});
this.parameter('distributionCryptoSpec', {
type: 'string',
options: iotAuth.symmetricCryptoSpecs
});
this.parameter('numKeysPerRequest', {
value: 1,
type: 'int'
});
// For communication with server
this.parameter('targetServerGroup', {
value: '',
type: 'string'
});
this.parameter('sessionCryptoSpec', {
type: 'string',
options: iotAuth.symmetricCryptoSpecs
});
// Send/receive type
this.parameter('receiveType', {
type: 'string',
value: 'string',
options: ['string', 'image', 'byteArray']
});
this.parameter('sendType', {
type: 'string',
value: 'string',
options: ['string', 'image', 'byteArray']
});
};
// client communication state
var clientCommState = {
IDLE: 0,
IN_COMM: 30 // Session message
};
// local variables
var self;
var receiveType;
var sendType;
var authPublicKey;
var clientPrivateKey;
var currentSecureClient = null;
var currentDistributionKey = null;
var currentSessionKeyList = [];
var currentSessionKey = null;
var currentState = clientCommState.IDLE;
function outputError(errorMessage) {
console.log(errorMessage);
self.error(errorMessage);
}
// Event handlers for a secure client socket.
function onConnection(entityClientSocket) {
console.log('communication initialization succeeded');
currentSecureClient = entityClientSocket;
currentState = clientCommState.IN_COMM;
self.send('connected', true);
}
function onClose() {
console.log('secure connection with the server closed.');
self.send('connected', false);
}
function onError(message) {
outputError('Error in secure comm - details: ' + message);
}
function onData(data) {
console.log('data received from server via secure communication');
if (receiveType == 'string') {
self.send('received', data.toString());
} else if (receiveType == 'image') {
self.send('received', dataConverter.jsArrayToImage(data.getArray()));
} else if (receiveType == 'byteArray') {
self.send('received', data.getArray());
}
}
function initSecureCommWithSessionKey(sessionKey, serverHost, serverPort) {
currentSessionKey = sessionKey;
if (currentSecureClient) {
currentSecureClient.close();
console.log('Status: Secure connection closed before starting a new connection.');
currentState = clientCommState.IDLE;
}
var options = {
serverHost: serverHost,
serverPort: serverPort,
sessionKey: currentSessionKey,
sessionCryptoSpec: self.getParameter('sessionCryptoSpec')
};
var eventHandlers = {
onClose: onClose,
onError: onError,
onData: onData,
onConnection: onConnection
};
iotAuth.initializeSecureCommunication(options, eventHandlers);
}
/*
callbackParameters = {
host,
port
}
*/
function sessionKeyResponseCallback(status, distributionKey, sessionKeyList, callbackParameters) {
if (status.error) {
console.log(status.error);
console.log('session key request failed...');
return;
}
console.log('session key request succeeded');
if (distributionKey) {
console.log('Updating to a new distribution key key');
currentDistributionKey = distributionKey;
}
console.log('received ' + sessionKeyList.length + ' session keys');
for (var i = 0; i < sessionKeyList.length; i++) {
currentSessionKeyList.push(sessionKeyList[i]);
}
if (currentSessionKeyList.length > 0) {
initSecureCommWithSessionKey(currentSessionKeyList.shift(),
callbackParameters.host, callbackParameters.port);
}
}
exports.serverHostPortInputHandler = function () {
var serverHostPort = this.get('serverHostPort');
if (currentSessionKeyList.length > 0) {
initSecureCommWithSessionKey(currentSessionKeyList.shift(),
serverHostPort.host, serverHostPort.port);
} else {
var options = {
authHost: this.getParameter('authHost'),
authPort: this.getParameter('authPort'),
entityName: this.getParameter('clientName'),
numKeysPerRequest: this.getParameter('numKeysPerRequest'),
purpose: {
group: this.getParameter('targetServerGroup')
},
distributionKey: currentDistributionKey,
distributionCryptoSpec: this.getParameter('distributionCryptoSpec'),
publicKeyCryptoSpec: this.getParameter('publicKeyCryptoSpec'),
authPublicKey: authPublicKey,
entityPrivateKey: clientPrivateKey
};
iotAuth.sendSessionKeyRequest(options, sessionKeyResponseCallback, serverHostPort);
}
};
exports.toSendInputHandler = function () {
var toSend = this.get('toSend');
if (sendType == 'image') {
toSend = dataConverter.imageToJSArray(toSend);
}
if (currentSecureClient && currentState == clientCommState.IN_COMM) {
if (!currentSecureClient.checkSessionKeyValidity()) {
outputError('session key expired!');
} else if (!currentSecureClient.send(toSend)) {
outputError('Error in sending data');
}
} else {
console.log('Discarding data because socket is not open.');
}
};
exports.initialize = function () {
currentState = clientCommState.IDLE;
currentSessionKeyList = [];
currentSessionKey = null;
authPublicKey = iotAuth.loadPublicKey(this.getParameter('authCertPath'));
clientPrivateKey = iotAuth.loadPrivateKey(this.getParameter('clientPrivateKeyPath'));
receiveType = this.getParameter('receiveType');
sendType = this.getParameter('sendType');
self = this;
this.addInputHandler('serverHostPort',
this.exports.serverHostPortInputHandler.bind(this));
this.addInputHandler('toSend',
this.exports.toSendInputHandler.bind(this));
};
exports.wrapup = function () {
if (currentSecureClient) {
currentSecureClient.close();
console.log('Status: Connection closed in wrap-up.');
currentState = clientCommState.IDLE;
}
};