Source: protocol/crypto.js

'use strict';
/**
 * @module lib/protocol/crypto
 * @summary Whiteflag cryptography module
 * @description Module for Whiteflag cryptographic functions
 * @tutorial modules
 * @tutorial protocol
 */
module.exports = {
    encrypt: encryptMessage,
    decrypt: decryptMessage,
    getTokenVerificationData,
    getECDHpublicKey,
    generateECDHsecret,
    test: {
        genInitVector,
        generateKey,
        encrypt,
        decrypt
    }
};

/* Node.js core and external modules */
const crypto = require('crypto');

/* Common internal functions and classes */
const { ignore } = require('../_common/processing');
const { hkdf, hash, zeroise } = require('../_common/crypto');
const { ProcessingError,
        ProtocolError } = require('../_common/errors');
const { hexToBuffer,
        bufferToHex } = require('../_common/encoding');

/* Whiteflag modules */
const wfBlockchains = require('../blockchains');
const wfState = require('./state');

/* Whiteflag configuration data */
const wfConfigData = require('./config').getConfig();

/* Module constants */
const KEYIDLENGTH = 12;
const KEYENCODING = 'hex';
const ECDHCURVE = 'brainpoolP256r1';

/**
 * @constant {Object} cryptoParams
 * @description Defines the Whiteflag encryption parameters as per par 5.2.3 of the WF specification
 */
const cryptoParams = {
    '1': {
        algorithm: 'aes-256-ctr',
        keylength: 32,
        ivlength: 16,
        salt: '8ddb03085a2c15e69c35c224bce2952dca7878770724741cbce5a135328be0c0'
    },
    '2': {
        algorithm: 'aes-256-ctr',
        keylength: 32,
        ivlength: 16,
        salt: 'c4d028bd45c876135e80ef7889835822a6f19a31835557d5854d1334e8497b56'
    }
};
/**
 * @constant {Object} authParams
 * @description Defines the Whiteflag secret authentication token parameters as per par 5.2.3 of the WF specification
 */
const authParams = {
    '2': {
        tokenlength: 32,
        salt: '420abc48f5d69328c457d61725d3fd7af2883cad8460976167e375b9f2c14081'
    }
};

/* MAIN MODULE FUNCTIONS */
/**
 * Encrypts a binary encoded Whiteflag message
 * @function encryptMessage
 * @alias module:lib/protocol/crypto.encrypt
 * @param {wfMessage} wfMessage a Whiteflag message
 * @param {Buffer} encodedMessage an unencrypted binary encoded Whiteflag message
 * @param {wfMessageCb} callback function called on completion
 */
function encryptMessage(wfMessage, encodedMessage, callback) {
    // Cryptograhic parameters
    const { MessageHeader: {
            EncryptionIndicator: method }} =  wfMessage;
    let { MetaHeader: {
            blockchain: blockchain,
            originatorAddress: originator,
            recipientAddress: recipient,
            encryptionKeyInput: messageKey }} =  wfMessage;
    let keyCateory;
    let secretId;

    // Check indicator for encryption type
    switch (method) {
        case '0': {
            delete wfMessage.MetaHeader.recipientAddress;
            wfMessage.MetaHeader.encodedMessage = bufferToHex(encodedMessage);
            return callback(null, wfMessage);
        }
        case '1': {
            // Use negotiated secret as IKM
            keyCateory = 'negotiatedKeys';

            // If key provided, use that one
            if (messageKey) {
                secretId = '0';
                break;
            }
            // Check encryption paramters; we are the originator
            const metaErrors = checkMetaErrors(wfMessage);
            if (metaErrors.length > 0) {
                return callback(new ProcessingError('Missing required parameters for encryption', metaErrors, 'WF_API_BAD_REQUEST'));
            }
            secretId = hash(blockchain + originator + recipient, KEYIDLENGTH);
            break;
        }
        case '2': {
            // Use pre-shared secret as IKM
            keyCateory = 'presharedKeys';

            // If key provided, use that one
            if (messageKey) {
                secretId = '0';
                break;
            }
            // Check encryption parameters; we are the originator
            const metaErrors = checkMetaErrors(wfMessage);
            if (metaErrors.length > 0) {
                return callback(new ProcessingError('Missing required parameters for encryption', metaErrors, 'WF_API_BAD_REQUEST'));
            }
            secretId = hash(blockchain + recipient + originator, KEYIDLENGTH);
            break;
        }
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9': {
            // Encryption/Decryption is reserved
            return callback(new ProtocolError(`Illegal (reserved) encryption method: ${method}`, null, 'WF_ENCRYPTION_ERROR'));
        }
        default: {
            // Encryption/Decryption is not implemented
            return callback(new ProcessingError(`Encryption method not implemented: ${method}`, null, 'WF_API_NOT_IMPLEMENTED'));
        }
    }
    // Get cryptograhic parameters and encryption key
    const iv = genInitVector(method);
    wfState.getKey(keyCateory, secretId, function cryptoEncryptKeyCb(err, stateKey) {
        if (err) return callback(err);
        wfBlockchains.getBinaryAddress(originator, blockchain, function cryptoEncryptAddressCb(err, address) {
            if (err) return callback(err);
            let encryptedMessage = Buffer.alloc(encodedMessage.length);
            try {
                // Get correct ikm
                let ikm = determineKey(messageKey, stateKey, method);
                messageKey = null;
                stateKey = null;

                // Generate key
                let key = generateKey(ikm, address, method);
                zeroise(ikm);

                // Encrypt message
                encrypt(encodedMessage, method, key, iv).copy(encryptedMessage, 0);
                zeroise(key);
                zeroise(encodedMessage);
            } catch(err) {
                if (err instanceof ProcessingError || err instanceof ProtocolError) {
                    return callback(err);
                }
                return callback(new Error(`Encryption error: ${err.message}`));
            }
            // Update message and return result
            wfMessage.MetaHeader.encodedMessage = bufferToHex(encryptedMessage);
            wfMessage.MetaHeader.encryptionInitVector = bufferToHex(iv);
            return callback(null, wfMessage);
        });
    });
}

/**
 * Decrypts a binary encoded encrypted Whiteflag message
 * @function decryptMessage
 * @alias module:lib/protocol/crypto.decrypt
 * @param {wfMessage} wfMessage a Whiteflag message
 * @param {Buffer} encodedMessage a binary encoded and encrypted Whiteflag message
 * @param {wfMessageCb} callback function called on completion
 */
function decryptMessage(wfMessage, encodedMessage, callback) {
    // Cryptograhic parameters
    const { MessageHeader: {
            EncryptionIndicator: method }} =  wfMessage;
    let { MetaHeader: {
            blockchain: blockchain,
            originatorAddress: originator,
            recipientAddress: recipient,
            encryptionKeyInput: messageKey,
            encryptionInitVector: initVector }} =  wfMessage;
    let keyCateory;
    let secretId;

    // Check indicator for encryption type
    switch (method) {
        case '0': {
            delete wfMessage.MetaHeader.recipientAddress;
            return callback(null, encodedMessage);
        }
        case '1': {
            // Use negotiated secret as IKM
            keyCateory = 'negotiatedKeys';

            // If key provided, use that one
            if (messageKey) {
                delete wfMessage.MetaHeader.recipientAddress;
                secretId = '0';
                break;
            }
            // Check encryption parameters; we are the recipient
            let metaErrors = [];
            if (!recipient || recipient === 'unknown') recipient = '';
            if (!blockchain) metaErrors.push('Blockchain not specified in metaheader');
            if (!originator) metaErrors.push('Originator address not specified in metaheader');
            if (metaErrors.length > 0) {
                return callback(new ProcessingError('Missing required parameters for encryption', metaErrors, 'WF_API_BAD_REQUEST'));
            }
            secretId = hash(blockchain + recipient + originator, KEYIDLENGTH);
            break;
        }
        case '2': {
            // Use pre-shared secret as IKM
            keyCateory = 'presharedKeys';

            // If key provided, use that one
            if (messageKey) {
                delete wfMessage.MetaHeader.recipientAddress;
                secretId = '0';
                break;
            }
            // Check encryption parameters; we are the recipient
            let metaErrors = [];
            if (!recipient || recipient === 'unknown') recipient = '';
            if (!blockchain) metaErrors.push('Blockchain not specified in metaheader');
            if (!originator) metaErrors.push('Originator address not specified in metaheader');
            if (metaErrors.length > 0) {
                return callback(new ProcessingError('Missing required parameters for encryption', metaErrors, 'WF_API_BAD_REQUEST'));
            }
            secretId = hash(blockchain + originator + recipient, KEYIDLENGTH);
            break;
        }
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9': {
            // Encryption / Decryption method is reserved
            return callback(new ProtocolError(`Invalid encryption method (reserved): ${method}`, null, 'WF_FORMAT_ERROR'));
        }
        default: {
            // Encryption / Decryption method is not implemented
            return callback(new ProcessingError(`Encryption method not implemented: ${method}`, null, 'WF_API_NOT_IMPLEMENTED'));
        }
    }
    // Check initialisation vector
    if (!initVector) {
        return callback(new ProcessingError('Cannot decrypt message without initialisation vector', null, 'WF_API_BAD_REQUEST'));
    }
    const iv = hexToBuffer(initVector);
    if (iv.length !== cryptoParams[method].ivlength) {
        return callback(new ProcessingError(`Invalid initialisation vector length: ${(iv.length * 8)} bits`, null, 'WF_API_BAD_REQUEST'));
    }
    // Get decryption key
    wfState.getKey(keyCateory, secretId, function cryptoDecryptKeyCb(err, stateKey) {
        if (err) return callback(err);
        wfBlockchains.getBinaryAddress(originator, blockchain, function cryptoDecryptAddressCb(err, address) {
            if (err) return callback(err);
            let decryptedMessage = Buffer.alloc(encodedMessage.length);
            try {
                // Get correct ikm
                let ikm = determineKey(messageKey, stateKey, method);
                messageKey = null;
                stateKey = null;

                // Generate key
                let key = generateKey(ikm, address, method);
                zeroise(ikm);

                // Decrypt message
                decrypt(encodedMessage, method, key, iv).copy(decryptedMessage, 0);
                zeroise(key);
            } catch(err) {
                if (err instanceof ProcessingError || err instanceof ProtocolError) {
                    return callback(err);
                }
                return callback(new Error(`Decryption error: ${err.message}`));
            }
            // Return result
            return callback(null, decryptedMessage);
        });
    });
}

/**
 * Generates verification data from shared secret authentication token
 * @function getTokenVerificationData
 * @alias module:lib/protocol/crypto.getTokenVerificationData
 * @param {Buffer} authToken binary encoded secret authentication token as input key material
 * @param {Buffer} address binary encoded blockchain address by which the token is used
 * @param {genericCb} callback function called on completion
 */
function getTokenVerificationData(authToken, address, callback) {
    // Check input buffers
    if (authToken.length === 0) {
        return callback(new ProcessingError('Zero-length input buffer for authentication token generation', null, 'WF_API_BAD_REQUEST'), null);
    }
    if (address.length === 0) {
        return callback(new ProcessingError('Zero-length info buffer for authentication token generation', null, 'WF_API_BAD_REQUEST'), null);
    }
    // Get authentication method dependent parameters and generate token using HKDF
    const AUTHMETHOD = '2';
    const salt = hexToBuffer(authParams[AUTHMETHOD].salt);
    const tokenlength = authParams[AUTHMETHOD].tokenlength;
    const verificationData = hkdf(authToken, salt, address, tokenlength);
    zeroise(authToken);

    // Return token
    return callback(null, verificationData);
}

/**
 * Generates ECDH key pair and provides the public key
 * @function getECDHpubKey
 * @alias module:lib/protocol/crypto.getECDHpubKey
 * @param {string} id ECDH key pair identifier
 * @param {boolean} newKeyPair replace existing ECDH key pair if true
 * @param {cryptoCDHpublicKeyCb} callback function called on completion
 */
function getECDHpublicKey(id, newKeyPair, callback) {
    // Cryptographic parameters
    const ecdh = crypto.createECDH(ECDHCURVE);
    let publicKey;

    // Check for private key and determine public key
    wfState.getKey('ecdhPrivateKeys', id, function cryptoGetKeyCb(err, privateKey) {
        if (err) return callback(err);
        let newKey;
        try {
            if (!privateKey || newKeyPair) {
                newKey = true;
                publicKey = ecdh.generateKeys(KEYENCODING, 'compressed');
                wfState.upsertKey('ecdhPrivateKeys', id, ecdh.getPrivateKey(KEYENCODING));
            } else {
                newKey = false;
                ecdh.setPrivateKey(privateKey, KEYENCODING);
                publicKey = ecdh.getPublicKey(KEYENCODING, 'compressed');
                privateKey = null;
            }
        } catch(err) {
            return callback(err);
        }
        /**
         * @callback cryptoCDHpublicKeyCb
         * @param {Error} err any error
         * @param {string} publicKey the hexadecimal encoded ECDH public key
         * @param {boolean} newKey true if new keypair generated, else false
         */
        return callback(null, publicKey, newKey);
    });
}

/**
 * Generates a shared secret based on incoming public ECDH key pair and provides the public key
 * @function generateECDHsecret
 * @alias module:lib/protocol/crypto.generateECDHsecret
 * @param {string} id ECDH key pair identifier
 * @param {string} otherPublicKey hexadecimal representation of a received public ECDH key
 * @param {genericCb} callback function called on completion
 */
function generateECDHsecret(id, otherPublicKey, callback) {
    // Cryptographic parameters
    const ecdh = crypto.createECDH(ECDHCURVE);
    let secret;

    // Get private key and calculate secret
    wfState.getKey('ecdhPrivateKeys', id, function cryptoGetKeyCb(err, privateKey) {
        if (err) return callback(err);
        if (!privateKey) return callback(new ProcessingError(`No private ECDH key with id ${id} available`));
        try {
            ecdh.setPrivateKey(privateKey, KEYENCODING);
            secret = ecdh.computeSecret(otherPublicKey, KEYENCODING, KEYENCODING);
        } catch(err) {
            return callback(err);
        }
        return callback(null, secret);
    });
}

/* PRIVATE MODULE FUNCTIONS */
/**
 * Encrypts compressed binary encoded Whiteflag message
 * @private
 * @param {Buffer} unencryptedMessage binary encoded Whiteflag message to be encrypted
 * @param {string} method the Whiteflag encrytpion method
 * @param {Buffer} key binary encoded encryption key
 * @param {Buffer} iv binary encoded initialisation vector
 * @returns {Buffer} binary encoded encrypted Whiteflag message
 */
function encrypt(unencryptedMessage, method, key, iv) {
    // Cryptographic parameters
    const cipher = cryptoParams[method].algorithm;
    const encrypter = crypto.createCipheriv(cipher, key, iv);

    // Perform encryption and return result
    return Buffer.concat([
        unencryptedMessage.subarray(0, 4),
        encrypter.update(unencryptedMessage.subarray(4)),
        encrypter.final()
    ], unencryptedMessage.length);
}

/**
 * Decrypts binary encoded encypted Whiteflag message
 * @private
 * @param {Buffer} encryptedMessage binary encoded encrypted Whiteflag message
 * @param {string} method the Whiteflag encrytpion method
 * @param {Buffer} key binary encoded encryption key
 * @param {Buffer} iv binary encoded initialisation vector
 * @param {Buffer} tag binary encoded message authentication tag
 * @returns {Buffer} binary encoded decrypted Whiteflag message
 */
function decrypt(encryptedMessage, method, key, iv, tag) {
    // Cryptographic parameters
    const cipher = cryptoParams[method].algorithm;
    const decrypter = crypto.createDecipheriv(cipher, key, iv);
    ignore(tag);

    // Perform decryption and return result
    return Buffer.concat([
        encryptedMessage.subarray(0, 4),
        decrypter.update(encryptedMessage.subarray(4)),
        decrypter.final()
    ], encryptedMessage.length);
}

/**
 * Check message metaheader for errors
 * @private
 * @param {wfMessage} wfMessage
 * @returns {Array} message metaheader errors
 */
function checkMetaErrors(wfMessage) {
    let metaErrors = [];
    if (!wfMessage?.MetaHeader?.blockchain) metaErrors.push('Blockchain not specified in metaheader');
    if (!wfMessage?.MetaHeader?.originatorAddress) metaErrors.push('Originator address not specified in metaheader');
    if (!wfMessage?.MetaHeader?.recipientAddress) metaErrors.push('Recipient address not specified in metaheader');
    return metaErrors;
}

/**
 * Generates a random initialisation vector
 * @private
 * @param {number} length initialisation vector length in octets
 * @returns {Buffer} a random initialisation vector
 */
function genInitVector(method) {
    return crypto.randomBytes(cryptoParams[method].ivlength);
}

/**
 * Generates encryption/decryption key from input key material
 * @private
 * @param {Buffer} ikm binary encoded encryption secret as input key material
 * @param {Buffer} address binary encoded blockchain address by which the message is encrypted
 * @param {string} method single character indicating the Whiteflag encryption method
 * @returns {Buffer} encryption key
 */
function generateKey(ikm, address, method) {
    if (ikm.length === 0) {
        throw new ProcessingError('Zero-length key input buffer for encryption key generation', null, 'WF_API_BAD_REQUEST');
    }
    // Get algorithm dependent parameters
    const salt = hexToBuffer(cryptoParams[method].salt);
    const keylength = cryptoParams[method].keylength;

    // Use HKDF to generate and return key
    return hkdf(ikm, salt, address, keylength);
}

/**
 * Determines which encryption key to use
 * @private
 * @param {string} metaheader hexadecimal representation of the secret in the metaheader
 * @param {string} keystore hexadecimal representation of the secret from the key store
 * @param {string} method single character indicating the Whiteflag encryption method
 * @returns {Buffer} input key material
 */
function determineKey(messageKey, stateKey, method) {
    if (messageKey && method === '2') {
        return hexToBuffer(messageKey);
    }
    if (stateKey) {
        return hexToBuffer(stateKey);
    }
    if (method === '2' && wfConfigData.encryption.psk) {
        return hexToBuffer(wfConfigData.encryption.psk);
    }
    switch (method) {
        case '1': {
            throw new ProtocolError('No ECDH encryption key available', null, 'WF_ENCRYPTION_ERROR');
        }
        case '2': {
            throw new ProtocolError('No pre-shared encryption key available', null, 'WF_ENCRYPTION_ERROR');
        }
        default: {
            throw new ProtocolError('No encryption key available', null, 'WF_ENCRYPTION_ERROR');
        }
    }
}