'use strict';
/**
* @module lib/protocol/management
* @summary Whiteflag protocol management module
* @description Module for the processing of Whiteflag management messages
* @tutorial modules
* @tutorial protocol
*/
module.exports = {
// Management messages functions
init: initManagement
};
// Whiteflag common functions and classes //
const log = require('../common/logger');
const { hash } = require('../common/crypto');
const { type } = require('../common/protocol');
const { ProcessingError, ProtocolError } = require('../common/errors');
// Whiteflag modules //
const wfState = require('./state');
const wfCrypto = require('./crypto');
const wfRetrieve = require('./retrieve');
const wfAuthenticate = require('./authenticate');
// Whiteflag event emitters //
const wfRxEvent = require('./events').rxEvent;
const wfTxEvent = require('./events').txEvent;
// Module constants //
const MODULELOG = 'protocol';
const KEYIDLENGTH = 12;
const AUTHMESSAGECODE = 'A';
const CRYPTOMESSAGECODE = 'K';
const IV1DATATYPE = '11';
const IV2DATATYPE = '21';
const ECDHPUBKEYDATATYPE = '0A';
/**
* Initialises processing of management messages
* @function init
* @alias module:lib/protocol/management.init
* @param {function(Error)} callback function to be called upon completion
*/
function initManagement(callback) {
/**
* Listener for received management messages
* @listens module:lib/protocol/receive.rxEvent:messageProcessed
* @param {wfMessage} wfMessage a Whiteflag message
*/
wfRxEvent.on('messageProcessed', receivedMessage);
/**
* Listener for encrypted messages without initialisation vectors
* @listens module:lib/protocol/events.rxEvent:messageSent
* @param {wfMessage} wfMessage a Whiteflag message
*/
wfTxEvent.on('messageProcessed', sentMessage);
// Invoke callback after binding all events to listeners/handlers
return callback(null);
}
// PRIVATE MODULE FUNCTIONS //
/**
* Passes received management messages to correct handlers
* @private
* @param {wfMessage} wfMessage a Whiteflag message
*/
function receivedMessage(wfMessage) {
// Check required actions for specific received message types
switch (wfMessage.MessageHeader.MessageCode) {
case AUTHMESSAGECODE: {
receiveAuthenticationData(wfMessage);
return;
}
case CRYPTOMESSAGECODE: {
switch (wfMessage.MessageBody.CryptoDataType) {
case IV1DATATYPE:
case IV2DATATYPE: {
// Initialisation Vector for Encryption Types 1 and 2
receiveInitVector(wfMessage);
return;
}
case ECDHPUBKEYDATATYPE: {
// ECDH Public Key
receiveECDHpublicKey(wfMessage);
return;
}
default: break;
}
return;
}
default: return;
}
}
/**
* Triggers correct management message handlers after a message has been sent
* @private
* @param {wfMessage} wfMessage a Whiteflag message
*/
function sentMessage(wfMessage) {
// All message types require K message for init vector if encrypted
sendInitVector(wfMessage);
// Check required post transmission actions for specific message types
switch (wfMessage.MessageHeader.MessageCode) {
case AUTHMESSAGECODE: {
// ECDH public key is automatically sent after an authentication message
sendECDHpublicKey(wfMessage);
return;
}
default: return;
}
}
// MANAGEMENT MESSAGE HANDLER FUNCTIONS //
/**
* Processes received authentication message for verification method 1
* @private
* @param {Object} wfAuthMessage Whiteflag authentication message
* @emits module:lib/protocol/events.rxEvent:messageUpdated
*/
function receiveAuthenticationData(wfAuthMessage) {
switch (wfAuthMessage.MessageHeader.ReferenceIndicator) {
case '0':
case '2': {
// Original and update message
log.trace(MODULELOG, `Received ${type(wfAuthMessage)} message: Verifying originator authentication data`);
wfAuthenticate.verify(wfAuthMessage, function mgmtVerifyAuthCb(err, wfAuthMessage) {
if (err) {
if (err instanceof ProtocolError) {
if (err.causes) {
log.debug(MODULELOG, `Could not verify received ${type(wfAuthMessage)} message ${wfAuthMessage.MetaHeader.transactionHash}: ${err.message}: ` + JSON.stringify(err.causes));
} else {
log.debug(MODULELOG, `Could not verify received ${type(wfAuthMessage)} message ${wfAuthMessage.MetaHeader.transactionHash}: ${err.message}`);
}
} else {
log.warn(MODULELOG, `Could not verify received ${type(wfAuthMessage)} message ${wfAuthMessage.MetaHeader.transactionHash}: ${err.message}`);
}
}
wfRxEvent.emit('messageUpdated', wfAuthMessage);
});
return;
}
case '1':
case '4': {
// Recall and discontinue message
log.trace(MODULELOG, `Received ${type(wfAuthMessage)} message: Removing originator authentication data`);
wfAuthenticate.remove(wfAuthMessage, function mgmtRemoveAuthCb(err, wfAuthMessage) {
if (err) {
if (err instanceof ProtocolError) {
if (err.causes) {
log.debug(MODULELOG, `Could not verify received ${type(wfAuthMessage)} message ${wfAuthMessage.MetaHeader.transactionHash}: ${err.message}: ` + JSON.stringify(err.causes));
} else {
log.debug(MODULELOG, `Could not verify received ${type(wfAuthMessage)} message ${wfAuthMessage.MetaHeader.transactionHash}: ${err.message}`);
}
} else {
log.warn(MODULELOG, `Could not update originator state after receiving ${type(wfAuthMessage)} message ${wfAuthMessage.MetaHeader.transactionHash}: ${err.message}`);
}
}
wfRxEvent.emit('messageUpdated', wfAuthMessage);
});
return;
}
case '3': {
// Additional information is currently not implemented
return;
}
default: return;
}
}
/**
* Processes received initialisation vector
* @private
* @param {Object} wfCryptoMessage Whiteflag crypto message with initialisation vector
*/
function receiveInitVector(wfCryptoMessage) {
const blockchain = wfCryptoMessage.MetaHeader.blockchain;
const transactionHash = wfCryptoMessage.MetaHeader.transactionHash;
const referencedMessage = wfCryptoMessage.MessageHeader.ReferencedMessage;
const initVector = wfCryptoMessage.MessageBody.CryptoData;
switch (wfCryptoMessage.MessageHeader.ReferenceIndicator) {
case '0': {
// Original: stand-alone iv does nothing
break;
}
case '1':
case '4': {
// Recall or Discontinue: remove iv from queue
log.trace(MODULELOG, `Received ${type(wfCryptoMessage)} message ${wfCryptoMessage.MetaHeader.transactionHash}: Removing initialisation vector from queue`);
wfState.removeQueueData('initVectors', 'transactionHash', referencedMessage);
break;
}
case '2': {
// Update iv if on queue
log.trace(MODULELOG, `Received ${type(wfCryptoMessage)} message ${wfCryptoMessage.MetaHeader.transactionHash}: Updating initialisation vector on queue`);
wfState.getQueueData('initVectors', 'transactionHash', referencedMessage, function mgmtUpdateInitVectorCb(err, ivObject) {
if (err) log.warn(MODULELOG, `Error getting initialisation vector from queue: ${err.message}`);
if (ivObject) {
ivObject.initVector = initVector;
wfState.upsertQueueData('initVectors', 'transactionHash', ivObject);
}
});
break;
}
case '3': {
// Add: iv is part of the encrypted message it references
log.trace(MODULELOG, `Received ${type(wfCryptoMessage)} message ${wfCryptoMessage.MetaHeader.transactionHash} with initialisation vector`);
wfRetrieve.getMessage(referencedMessage, blockchain, function mgmtGetMessageInitVectorCb(err, wfMessages) {
if (err && !(err instanceof ProcessingError)) {
log.warn(MODULELOG, `${err.message}`);
}
// No message found; put iv on queue
if (!wfMessages || !Array.isArray(wfMessages) || wfMessages.length === 0) {
const ivObject = {
transactionHash,
referencedMessage,
initVector
};
wfState.upsertQueueData('initVectors', 'referencedMessage', ivObject);
log.trace(MODULELOG, `Initialisation vector for message ${referencedMessage} put on queue: ` + JSON.stringify(ivObject));
return;
}
// Found message in database or on blockchain
if (wfMessages.length > 0) {
const wfMessage = wfMessages[0];
// No need to decrypt if messages is sent and already has an init vector
if (
wfMessage.MetaHeader.transceiveDirection === 'TX'
&& wfMessage.MetaHeader.encryptionInitVector
) {
return;
}
// Add initialistion vector to message and trigger further processing
log.trace(MODULELOG, `Found encrypted message matching incoming initialisation vector: ${wfMessage.MetaHeader.transactionHash}`);
wfMessage.MetaHeader.encryptionInitVector = initVector;
return wfRxEvent.emit('messageReceived', wfMessage);
}
});
break;
}
default: return;
}
}
/**
* Sends an initialisation vector after an encrypted message
* @private
* @param {wfMessage} wfMessage a Whiteflag message
* @emits _txEvent:messageCommitted
*/
function sendInitVector(wfMessage) {
// Check encryption indicator
let cryptoDataType;
switch (wfMessage.MessageHeader.EncryptionIndicator) {
case '1': {
cryptoDataType = IV1DATATYPE;
break;
}
case '2': {
cryptoDataType = IV2DATATYPE;
break;
}
default: return;
}
// Check initialisation vector and build K message
if (wfMessage.MetaHeader.encryptionInitVector) {
const wfCryptoMessage = {
'MetaHeader': {
'autoGenerated': true,
'blockchain': wfMessage.MetaHeader.blockchain,
'originatorAddress': wfMessage.MetaHeader.originatorAddress
},
'MessageHeader': {
'Prefix': 'WF',
'Version': wfMessage.MessageHeader.Version,
'EncryptionIndicator': '0',
'DuressIndicator': '0',
'MessageCode': CRYPTOMESSAGECODE,
'ReferenceIndicator': '3',
'ReferencedMessage': wfMessage.MetaHeader.transactionHash
},
'MessageBody': {
'CryptoDataType': cryptoDataType,
'CryptoData': wfMessage.MetaHeader.encryptionInitVector
}
};
// Commit the crypto message to the tx event chain
log.debug(MODULELOG, `Sending ${type(wfCryptoMessage)} message with initialisation vector for ${type(wfMessage)} message: ${wfMessage.MetaHeader.transactionHash}`);
return wfTxEvent.emit('messageCommitted', wfCryptoMessage);
}
}
/**
* Processes received ECDH public key
* @private
* @param {Object} wfCryptoMessage Whiteflag crypto message with ECDH public key
*/
function receiveECDHpublicKey(wfCryptoMessage) {
// Paramaters
const blockchain = wfCryptoMessage.MetaHeader.blockchain;
const originatorAddress = wfCryptoMessage.MetaHeader.originatorAddress;
const originatorECDHpubKey = wfCryptoMessage.MessageBody.CryptoData;
wfState.getOriginatorData(originatorAddress, function mgmtGetOriginatorCb(err, originator) {
if (err) return log.error(MODULELOG, `Received ${type(wfCryptoMessage)} message ${wfCryptoMessage.MetaHeader.transactionHash} but could not get originator state to compute shared secret: ${err.message}`);
// Check reference indicator
switch (wfCryptoMessage.MessageHeader.ReferenceIndicator) {
case '0':
case '2': {
// Store the ECDH public key
log.trace(MODULELOG, `Received ${type(wfCryptoMessage)} message ${wfCryptoMessage.MetaHeader.transactionHash} with ECDH public key from address ${originatorAddress}`);
if (originator) {
// Known origintaor
originator.ecdhPublicKey = originatorECDHpubKey;
wfState.upsertOriginatorData(originator);
} else {
// Unknown originator
const newOriginator = {
name: '',
blockchain: blockchain,
address: originatorAddress,
originatorPubKey: null,
ecdhPublicKey: originatorECDHpubKey,
url: null,
authTokenId: '',
authenticationValid: false,
authenticationMessages: []
};
wfState.upsertOriginatorData(newOriginator);
}
break;
}
case '1':
case '4': {
// Remove the ECDH public key if originator is known
log.trace(MODULELOG, `Received ${type(wfCryptoMessage)} message ${wfCryptoMessage.MetaHeader.transactionHash} to remove ECDH public key from address ${originatorAddress}`);
if (originator) {
originator.ecdhPublicKey = null;
wfState.upsertOriginatorData(originator);
}
return;
}
default: return;
}
// Get accounts for this blockchain and generate a shared secret for each
wfState.getBlockchainData(blockchain, function mgmtGetBlockchainDataCb(err, blockchainState) {
if (!err && !blockchainState) err = new Error(`Blockchain ${blockchain} does not exist in state`);
if (err) return log.error(MODULELOG, `Could not retrieve ${blockchain} state to compute shared secrets: ${err.message}`);
blockchainState.accounts.forEach(account => {
generateECDHsecret(blockchain, account.address, originatorAddress, originatorECDHpubKey);
});
});
});
}
/**
* Sends an ECDH public key after an authentication message
* @private
* @param {wfMessage} wfMessage a Whiteflag message
* @emits _txEvent:messageCommitted
*/
function sendECDHpublicKey(wfAuthMessage) {
let newKeyPair = false;
// Do not send ECDH public key after encyrpted or duress A message
if (wfAuthMessage.MessageHeader.EncryptionIndicator !== '0') {
return log.debug(MODULELOG, `Not sending ECDH public key after encrypted ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}`);
}
if (wfAuthMessage.MessageHeader.DuressIndicator !== '0') {
return log.debug(MODULELOG, `Not sending ECDH public key after ${type(wfAuthMessage)} message under duress: ${wfAuthMessage.MetaHeader.transactionHash}`);
}
// Check reference indicator
switch (wfAuthMessage.MessageHeader.ReferenceIndicator) {
case '0': {
// An original authentication message resends the ECDH public key if already existing
newKeyPair = false;
break;
}
case '2': {
// A message with updated authentication information triggers to renew the ECDH key pair
newKeyPair = true;
break;
}
default: return;
}
// Get own ECDH public key for this blockchain account
const blockchain = wfAuthMessage.MetaHeader.blockchain;
const address = wfAuthMessage.MetaHeader.originatorAddress;
const ecdhId = hash(blockchain + address, KEYIDLENGTH);
wfCrypto.getECDHpublicKey(ecdhId, newKeyPair, function mgmtGetMessageInitVectorCb(err, ecdhPublicKey, newKey) {
if (err) return log.error(MODULELOG, `Could not get and send ECDH public key for account ${address}: ${err.message}`);
// Build K message
const wfCryptoMessage = {
'MetaHeader': {
'autoGenerated': true,
'blockchain': wfAuthMessage.MetaHeader.blockchain,
'originatorAddress': wfAuthMessage.MetaHeader.originatorAddress
},
'MessageHeader': {
'Prefix': 'WF',
'Version': wfAuthMessage.MessageHeader.Version,
'EncryptionIndicator': wfAuthMessage.MessageHeader.EncryptionIndicator,
'DuressIndicator': wfAuthMessage.MessageHeader.DuressIndicator,
'MessageCode': CRYPTOMESSAGECODE,
'ReferenceIndicator': '0',
'ReferencedMessage': wfAuthMessage.MetaHeader.transactionHash
},
'MessageBody': {
'CryptoDataType': ECDHPUBKEYDATATYPE,
'CryptoData': ecdhPublicKey
}
};
// Logging
log.debug(MODULELOG, `Sending ${type(wfCryptoMessage)} message with ECDH public key after ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}`);
// Commit the crypto message to the tx event chain
wfTxEvent.emit('messageCommitted', wfCryptoMessage, function mgmtSendECDHpubKeyCb(err) {
// Only compute new secrets when newly generated key pair
if (!newKey && !err) return;
if (err) {
if (newKey) return log.error(MODULELOG, `Not computing new shared secrets: ${type(wfCryptoMessage)} message not sent after renewed ECDH key pair: ${err.message}`);
return log.warn(MODULELOG, `Could not send ${type(wfCryptoMessage)} message: ${err.message}`);
}
// Check ECDH public keys from known originators and calculate shared secret
wfState.getOriginators(function mgmtGetECDHoriginatorsCb(err, originators) {
if (err) return log.error(MODULELOG, `Could not get originator state to compute shared secrets: ${err.message}`);
// Check ECDH public key for each originator
originators.forEach(originator => {
if (originator.ecdhPublicKey && originator.blockchain === blockchain) {
generateECDHsecret(blockchain, address, originator.address, originator.ecdhPublicKey);
}
});
});
});
});
}
/**
* Generates an ECDH shared secret
* @private
* @param {string} blockchain the blockchain name
* @param {string} address the blockchain account to generate the secret for
* @param {string} originatorAddress the address of the other originator
* @param {string} originatorECHDpubKey the ECDH public key of the other originator
*/
function generateECDHsecret(blockchain, address, originatorAddress, originatorECHDpubKey) {
const ecdhId = hash(blockchain + address, KEYIDLENGTH);
wfCrypto.generateECDHsecret(ecdhId, originatorECHDpubKey, function mgmtGenECDHsecretCb(err, secret) {
if (err) {
if (err instanceof ProcessingError) {
return log.debug(MODULELOG, `Could not compute ECDH negotiated secret for account ${address} with originator address ${originatorAddress}: ${err.message}`);
}
return log.error(MODULELOG, `Could not compute ECDH negotiated secret for account ${address} with originator address ${originatorAddress}: ${err.message}`);
}
const secretId = hash(blockchain + address + originatorAddress, KEYIDLENGTH);
wfState.upsertKey('negotiatedKeys', secretId, secret);
log.debug(MODULELOG, `Computed ECDH negotiated secret for account ${address} with originator address ${originatorAddress}`);
});
}