@bynorth/focals-client-js
Version:
Client library used to integrate with the Focals Developer APIs
172 lines (144 loc) • 5.52 kB
JavaScript
;
const jose = require('node-jose');
const NodeRSA = require('node-rsa');
const jsonpointer = require('jsonpointer');
const jsonrefs = require('json-refs');
const Err = require('./util/error.js');
const config = require('./util/config.js').options;
const wrapperVersion = '2.0.0';
const convertPublicKeys = async (publicKeys) => {
const keystore = jose.JWK.createKeyStore();
const convertedKeys = [];
for (let publicKey of publicKeys) {
if (publicKey.startsWith('-----BEGIN RSA PUBLIC KEY----')) {
const key = new NodeRSA();
key.importKey(publicKey, 'pkcs1-public');
publicKey = key.exportKey('pkcs8-public');
}
const convertedKey = await keystore.add(publicKey, 'pem');
convertedKeys.push(convertedKey);
}
return convertedKeys;
};
const getReplacedObject = (input) => {
let inputString = JSON.stringify(input);
inputString = inputString.replace(/\$ref/g, '$$$$ref');
return JSON.parse(inputString);
};
const getEncryptedPaths = (inputObject, pathsToEncrypt) => {
const valuesToEncrypt = [];
let encryptedFieldCount = 0;
for (const pathIndex in pathsToEncrypt) {
const path = pathsToEncrypt[pathIndex];
const value = jsonpointer.get(inputObject, path);
if (!value) {
throw Err.encryptionBadPath(`A path to encrypt was provided that could not be found: ${path}`);
}
valuesToEncrypt.push(value);
jsonpointer.set(inputObject, path, { $ref: `#/encrypted/${encryptedFieldCount}` });
encryptedFieldCount++;
}
return valuesToEncrypt;
};
const getFinalEncryptedPacket = async (inputObject, valuesToEncrypt, publicKeys) => {
const jsonStringToEncrypt = JSON.stringify(valuesToEncrypt);
const encrypted = await jose.JWE.createEncrypt(
{
protect: false,
contentAlg: 'A256CBC-HS512'
}, publicKeys).update(jsonStringToEncrypt).final();
return {
version: wrapperVersion,
plain: inputObject,
encrypted
};
};
const decryptValues = async (input) => {
const keystore = jose.JWK.createKeyStore();
const key = await keystore.add(config.privateKey, 'pem');
const decryptKey = jose.JWE.createDecrypt(key);
const decrypted = await decryptKey.decrypt(input.encrypted);
return JSON.parse(decrypted.plaintext);
};
const resolveObject = async (input) => {
const resolvedRefs = await jsonrefs.resolveRefs(input, {
filter: ['local']
});
const originalObject = resolvedRefs.resolved.plain;
// Unescape any escaped $refs from the original plaintext
let plainString = JSON.stringify(originalObject);
plainString = plainString.replace(/\$\$ref/g, '$$ref');
return JSON.parse(plainString);
};
/** Unflattens a flattened JWE by moving top-level parameters into a recipients object
* Has no effect on a non-flattened JWE
* @param {object} input - The JWE. Will be modified in-place
*/
const unflatten = (input) => {
if (input.encrypted.recipients) {
return input;
}
input.encrypted.recipients = [
{
encrypted_key: input.encrypted.encrypted_key,
header: input.encrypted.header,
}
]; // TODO, handle unprotected as well?
delete input.encrypted.encrypted_key;
delete input.encrypted.header;
return input;
};
const validateEncryptionData = (input, publicKeys) => {
if (!publicKeys || publicKeys.length == 0) {
throw Err.publicKeys();
}
if (!input) {
throw Err.encryptionBadInput();
}
};
const validateDecryptionData = (input) => {
if (!input) {
throw Err.encryptionBadInput();
}
if (!config.privateKey) {
throw Err.privateKey();
}
};
/**
* Provides functionality to encrypt and decrypt packets
* @module EncryptionService
*/
const EncryptionService = {
/**
* Encrypts the provided paths on a packet using the provided public keys.
*
* @param {object} input - The packet object containing the values to encrypt
* @param {string[]} pathsToEncrypt - An array of paths on the input object to encrypt
* @param {string[]} publicKeys - An array of string values of the public keys to use for encryption
*
* @returns {object} The modified packet input with encrypted values
* @throws Will throw an error if invalid input is provided, or a path to encrypt cannot be found
*/
encryptPacket: async (input, pathsToEncrypt, publicKeys) => {
validateEncryptionData(input, publicKeys);
const inputObject = getReplacedObject(input);
const valuesToEncrypt = getEncryptedPaths(inputObject, pathsToEncrypt);
const convertedKeys = await convertPublicKeys(publicKeys);
return getFinalEncryptedPacket(inputObject, valuesToEncrypt, convertedKeys);
},
/**
* Decrypts the provided packet using the configured private key.
*
* @param {object} input - The encrypted packet object
*
* @returns {object} The decrypted packet
* @throws Will throw an error if invalid input is provided, or if the private key has not been configured during init
*/
decryptPacket: async (input) => {
validateDecryptionData(input);
unflatten(input);
input.encrypted = await decryptValues(input);
return resolveObject(input);
}
};
module.exports = EncryptionService;