Source: protocol/management.js

'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 = {
    init: initManagement
};

/* Common internal functions and classes */
const log = require('../_common/logger');
const { hash } = require('../_common/crypto');
const { type } = require('./_common/messages');
const { ProcessingError, ProtocolError } = require('../_common/errors');

/* Whiteflag modules */
const wfState = require('./state');
const wfCrypto = require('./crypto');
const wfRetrieve = require('./retrieve');
const wfAuthenticate = require('./authenticate');
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';
const AUTOMESGDELAY = 12000;

/**
 * Initialises processing of management messages
 * @function init
 * @alias module:lib/protocol/management.init
 * @param {errorCb} callback function called on 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) {
    const { MessageHeader: header,
            MessageBody: body } = wfMessage;

    // Check required actions for received message types
    switch (header.MessageCode) {
        case AUTHMESSAGECODE: {
            receiveAuthenticationData(wfMessage);
            return;
        }
        case CRYPTOMESSAGECODE: {
            switch (body.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) {
    switch (wfMessage.MessageHeader.MessageCode) {
        case AUTHMESSAGECODE: {
            // Send ECDH public key after an authentication message
            setTimeout(sendECDHpublicKey, AUTOMESGDELAY, wfMessage);
        }
        default: {
            // All messages require init vector if encrypted
            setTimeout(sendInitVector, AUTOMESGDELAY, wfMessage);
        }
    }
}

/* MANAGEMENT MESSAGE HANDLER FUNCTIONS */
/**
 * Processes received authentication message for verification method 1
 * @private
 * @param {wfMessage} wfAuthMessage Whiteflag authentication message
 * @emits module:lib/protocol/events.rxEvent:messageUpdated
 */
function receiveAuthenticationData(wfAuthMessage) {
    const { MetaHeader: meta,
            MessageHeader: header } = wfAuthMessage;

    // Determine reference indicator of authentication message
    const msgStr = `${type(wfAuthMessage)} message ${meta.transactionHash}`
    switch (header.ReferenceIndicator) {
        case '0':
        case '2': {
            // Original and update message
            log.trace(MODULELOG, `Received ${msgStr}: 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 ${msgStr}: ${err.message}: ` + JSON.stringify(err.causes));
                        } else {
                            log.debug(MODULELOG, `Could not verify received ${msgStr}: ${err.message}`);
                        }
                    } else {
                        log.warn(MODULELOG, `Could not verify received ${msgStr}: ${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 ${msgStr}: ${err.message}: ` + JSON.stringify(err.causes));
                        } else {
                            log.debug(MODULELOG, `Could not verify received ${msgStr}: ${err.message}`);
                        }
                    } else {
                        log.warn(MODULELOG, `Could not update originator state after receiving ${msgStr}: ${err.message}`);
                    }
                }
                wfRxEvent.emit('messageUpdated', wfAuthMessage);
            });
            return;
        }
        case '3': {
            // Additional information is currently not implemented
            return log.debug(MODULELOG, `Received ${msgStr}: Ignoring additional information for authentication messages`);;
        }
        default:{
            return log.debug(MODULELOG, `Received ${msgStr} has invalid reference code`);
        }
    }
}

/**
 * Processes received initialisation vector
 * @private
 * @param {wfMessage} wfCryptoMessage Whiteflag crypto message with initialisation vector
 */
function receiveInitVector(wfCryptoMessage) {
    const { 
        MetaHeader: {
            blockchain: blockchain,
            transactionHash: cryptoMessageHash },
        MessageHeader: {
            ReferenceIndicator: refIndicator,
            ReferencedMessage: refMessageHash },
        MessageBody: {
            CryptoData: initVector }} =  wfCryptoMessage;

    // Determine reference indicator of crypto message
    const msgStr = `${type(wfCryptoMessage)} message ${cryptoMessageHash}`
    switch (refIndicator) {
        case '0': {
            // Original: stand-alone iv does nothing
            break;
        }
        case '1':
        case '4': {
            // Recall or Discontinue: remove iv from queue
            log.trace(MODULELOG, `Received ${msgStr}: Removing initialisation vector from queue`);
            wfState.removeQueueData('initVectors', 'transactionHash', refMessageHash);
            break;
        }
        case '2': {
            // Update iv if on queue
            log.trace(MODULELOG, `Received ${msgStr}: Updating initialisation vector on queue`);
            wfState.getQueueData('initVectors', 'transactionHash', refMessageHash, 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 ${msgStr} with initialisation vector`);
            wfRetrieve.getMessage(refMessageHash, 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 = {
                        cryptoMessageHash,
                        refMessageHash,
                        initVector
                    };
                    wfState.upsertQueueData('initVectors', 'refMessage', ivObject);
                    log.trace(MODULELOG, `Initialisation vector for message ${refMessageHash} put on queue: ` + JSON.stringify(ivObject));
                    return;
                }
                // Found message in database or on blockchain
                if (wfMessages.length > 0) {
                    const wfMessage = wfMessages[0];
                    let { MetaHeader: meta } = wfMessage;

                    // No need to decrypt if messages is sent and already has an init vector
                    if (
                        meta.transceiveDirection === 'TX'
                        && meta.encryptionInitVector
                    ) {
                        return;
                    }
                    // Add initialistion vector to message and trigger further processing
                    log.trace(MODULELOG, `Found encrypted message matching incoming initialisation vector: ${meta.transactionHash}`);
                    meta.encryptionInitVector = initVector;
                    return wfRxEvent.emit('messageReceived', wfMessage);
                }
            });
            break;
        }
        default: {
            return log.debug(MODULELOG, `Received ${msgStr} has invalid reference code`);
        }
    }
}

/**
 * Sends an initialisation vector after an encrypted message
 * @private
 * @param {wfMessage} wfMessage a Whiteflag message
 * @emits _txEvent:messageCommitted
 */
function sendInitVector(wfMessage) {
    const { MetaHeader: meta,
            MessageHeader: header } = wfMessage;

    // Check encryption indicator
    let cryptoDataType;
    switch (header.EncryptionIndicator) {
        case '1': {
            cryptoDataType = IV1DATATYPE;
            break;
        }
        case '2': {
            cryptoDataType = IV2DATATYPE;
            break;
        }
        default: return;
    }
    // Check initialisation vector and build K message
    if (meta.encryptionInitVector) {
        const wfCryptoMessage = {
            'MetaHeader': {
                'autoGenerated': true,
                'blockchain': meta.blockchain,
                'originatorAddress': meta.originatorAddress
            },
            'MessageHeader': {
                'Prefix': 'WF',
                'Version': header.Version,
                'EncryptionIndicator': '0',
                'DuressIndicator': '0',
                'MessageCode': CRYPTOMESSAGECODE,
                'ReferenceIndicator': '3',
                'ReferencedMessage': meta.transactionHash
            },
            'MessageBody': {
                'CryptoDataType': cryptoDataType,
                'CryptoData': meta.encryptionInitVector
            }
        };
        // Commit the crypto message to the tx event chain
        log.debug(MODULELOG, `Sending ${type(wfCryptoMessage)} message with initialisation vector for ${type(wfMessage)} message: ${meta.transactionHash}`);
        return wfTxEvent.emit('messageCommitted', wfCryptoMessage);
    }
}

/**
 * Processes received ECDH public key
 * @private
 * @param {wfMessage} wfCryptoMessage Whiteflag crypto message with ECDH public key
 */
function receiveECDHpublicKey(wfCryptoMessage) {
    const { 
        MetaHeader: {
            blockchain: blockchain,
            transactionHash: cryptoMessageHash,
            originatorAddress: orgAddress },
        MessageHeader: {
            ReferenceIndicator: refIndicator },
        MessageBody: {
            CryptoData: orgECDHpubKey }} =  wfCryptoMessage;

    // Check if origintaor is known
    const msgStr = `${type(wfCryptoMessage)} message ${cryptoMessageHash}`
    wfState.getOriginatorData(orgAddress, function mgmtGetOriginatorCb(err, originator) {
        if (err) return log.error(MODULELOG, `Received ${msgStr} but could not get originator state to compute shared secret: ${err.message}`);

        // Check reference indicator
        switch (refIndicator) {
            case '0':
            case '2': {
                // Store the ECDH public key
                log.trace(MODULELOG, `Received ${msgStr} with ECDH public key from originator ${orgAddress}`);
                if (originator) {
                    // Known origintaor
                    originator.ecdhPublicKey = orgECDHpubKey;
                    wfState.upsertOriginatorData(originator);
                } else {
                    // Unknown originator
                    const newOriginator = {
                        name: '',
                        blockchain: blockchain,
                        address: orgAddress,
                        publicKey: null,
                        ecdhPublicKey: orgECDHpubKey,
                        url: null,
                        authTokenId: '',
                        authValid: false,
                        authMessages: []
                    };
                    wfState.upsertOriginatorData(newOriginator);
                }
                break;
            }
            case '1':
            case '4': {
                // Remove the ECDH public key if originator is known
                log.trace(MODULELOG, `Received ${msgStr} to remove ECDH public key from originator ${orgAddress}`);
                if (originator) {
                    originator.ecdhPublicKey = null;
                    wfState.upsertOriginatorData(originator);
                }
                return;
            }
            default: {
                return log.debug(MODULELOG, `Received ${msgStr} has invalid reference code`);
            }
        }
        // Do not comuter secret if originator is unauthenticated
        if (!originator.authValid) {
            return log.info(MODULELOG, `Not computing shared secrets: Received ${msgStr} is from unauthenticated originator ${orgAddress}`)
        }
        // Get accounts for this blockchain and generate a shared secret for each
        wfState.getBlockchainData(blockchain, function mgmtGetBlockchainDataCb(err, bcState) {
            if (!err && !bcState) 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}`);

            bcState.accounts.forEach(account => {
                generateECDHsecret(blockchain, account.address, orgAddress, orgECDHpubKey);
            });
        });
    });
}

/**
 * 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.info(MODULELOG, `Not sending ECDH public key after encrypted ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}`);
    }
    if (wfAuthMessage.MessageHeader.DuressIndicator !== '0') {
        return log.info(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 = false) {
        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
            }
        };
        // Commit the crypto message to the tx event chain
        log.debug(MODULELOG, `Sending ${type(wfCryptoMessage)} message with ECDH public key after ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}`);
        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 secrets
            wfState.getOriginators(function mgmtGetECDHoriginatorsCb(err, originators) {
                if (err) return log.error(MODULELOG, `Could not get originator state to compute shared secrets: ${err.message}`);
                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} orgAddress the address of the other originator
 * @param {string} orgECDHpubKey the ECDH public key of the other originator
 */
function generateECDHsecret(blockchain, address, orgAddress, orgECDHpubKey) {
    const ecdhId = hash(blockchain + address, KEYIDLENGTH);
    wfCrypto.generateECDHsecret(ecdhId, orgECDHpubKey, 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 ${orgAddress}: ${err.message}`);
            }
            return log.error(MODULELOG, `Could not compute ECDH negotiated secret for account ${address} with originator address ${orgAddress}: ${err.message}`);
        }
        const secretId = hash(blockchain + address + orgAddress, KEYIDLENGTH);
        wfState.upsertKey('negotiatedKeys', secretId, secret);
        log.info(MODULELOG, `Computed ECDH negotiated secret for account ${address} with originator address ${orgAddress}`);
    });
}