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 = {
    // Crypographic functions
    encrypt: encryptMessage,
    decrypt: decryptMessage,
    getTokenVerificationData,
    getECDHpublicKey,
    generateECDHsecret,
    // Crypographic functions for testing
    test: {
        genInitVector,
        generateKey,
        encrypt,
        decrypt
    }
};

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

// Whiteflag common functions and classes //
const { ignore } = require('../common/processing');
const { hkdf, hash, zeroise } = require('../common/crypto');
const { ProcessingError, ProtocolError } = require('../common/errors');

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

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

// Module constants //
const KEYIDLENGTH = 12;
const BINENCODING = 'hex';
const ECDHCURVE = 'brainpoolP256r1';
/**
 * @constant {Object} encryptionParameters
 * @description Defines the Whiteflag encryption parameters as per par 5.2.3 of the WF specification
 */
const encryptionParameters = {
    '1': {
        algorithm: 'aes-256-ctr',
        keylength: 32,
        ivlength: 16,
        salt: '8ddb03085a2c15e69c35c224bce2952dca7878770724741cbce5a135328be0c0'
    },
    '2': {
        algorithm: 'aes-256-ctr',
        keylength: 32,
        ivlength: 16,
        salt: 'c4d028bd45c876135e80ef7889835822a6f19a31835557d5854d1334e8497b56'
    }
};
/**
 * @constant {Object} authenticationParameters
 * @description Defines the Whiteflag secret authentication token parameters as per par 5.2.3 of the WF specification
 */
const authenticationParameters = {
    '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 {function(Error, wfMessage)} callback function to be called upon completion
 */
function encryptMessage(wfMessage, encodedMessage, callback) {
    // Cryptograhic parameters
    let blockchain = wfMessage.MetaHeader.blockchain;
    let originator = wfMessage.MetaHeader.originatorAddress;
    let recipient = wfMessage.MetaHeader.recipientAddress;
    let messageKey = wfMessage.MetaHeader.encryptionKeyInput;
    let keyCateory;
    let secretId;

    // Check indicator for encryption type
    let method = wfMessage.MessageHeader.EncryptionIndicator;
    switch (method) {
        case '0': {
            recipient = undefined;
            wfMessage.MetaHeader.encodedMessage = encodedMessage.toString(BINENCODING);
            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
            let metaheaderErrors = [];
            if (!blockchain) metaheaderErrors.push('Blockchain not specified in metaheader');
            if (!originator) metaheaderErrors.push('Originator address not specified in metaheader');
            if (!recipient) metaheaderErrors.push('Recipient address not specified in metaheader');
            if (metaheaderErrors.length > 0) {
                return callback(new ProcessingError('Missing required parameters for encryption', metaheaderErrors, '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
            let metaheaderErrors = [];
            if (!blockchain) metaheaderErrors.push('Blockchain not specified in metaheader');
            if (!originator) metaheaderErrors.push('Originator address not specified in metaheader');
            if (!recipient) metaheaderErrors.push('Recipient address not specified in metaheader');
            if (metaheaderErrors.length > 0) {
                return callback(new ProcessingError('Missing required parameters for encryption', metaheaderErrors, '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
    const iv = genInitVector(method);

    // Get decryption key
    wfState.getKey(keyCateory, secretId, function cryptoDecryptKeyCb(err, stateKey) {
        if (err) return callback(err);

        wfApiBlockchains.getBinaryAddress(originator, blockchain, function cryptoDecryptAddressCb(err, address) {
            if (err) return callback(err);

            // Derrive key and encrypt
            let encryptedMessage = Buffer.alloc(encodedMessage.length);
            try {
                // Get correct ikm
                let ikm = determineEncryptionKey(messageKey, stateKey, method);
                messageKey = undefined;
                stateKey = undefined;

                // 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 = encryptedMessage.toString(BINENCODING);
            wfMessage.MetaHeader.encryptionInitVector = iv.toString(BINENCODING);
            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 {function(Error, wfMessage)} callback function to be called upon completion
 */
function decryptMessage(wfMessage, encodedMessage, callback) {
    // Cryptograhic parameters
    let blockchain = wfMessage.MetaHeader.blockchain;
    let originator = wfMessage.MetaHeader.originatorAddress;
    let recipient = wfMessage.MetaHeader.recipientAddress;
    let messageKey = wfMessage.MetaHeader.encryptionKeyInput;
    let keyCateory;
    let secretId;

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

            // If key provided, use that one
            if (messageKey) {
                wfMessage.MetaHeader.recipientAddress = undefined;
                secretId = '0';
                break;
            }
            // Check encryption parameters; we are the recipient
            let metaheaderErrors = [];
            if (!recipient || recipient === 'unknown') recipient = '';
            if (!blockchain) metaheaderErrors.push('Blockchain not specified in metaheader');
            if (!originator) metaheaderErrors.push('Originator address not specified in metaheader');
            if (metaheaderErrors.length > 0) {
                return callback(new ProcessingError('Missing required parameters for encryption', metaheaderErrors, '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) {
                wfMessage.MetaHeader.recipientAddress = undefined;
                secretId = '0';
                break;
            }
            // Check encryption parameters; we are the recipient
            let metaheaderErrors = [];
            if (!recipient || recipient === 'unknown') recipient = '';
            if (!blockchain) metaheaderErrors.push('Blockchain not specified in metaheader');
            if (!originator) metaheaderErrors.push('Originator address not specified in metaheader');
            if (metaheaderErrors.length > 0) {
                return callback(new ProcessingError('Missing required parameters for encryption', metaheaderErrors, '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 (!wfMessage.MetaHeader.encryptionInitVector) {
        return callback(new ProcessingError('Cannot decrypt message without initialisation vector', null, 'WF_API_BAD_REQUEST'));
    }
    const iv = Buffer.from(wfMessage.MetaHeader.encryptionInitVector, BINENCODING);
    if (iv.length !== encryptionParameters[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);

        wfApiBlockchains.getBinaryAddress(originator, blockchain, function cryptoDecryptAddressCb(err, address) {
            if (err) return callback(err);

            // Derrive key and decrypt
            let decryptedMessage = Buffer.alloc(encodedMessage.length);
            try {
                // Get correct ikm
                let ikm = determineDecryptionKey(messageKey, stateKey, method);
                messageKey = undefined;
                stateKey = undefined;

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

                // Encrypt 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 {function(Error, verificationData)} callback function to be called upon completion
 * @typedef {Buffer} verificationData authentication token verification data
 */
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 tokenlength = authenticationParameters[AUTHMETHOD].tokenlength;
    const salt = Buffer.from(authenticationParameters[AUTHMETHOD].salt, BINENCODING);
    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 {function(Error, publicKey, newKey)} callback function to be called upon completion
 * @typedef {string} publicKey hexadecimal representation of the ECDH public key
 * @typedef {boolean} newKey indicates whether a new ECDH key pair has been generated
 */
function getECDHpublicKey(id, newKeyPair, callback) {
    const ecdh = crypto.createECDH(ECDHCURVE);
    let publicKey;

    wfState.getKey('ecdhPrivateKeys', id, function cryptoGetKeyCb(err, privateKey) {
        if (err) return callback(err);
        let newKey;
        try {
            if (!privateKey || newKeyPair) {
                newKey = true;
                publicKey = ecdh.generateKeys(BINENCODING, 'compressed');
                wfState.upsertKey('ecdhPrivateKeys', id, ecdh.getPrivateKey(BINENCODING));
            } else {
                newKey = false;
                ecdh.setPrivateKey(privateKey, BINENCODING);
                publicKey = ecdh.getPublicKey(BINENCODING, 'compressed');
                privateKey = undefined;
            }
        } catch(err) {
            return callback(err);
        }
        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 {function(Error, secret)} callback function to be called upon completion
 * @typedef {string} secret hexadecimal representation of computed shared secret
 */
function generateECDHsecret(id, otherPublicKey, callback) {
    const ecdh = crypto.createECDH(ECDHCURVE);
    let 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, BINENCODING);
            secret = ecdh.computeSecret(otherPublicKey, BINENCODING, BINENCODING);
        } 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 = encryptionParameters[method].algorithm;
    const encrypter = crypto.createCipheriv(cipher, key, iv);

    // Perform encryption and return result
    return Buffer.concat([
        unencryptedMessage.slice(0, 4),
        encrypter.update(unencryptedMessage.slice(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 = encryptionParameters[method].algorithm;
    const decrypter = crypto.createDecipheriv(cipher, key, iv);
    ignore(tag);

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

/**
 * 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(encryptionParameters[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 keylength = encryptionParameters[method].keylength;
    const salt = Buffer.from(encryptionParameters[method].salt, BINENCODING);

    // 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 determineEncryptionKey(messageKey, stateKey, method) {
    if (messageKey && method === '2') {
        return Buffer.from(messageKey, BINENCODING);
    }
    if (stateKey) {
        return Buffer.from(stateKey, BINENCODING);
    }
    if (method === '2' && wfConfigData.encryption.psk) {
        return Buffer.from(wfConfigData.encryption.psk, BINENCODING);
    }
    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');
        }
    }
}

/**
 * Determines which decryption 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 determineDecryptionKey(messageKey, stateKey, method) {
    if (messageKey && method === '2') {
        return Buffer.from(messageKey, BINENCODING);
    }
    if (stateKey) {
        return Buffer.from(stateKey, BINENCODING);
    }
    switch (method) {
        case '1': {
            throw new ProtocolError('No ECDH decryption key available', null, 'WF_ENCRYPTION_ERROR');
        }
        case '2': {
            throw new ProtocolError('No pre-shared decryption key available', null, 'WF_ENCRYPTION_ERROR');
        }
        default: {
            throw new ProtocolError('No decryption key available', null, 'WF_ENCRYPTION_ERROR');
        }
    }
}