'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');
}
}
}