'use strict';
/**
* @module lib/protocol/state
* @summary Whiteflag state module
* @description Module for Whiteflag protocol state management
* @tutorial modules
* @tutorial protocol
* @tutorial state
* @todo review blockchain account backup method
*/
module.exports = {
// State functions
init: initState,
close: closeState,
save: saveState,
// Blockchain state functions
backupAccount,
getBlockchains,
getBlockchainData,
updateBlockchainData,
// Originator state functions
getOriginators,
getOriginatorData,
upsertOriginatorData,
removeOriginatorData,
getOriginatorAuthToken,
removeOriginatorAuthToken,
// Queue function
getQueue,
getQueueData,
upsertQueueData,
removeQueueData,
// Cryptographic keys
getKeyIds,
getKey,
upsertKey,
removeKey,
// State functions for testing
test: {
validate: validateState,
enclose: encloseState,
extract: extractState
}
};
// Module objects //
/**
* Whiteflag state
* @type {Object}
*/
let _wfState = {
blockchains: {},
originators: [],
queue: {
initVectors: [],
blockDepths: []
},
crypto: {
blockchainKeys: [],
ecdhPrivateKeys: [],
presharedKeys: [],
negotiatedKeys: [],
authTokens: []
}
};
// Node.js core and external modules //
const fs = require('fs');
const crypto = require('crypto');
const jsonValidate = require('jsonschema').validate;
// Whiteflag common functions and classes //
const array = require('../common/arrays');
const log = require('../common/logger');
const object = require('../common/objects');
const { hkdf, hash, zeroise } = require('../common/crypto');
const { ProcessingError } = require('../common/errors');
// Whiteflag modules //
const wfApiDatastores = require('../datastores');
// Whiteflag event emitters //
const wfStateEvent = require('./events').stateEvent;
// Whiteflag configuration data //
const wfConfigData = require('./config').getConfig();
// Module constants //
const MODULELOG = 'state';
const MEKLENGTH = 32;
const DEKLENGTH = 32;
const KEKLENGTH = 16;
const DEKSALT = '5dbfb2cc6c0f8fa314fd12b662da1bb2f7bef77f8df7ae0c0fb9d15750cfe423';
const KEKSALT = '22d79745768d144783ba6ce8e7b3047b47651e4729168e8d8741b7d094d46d92';
const DATACIPHER = 'aes-256-gcm';
const DATAIVLENGTH = 12;
const KEYCIPHER = 'aes-128-gcm';
const KEYIVLENGTH = 12;
const KEYIDLENGTH = 12;
const BINENCODING = 'hex';
const STATEENCODING = 'base64';
const wfStateSchema = JSON.parse(fs.readFileSync('./static/protocol/state.schema.json'));
// Module variables //
let _masterEncryptionKey = 'a15e6437e23fdaaed9b454d7d51bd69e6a9ffcdbcf0a7528538a2f3bfe2d54e4';
let _encryption = true;
let _saveToFile = false;
let _stateFile = '/tmp/whiteflag.state';
// MAIN MODULE FUNCTIONS //
/**
* Restores previous state from file and binds event handlers to state changes
* @function initState
* @alias module:lib/protocol/state.init
* @param {function(Error)} callback function called after initialising/restoring state
* @emits module:lib/protocol/events.stateEvent:initialised
*/
function initState(callback) {
// Check state configuration parameters
if (!wfConfigData.state) {
return callback(new Error('State parameters missing in Whiteflag configuration file'));
}
if (wfConfigData.state.masterKey.length !== (MEKLENGTH * 2)) {
return callback(new Error(`Master key in Whiteflag configuration file is not ${(MEKLENGTH * 8)} bits`));
}
// Preserve state configuration
_masterEncryptionKey = wfConfigData.state.masterKey;
if (wfConfigData.state.encryption === false) _encryption = false;
delete wfConfigData.state.masterKey;
// Retrieve state
log.trace(MODULELOG, 'Retrieving previous state from datastore');
wfApiDatastores.getState(function stateReadDatastoreCb(err, stateDbData) {
if (err) return callback(new Error(`Could not retrieve state from datastore: ${err.message}`));
if (!stateDbData) {
log.info(MODULELOG, 'Found no existing state: Starting with clean internal state');
wfStateEvent.emit('initialised');
return callback(null);
}
// Extract and decrypt state
try {
_wfState = extractState(stateDbData);
} catch(err) {
return callback(new Error(`Could not extract state from datastore: ${err.message}`));
}
log.info(MODULELOG, 'Restored previous state from datastore');
// Done restoring state
saveState();
wfStateEvent.emit('initialised');
return callback(null);
});
}
/**
* Ensures a proper shutdown
* @function closeState
* @alias module:lib/protocol/state.closeState
* @param {function(Error)} [callback] function called after initialising/restoring state
* @emits module:lib/protocol/events.stateEvent:closed
*/
function closeState(callback) {
// Closing is triggered when state is saved
wfStateEvent.once('saved', function stateCloseCb() {
if (_saveToFile) log.info(MODULELOG, 'Saved state to file before closing');
else log.info(MODULELOG, 'Saved state to datastore before closing');
_masterEncryptionKey = undefined;
wfStateEvent.emit('closed');
if (callback) return callback(null);
});
// Save state to trigger closing
saveState();
}
/**
* Saves current state to file
* @function saveState
* @alias module:lib/protocol/state.save
* @emits module:lib/protocol/events.stateEvent:saved
*/
function saveState() {
// Enclose and encrypt state object
let stateObject = {};
try {
stateObject = encloseState(_wfState);
} catch(err) {
return log.error(MODULELOG, `Could not save current state: ${err.message}`);
}
// Save state in datastore
log.trace(MODULELOG, 'Saving current state to datastore');
wfApiDatastores.storeState(stateObject, function stateStoreDatastoreCb(err) {
if (err) return log.error(MODULELOG, `Could not store state in datastore: ${err.message}`);
return wfStateEvent.emit('saved');
});
// Save state to file if set in config
if (_saveToFile) {
log.trace(MODULELOG, `Saving current state to ${_stateFile}`);
fs.writeFile(_stateFile, JSON.stringify(stateObject, null, 2), function stateWriteFileCb(err) {
if (err) return log.error(MODULELOG, `Could not save current state to ${_stateFile}: ${err.message}`);
return wfStateEvent.emit('saved');
});
}
}
/**
* Returns blockchain state data to callback
* @function getBlockchains
* @alias module:lib/protocol/state.getBlockchains
* @param {function(Error, blockchainsData)} [callback] function to which data is passed
* @returns {blockchainsData}
* @typedef {Object} blockchainsData state data of all blockchains
*/
function getBlockchains(callback) {
if (callback) return callback(null, _wfState.blockchains);
return _wfState.blockchains;
}
/**
* Gets state of a specific blockchain
* @function getBlockchainData
* @alias module:lib/protocol/state.getBlockchainData
* @param {string} blockchain name of the blockchain
* @param {function(Error, blockchainData)} callback function to which data is passed
* @typedef {Object} blockchainData state data of a blockchain
*/
function getBlockchainData(blockchain, callback) {
if (!Object.prototype.hasOwnProperty.call(_wfState.blockchains, blockchain)) {
return callback(null, null);
}
return callback(null, _wfState.blockchains[blockchain]);
}
/**
* Updates state of a specific blockchain
* @function updateBlockchains
* @alias module:lib/protocol/state.updateBlockchains
* @param {string} blockchain name of the blockchain
* @param {Object} data parameters of the specific blockchain
* @emits module:lib/protocol/events.stateEvent:updatedBlockchain
*/
function updateBlockchainData(blockchain, data) {
if (!Object.prototype.hasOwnProperty.call(_wfState.blockchains, blockchain)) {
log.warn(MODULELOG, `Adding previously unknown blockchain to state: ${blockchain}`);
}
_wfState.blockchains[blockchain] = data;
wfStateEvent.emit('updatedBlockchain', blockchain);
return saveState();
}
/**
* Saves a specific blockchain account to file
* @function backupAccount
* @alias module:lib/protocol/state.backupAccount
* @param {string} blockchain the blockchain of the account to be saved
* @param {string} address the address of the account to be saved
*/
function backupAccount(blockchain, address) {
if (!Object.prototype.hasOwnProperty.call(_wfState.blockchains, blockchain)) {
return log.error(MODULELOG, `Cannot backup account because blockchain does not exist in state: ${blockchain}`);
}
const account = _wfState.blockchains[blockchain].accounts.find(account => account.address === address);
if (account) {
const accountFile = _stateFile + '.' + account.address.substring(0, 8);
fs.writeFile(accountFile, JSON.stringify(account, null, 2), function stateWriteAccountFileCb(err) {
if (err) return log.error(MODULELOG, `Could not save blockchain account to ${accountFile}: ${err.message}`);
return log.debug(MODULELOG, `Blockchain account saved to ${accountFile}`);
});
}
}
/**
* Returns originator state data to callback
* @function getOriginators
* @alias module:lib/protocol/state.getOriginators
* @param {function(Error, originatorsData)} callback function to which data is passed
* @typedef {Array} originatorsData state data of all originators
*/
function getOriginators(callback) {
if (callback) return callback(null, _wfState.originators);
return _wfState.originators;
}
/**
* Gets state of a specific originator
* @function getOriginatorData
* @alias module:lib/protocol/state.getOriginatorData
* @param {string} address the originator's blockchain address
* @param {function(Error, originatorData)} callback function to which data is passed
* @typedef {Object} originatorData state data of all originators
*/
function getOriginatorData(address, callback) {
const index = _wfState.originators.findIndex(
originator => originator.address.toLowerCase() === address.toLowerCase()
);
if (index < 0) return callback(null, null);
return callback(null, _wfState.originators[index]);
}
/**
* Inserts or updates an originator
* @function upsertOriginatorData
* @alias module:lib/protocol/state.upsertOriginatorData
* @param {Object} data originator data
* @emits module:lib/protocol/events.stateEvent:insertedOriginator
* @emits module:lib/protocol/events.stateEvent:updatedOriginator
* @emits module:lib/protocol/events.stateEvent:insertedAuthenticationToken
* @emits module:lib/protocol/events.stateEvent:updatedAuthenticationToken
*/
function upsertOriginatorData(data) {
// Check if originator with the same address or authentication token already exists
let indexA = null;
if (Object.prototype.hasOwnProperty.call(data, 'address')) {
indexA = _wfState.originators.findIndex(
originator => originator.address.toLowerCase() === data.address.toLowerCase()
);
} else {
data.address = '';
}
let indexT = null;
if (Object.prototype.hasOwnProperty.call(data, 'authTokenId')) {
indexT = _wfState.originators.findIndex(
originator => originator.authTokenId === data.authTokenId
);
} else {
data.authTokenId = '';
}
// Error if no address and no authentication token
if (indexA === null && indexT === null) {
return log.error(MODULELOG, 'Attempt to add originator to state without address or authentication token');
}
// Check if not existing originator with address
if (indexA === null || indexA < 0) {
// No address; check if not also existing originator with token
if (indexT === null || indexT < 0) {
// Insert if no originator with token or address
_wfState.originators.push(data);
if (indexT === null) wfStateEvent.emit('insertedOriginator', data.address);
if (indexA === null) wfStateEvent.emit('insertedOriginatorAuthToken', data.authTokenId);
return saveState();
} else {
// Update if existing originator with token but (different) address
if (!_wfState.originators[indexT].address) {
// No address, so update originator with same token
object.update(data, _wfState.originators[indexT]);
wfStateEvent.emit('updatedOriginatorAuthToken', data.authTokenId);
return saveState();
} else {
// Create new originator with same token but different address
_wfState.originators.push(data);
wfStateEvent.emit('insertedOriginator', data.address);
return saveState();
}
}
} else {
// Originator with address
if (indexT === null || indexT < 0) {
// Check if existing originator has different token
if (_wfState.originators[indexA].authTokenId && data.authTokenId &&
_wfState.originators[indexA].authTokenId !== data.authTokenId) {
// Token does not match; create new entry to preserve token
_wfState.originators.push({
name: _wfState.originators[indexA].name,
blockchain: _wfState.originators[indexA].blockchain,
authTokenId: _wfState.originators[indexA].authTokenId
});
}
// Update if existing originator with address but no token or same token
object.update(data, _wfState.originators[indexA]);
wfStateEvent.emit('updatedOriginator', data.address);
return saveState();
}
}
// Remove same originator with no address
if (!_wfState.originators[indexT].address) {
_wfState.originators.splice(indexT, 1);
}
// Update existing originator
object.update(data, _wfState.originators[indexA]);
wfStateEvent.emit('updatedOriginator', data.address);
return saveState();
}
/**
* Removes state of a specific originator
* @function removeOriginatorData
* @alias module:lib/protocol/state.removeOriginatorData
* @param {string} address the originator's blockchain address
* @emits module:lib/protocol/events.stateEvent:removedOriginator
*/
function removeOriginatorData(address) {
const index = _wfState.originators.findIndex(
originator => originator.address.toLowerCase() === address.toLowerCase()
);
if (index >= 0) {
_wfState.originators.splice(index, 1);
wfStateEvent.emit('removedOriginator', address);
return saveState();
}
}
/**
* Returns originator authentication token
* @function getOriginatorAuthToken
* @alias module:lib/protocol/state.getOriginatorAuthToken
* @param {string} authTokenId the originator's authentication token
* @param {function(Error, originatorData)} callback function to which data is passed
*/
function getOriginatorAuthToken(authTokenId, callback) {
const index = _wfState.originators.findIndex(
originator => originator.authTokenId === authTokenId
);
if (index < 0) return callback(null, null);
return callback(null, _wfState.originators[index]);
}
/**
* Removes originator authentication data
* @function removeOriginatorAuthToken
* @alias module:lib/protocol/state.removeOriginatorAuthToken
* @param {string} address the originator's blockchain address
* @emits module:lib/protocol/events.stateEvent:removedAuthenticationToken
* @emits module:lib/protocol/events.stateEvent:removedOriginatorAuthToken
*/
function removeOriginatorAuthToken(authTokenId) {
const index = _wfState.originators.findIndex(
originator => originator.authTokenId === authTokenId
);
if (index >= 0) {
if (!_wfState.originators[index].address) {
_wfState.originators.splice(index, 1);
wfStateEvent.emit('removedOriginator', authTokenId);
} else {
_wfState.originators[index].authTokenId = null;
wfStateEvent.emit('removedAuthenticationToken', _wfState.originators[index].address);
}
}
removeKey('authTokens', authTokenId);
return saveState();
}
/**
* Returns specific queue to callback
* @function getQueue
* @alias module:lib/protocol/state.getQueue
* @param {string} queue the name of the queue
* @param {function(Error, queueData)} callback function to which data is passed
* @typedef {Array} queueData state data from a specific queue
*/
function getQueue(queue, callback) {
if (!Object.prototype.hasOwnProperty.call(_wfState.queue, queue)) {
return callback(new ProcessingError(`Queue ${queue} does not exist`, null, 'WF_API_NO_RESOURCE'));
}
return callback(null, _wfState.queue[queue]);
}
/**
* Gets specific item from specific queue
* @function getQueueData
* @alias module:lib/protocol/state.getQueueData
* @param {string} queue the name of the queue
* @param {string} property the data object property name by which the item is identified
* @param {Object} value the value of the property by which the item is identified
* @param {function(Error, Object)} callback function to which data is passed
*/
function getQueueData(queue, property, value, callback) {
if (!Object.prototype.hasOwnProperty.call(_wfState.queue, queue)) {
return callback(new Error(`Queue ${queue} does not exist`));
}
const index = _wfState.queue[queue].findIndex(
item => item[property].toLowerCase() === value.toLowerCase()
);
if (index < 0) return callback(null, null);
return callback(null, _wfState.queue[queue][index]);
}
/**
* Inserts or updates specific item in specific queue
* @function upsertQueueData
* @alias module:lib/protocol/state.upsertQueueData
* @param {string} queue the name of the queue
* @param {string} property the data object property name by which the item is identified
* @param {Object} data the data to be updated or put on queue
* @emits module:lib/protocol/events.stateEvent:insertedInQueue
* @emits module:lib/protocol/events.stateEvent:updatedQueue
*/
function upsertQueueData(queue, property, data) {
const index = _wfState.queue[queue].findIndex(
item => item[property].toLowerCase() === data[property].toLowerCase()
);
if (index < 0) {
_wfState.queue[queue].push(data);
wfStateEvent.emit('insertedInQueue', queue, data);
} else {
_wfState.queue[queue][index] = data;
wfStateEvent.emit('updatedQueue', queue, data);
}
return saveState();
}
/**
* Removes specific item in specific queue
* @function removeQueueData
* @alias module:lib/protocol/state.removeQueueData
* @param {string} queue the name of the queue
* @param {string} property the data object property name by which the item is identified
* @param {Object} value the value of the property by which the item to be removed is identified
* @emits module:lib/protocol/events.stateEvent:removedFromQueue
*/
function removeQueueData(queue, property, value) {
const index = _wfState.queue[queue].findIndex(
item => item[property].toLowerCase() === value.toLowerCase()
);
if (index >= 0) {
const data = _wfState.queue[queue][index];
_wfState.queue[queue].splice(index, 1);
wfStateEvent.emit('removedFromQueue', queue, data);
return saveState();
}
}
/**
* Returns all key ids of a cryptographic key category
* @function getKeyIds
* @alias module:lib/protocol/state.getKeyIds
* @param {string} category the key category
* @param {function(Error, Array)} callback function to which the key is passed
*/
function getKeyIds(category, callback) {
if (!Object.prototype.hasOwnProperty.call(_wfState.crypto, category)) {
return callback(new Error(`Key category ${category} does not exist`));
}
return callback(null, array.pluck(_wfState.crypto[category], 'id'));
}
/**
* Gets a cryptographic key
* @function getKey
* @alias module:lib/protocol/state.getKey
* @param {string} category the key category
* @param {string} id unique identifier of the key
* @param {function(Error, key)} callback function to which the key is passed
* @typedef {string} key hexadecimal representation of a cryptographic key
*/
function getKey(category, id, callback) {
if (!Object.prototype.hasOwnProperty.call(_wfState.crypto, category)) {
return callback(new Error(`Key category ${category} does not exist`));
}
const index = _wfState.crypto[category].findIndex(
item => item.id.toLowerCase() === id.toLowerCase()
);
if (index < 0) return callback(null, null);
let key = decryptKey(_wfState.crypto[category][index].secret, id);
return callback(null, key);
}
/**
* Inserts or updates a cryptographic key
* @function upsertKey
* @alias module:lib/protocol/state.upsertKey
* @param {string} category the key category
* @param {string} id unique identifier of the key
* @param {string} key the key to be stored or updated in raw hexadecimal format
* @emits module:lib/protocol/events.stateEvent:insertedKey
* @emits module:lib/protocol/events.stateEvent:updatedKey
*/
function upsertKey(category, id, key) {
const index = _wfState.crypto[category].findIndex(
item => item.id.toLowerCase() === id.toLowerCase()
);
const data = encryptKey(key, id);
// Hopefully the garbage collector will do its work
key = undefined;
// Upsert the encrypted key
if (index < 0) {
_wfState.crypto[category].push({
id: id,
secret: data
});
wfStateEvent.emit('insertedKey', category, id);
} else {
_wfState.crypto[category][index] = {
id: id,
secret: data
};
wfStateEvent.emit('updatedKey', category, id);
}
return saveState();
}
/**
* Removes a cryptographic key
* @function removeKey
* @alias module:lib/protocol/state.removeKey
* @param {string} category the key category
* @param {string} id unique identifier of the key
* @emits module:lib/protocol/events.stateEvent:removedKey
*/
function removeKey(category, id) {
const index = _wfState.crypto[category].findIndex(
item => item.id.toLowerCase() === id.toLowerCase()
);
if (index >= 0) {
_wfState.crypto[category].splice(index, 1);
wfStateEvent.emit('removedKey', category, id);
return saveState();
}
}
// PRIVATE MODULE FUNCTIONS //
/**
* Validates the state structure configuration against the state schema
* @private
* @param {Object} [stateData] the complete state
* @returns {Array} validation errors, empty if no errors
*/
function validateState(stateData = _wfState) {
let stateErrors = [];
try {
stateErrors = array.pluck(jsonValidate(stateData, wfStateSchema).errors, 'stack');
} catch(err) {
stateErrors.push(err.message);
}
return stateErrors;
}
/**
* Encloses (and encrypts) state
* @private
* @param {Object} [stateData] the complete state
* @returns {Object} state data enclosed in a storage / encryption container
*/
function encloseState(stateData = _wfState) {
let stateObject = {};
if (_encryption) {
stateObject = encryptState(JSON.stringify(stateData));
} else {
stateObject = { state: JSON.stringify(stateData) };
}
return stateObject;
}
/**
* Extracts (and decrypts) state from file or datastore contents
* @private
* @param {Object} stateObject state data enclosed in a storage / encryption container
* @returns {Object} the complete state
*/
function extractState(stateObject) {
let stateDataStr;
if (!Object.prototype.hasOwnProperty.call(stateObject, 'state')) {
throw new Error('State object might be corrupted: state property is missing');
}
if (
Object.prototype.hasOwnProperty.call(stateObject, 'tag')
&& Object.prototype.hasOwnProperty.call(stateObject, 'iv')
) {
stateDataStr = decryptState(stateObject);
} else {
stateDataStr = stateObject.state;
}
return checkState(JSON.parse(stateDataStr));
}
/**
* Checks state and performs updates from old versions
* @param {Object} stateData
* @returns {Object} updated state
*/
function checkState(stateData) {
// Check for complete object structure and add new properties as required
stateData =
checkStateDataOriginators(
checkStateDataBlockchains(
checkStateDataStructure(stateData)
)
);
// Finally, validate against schema
let stateErrors = validateState();
if (stateErrors.length > 0) throw new Error('State does not validate against schema: ' + JSON.stringify(stateErrors));
return stateData;
}
/**
* Encrypts state
* @private
* @param {string} stateDataStr stringified state data
* @returns {Object} encrypted state with authentication tag and initialisation vector
*/
function encryptState(stateDataStr) {
// Initialise encrypter object
const ivBuffer = crypto.randomBytes(DATAIVLENGTH);
const dekBuffer = generateDEK();
if (!dekBuffer) {
throw new Error('Could not generate DEK to encrypt state: Invalid master key?');
}
const encrypter = crypto.createCipheriv(DATACIPHER, dekBuffer, ivBuffer);
// Perform encrytption
const encryptedState = encrypter.update(stateDataStr, '', STATEENCODING) + encrypter.final(STATEENCODING);
const tag = encrypter.getAuthTag().toString(BINENCODING);
zeroise(dekBuffer);
// Return encryption result
return {
tag: tag,
iv: ivBuffer.toString(BINENCODING),
state: encryptedState
};
}
/**
* Decrypts state
* @private
* @param {Object} stateObject encrypted state with authentication tag and initialisation vector
* @returns {string} stringified state data
*/
function decryptState(stateObject) {
// Check data
if (!stateObject.iv || !stateObject.tag) {
throw new Error('Cannot decrypt: Missing tag and/or initialisation vector');
}
// Get information from data object
const ivBuffer = Buffer.from(stateObject.iv, BINENCODING);
const tagBuffer = Buffer.from(stateObject.tag, BINENCODING);
const stateBuffer = Buffer.from(stateObject.state, STATEENCODING);
// Initialise decrypter object
const dekBuffer = generateDEK();
if (!dekBuffer) {
throw new Error('Could not generate DEK to decrypt state: Invalid master key?');
}
const decrypter = crypto.createDecipheriv(DATACIPHER, dekBuffer, ivBuffer);
// Perform decryption and return result
decrypter.setAuthTag(tagBuffer);
const stateDataStr = Buffer.concat([decrypter.update(stateBuffer), decrypter.final()]);
zeroise(dekBuffer);
// Return decryption result
return stateDataStr;
}
/**
* Encrypts key data
* @private
* @param {string} keyStr key in hexadecimal string
* @param {string} id unique identifier for the key, usually a related blockchain address
* @returns {Object} encrypted key with authentication tag and initialisation vector
*/
function encryptKey(keyStr, id) {
// Initialise encrypter object
const ivBuffer = crypto.randomBytes(KEYIVLENGTH);
const kekBuffer = generateKEK(id);
if (!kekBuffer) {
throw new Error('Could not generate KEK to encrypt key: Invalid master key?');
}
const encrypter = crypto.createCipheriv(KEYCIPHER, kekBuffer, ivBuffer);
// Perform encrytption
const encryptedKey = encrypter.update(keyStr, '', BINENCODING) + encrypter.final(BINENCODING);
const tag = encrypter.getAuthTag().toString(BINENCODING);
// Hopefully the garbage collector will do its work
keyStr = undefined;
zeroise(kekBuffer);
// Return encryption result
return {
tag: tag,
iv: ivBuffer.toString(BINENCODING),
key: encryptedKey
};
}
/**
* Decrypts key data
* @private
* @param {Object} stateObject encrypted key with authentication tag and initialisation vector
* @param {string} id unique identifier for the key, usually a related blockchain address
* @returns {string} key in hexadecimal string
*/
function decryptKey(keyObject, id) {
// Check data
if (!keyObject.iv || !keyObject.tag) {
throw new Error('Cannot decrypt: Missing tag and/or initialisation vector');
}
// Get information from data object
const ivBuffer = Buffer.from(keyObject.iv, BINENCODING);
const tagBuffer = Buffer.from(keyObject.tag, BINENCODING);
const keyBuffer = Buffer.from(keyObject.key, BINENCODING);
// Initialise decrypter object
const kekBuffer = generateKEK(id);
if (!kekBuffer) {
throw new Error('Could not generate KEK to decrypt key: Invalid master key?');
}
const decrypter = crypto.createDecipheriv(KEYCIPHER, kekBuffer, ivBuffer);
// Perform decryption and return result
decrypter.setAuthTag(tagBuffer);
const keyStr = Buffer.concat([decrypter.update(keyBuffer), decrypter.final()]).toString();
zeroise(kekBuffer);
// Return decryption result
return keyStr;
}
/**
* Generates data encryption key from master key using HKDF
* @private
* @returns {buffer} the encryption key
*/
function generateDEK() {
const ikm = Buffer.from(_masterEncryptionKey, BINENCODING);
if (ikm.length === 0) return null;
const salt = Buffer.from(DEKSALT, BINENCODING);
const info = Buffer.from('DEK-00');
return hkdf(ikm, salt, info, DEKLENGTH);
}
/**
* Generates key encryption key from master key using HKDF
* @private
* @returns {buffer} the encryption key
*/
function generateKEK(id) {
const ikm = Buffer.from(_masterEncryptionKey, BINENCODING);
if (ikm.length === 0) return null;
const salt = Buffer.from(KEKSALT, BINENCODING);
const info = Buffer.from('KEK-' + id);
return hkdf(ikm, salt, info, KEKLENGTH);
}
// STATE STRUCTURE CORRECTIONS //
/**
* Check for complete object structure and add new properties as required
* @private
* @param {Object} stateData the state data object
* @returns {Object} the checked and corrected state data object
*/
function checkStateDataStructure(stateData) {
if (!Object.prototype.hasOwnProperty.call(stateData, 'blockchains')) stateData.blockchains = {};
if (!Object.prototype.hasOwnProperty.call(stateData, 'originators')) stateData.originators = [];
if (!Object.prototype.hasOwnProperty.call(stateData, 'queue')) stateData.queue = {};
if (!Object.prototype.hasOwnProperty.call(stateData.queue, 'initVectors')) stateData.queue.initVectors = [];
if (!Object.prototype.hasOwnProperty.call(stateData.queue, 'blockDepths')) stateData.queue.blockDepths = [];
if (!Object.prototype.hasOwnProperty.call(stateData, 'crypto')) stateData.crypto = {};
if (!Object.prototype.hasOwnProperty.call(stateData.crypto, 'blockchainKeys')) stateData.crypto.blockchainKeys = [];
if (!Object.prototype.hasOwnProperty.call(stateData.crypto, 'ecdhPrivateKeys')) stateData.crypto.ecdhPrivateKeys = [];
if (!Object.prototype.hasOwnProperty.call(stateData.crypto, 'negotiatedKeys')) stateData.crypto.negotiatedKeys = [];
if (!Object.prototype.hasOwnProperty.call(stateData.crypto, 'presharedKeys')) stateData.crypto.presharedKeys = [];
if (!Object.prototype.hasOwnProperty.call(stateData.crypto, 'authTokens')) stateData.crypto.authTokens = [];
return stateData;
}
/**
* Checks and corrects blockchain state data
* @private
* @param {Object} stateData the state data object
* @returns {Object} the checked and corrected state data object
*/
function checkStateDataBlockchains(stateData) {
// Check blockchains
for (const blockchain in stateData.blockchains) {
// Check blockchain accounts
if (Object.prototype.hasOwnProperty.call(stateData.blockchains[blockchain], 'accounts')) {
let accounts = stateData.blockchains[blockchain].accounts;
for (const index in accounts) {
// Remove invalid account entry with a non-string address
if (Object.prototype.hasOwnProperty.call(accounts[index], 'address')) {
if (typeof accounts[index].address !== 'string') {
accounts.splice(index, 1);
continue;
}
}
// Move naked private keys in acoount to keystore
stateData = upgradeStateDataKeyStorage(stateData, blockchain, accounts[index]);
}
}
}
return stateData;
}
/**
* Upgrades state by putting naked private keys in keystore
* @private
* @param {Object} stateData the state data object
* @param {string} blockchain the blockchain name
* @param {Object} account the account data
* @returns {Object} the checked and corrected state data object
*/
function upgradeStateDataKeyStorage(stateData, blockchain, account) {
if (Object.prototype.hasOwnProperty.call(account, 'privateKey')) {
const id = hash(blockchain + account.address, KEYIDLENGTH);
const index = stateData.crypto.blockchainKeys.findIndex(
item => item.id.toLowerCase() === id.toLowerCase()
);
const data = encryptKey(account.privateKey, id);
if (index < 0) {
stateData.crypto.blockchainKeys.push({
id: id,
secret: data
});
} else {
stateData.crypto.blockchainKeys[index] = {
id: id,
secret: data
};
}
account.privateKey = undefined;
}
return stateData;
}
/**
* Checks and corrects originator state data
* @private
* @param {Object} stateData the state data object
* @returns {Object} the checked and corrected state data object
*/
function checkStateDataOriginators(stateData) {
// Checks for unique values in originator authentication messages
stateData.originators.forEach(originator => {
if (Array.isArray(originator.authenticationMessages)) {
originator.authenticationMessages = [...new Set(originator.authenticationMessages)];
}
});
return stateData;
}