src/cascade.js
/**
* cascade.js
*/
import {Jscu} from './suite_jscu.js';
import {generateKeyObject, importKeys, Keys} from './keys.js';
import {Signature} from './signature.js';
import {CascadedData, createCascadedData} from './cascaded_data.js';
import * as core from './core.js';
import cloneDeep from 'lodash.clonedeep';//'lodash/cloneDeep';
export const createEncryptionCascade = async ({keys, procedure}) => {
const localKeys = cloneDeep(keys);
const localProcedure = procedure.map( (x) => cloneDeep(x));
const cascade = new Cascade();
cascade._init({mode: 'encrypt', keys: localKeys, procedure: localProcedure});
await cascade._initEncryptionProcedure();
return cascade;
};
export const createDecryptionCascade = ({keys, encrypted}) => {
const localKeys = cloneDeep(keys);
const cascade = new Cascade();
cascade._init({mode: 'decrypt', keys: localKeys, encrypted});
cascade._initDecryptionProcedure();
return cascade;
};
////////////////////
const modes = ['encrypt', 'decrypt'];
class Cascade extends Array {
_init({mode, keys, procedure, encrypted}){
// assertions
if (modes.indexOf(mode) < 0) throw new Error('InvalidMode');
if (!(keys instanceof Keys)) throw new Error('NotKeyObject');
if (keys.mode.indexOf(mode) < 0) throw new Error('UnmatchedKeyMode');
this._cascadeMode = mode;
this._orgKeys = keys;
if (mode === 'encrypt') {
if (!(procedure instanceof Array)) throw new Error('NotArrayProcedure');
const initial = procedure.map( (config) => {
if(typeof config.encrypt === 'undefined') throw new Error('InvalidProcedure');
return {config};
});
this.push(...initial);
}
if (mode === 'decrypt') {
if (!(encrypted instanceof CascadedData)) throw new Error('NotCascadedEncryptedData');
const initial = encrypted.map( (encryptedObject) => {
if(typeof encryptedObject.message === 'undefined') throw new Error('InvalidEncryptedMessage');
return {data: encryptedObject};
});
this.push(...initial);
}
// set original key to the final step in the procedure
this[this.length - 1].keys = this._orgKeys;
}
async _initEncryptionProcedure(){
// export signingKey for precedence
const signingKeys = this._orgKeys.keys.privateKeys;
const precedence = this.slice(0, this.length -1);
await Promise.all(precedence.map( async (proc, idx) => {
if (typeof proc.config.encrypt.externalKey === 'undefined' || proc.config.encrypt.externalKey) {
throw new Error('PrecedenceMustBeExternalKey');
}
if (typeof proc.config.encrypt.onetimeKey === 'undefined') throw new Error('NoKeyParamsGiven');
const suiteObject = {encrypt_decrypt: proc.config.encrypt.suite};
const modeArray = ['encrypt'];
// key generation for encryption at this step
const keyParams = Object.assign({ suite: proc.config.encrypt.suite}, proc.config.encrypt.onetimeKey);
delete proc.config.encrypt.onetimeKey;
const onetimeKey = await generateKeyObject(keyParams); // generate keys
const onetimeKeyObject = (keyParams.keyParams.type === 'session')
? {sessionKey: onetimeKey.key}
: {publicKeys: [onetimeKey.publicKey]};
// message for encryption at next step.
// [NOTE] message for the first step is directly given message to be encrypted, otherwise, previous private/session keys;
let nextStepMessage;
if (keyParams.keyParams.type === 'session') nextStepMessage = onetimeKey.key;
else {
if (keyParams.suite === 'jscu') nextStepMessage = await onetimeKey.privateKey.export('der');
else throw new Error('UnknownSuite');
}
this[idx+1].message = nextStepMessage;
// updated config and key object for signing and key import
if (typeof proc.config.sign !== 'undefined' && proc.config.sign.required){
proc.config.sign = Object.assign(proc.config.sign, this[this.length-1].config.sign);
onetimeKeyObject.privateKeys = signingKeys;
suiteObject.sign_verify = proc.config.sign.suite;
modeArray.push('sign');
}
this[idx].keys = await importKeys('object', {keys:onetimeKeyObject, suite: suiteObject, mode: modeArray});
if (typeof this[this.length-1].config.encrypt.externalKey === 'undefined' || !this[this.length-1].config.encrypt.externalKey) {
throw new Error('FinalStepMustBeExternalKey');
}
}));
}
_initDecryptionProcedure(){
// do nothing at this point
}
async encrypt(message){
if(this._cascadeMode !== 'encrypt') throw new Error('NotEncryptionCascade');
if(!(message instanceof Uint8Array)) throw new Error('NotUint8ArrayMessage');
// set given message as the first step message
this[0].message = message;
const data = await Promise.all(this.map( (proc) => core.encrypt(proc)));
return createCascadedData(data);
}
async decrypt(){
if(this._cascadeMode !== 'decrypt') throw new Error('NotDecryptionCascade');
// export verificationKey for precedence
const verificationKeys = this._orgKeys.keys.publicKeys;
// serialized decryption
const decrypted = new Array(this.length);
for(let idx = this.length-1; idx >= 0; idx--) {
if (!(this[idx].keys instanceof Keys)) throw new Error('InvalidKeysObject');
if (typeof this[idx].data === 'undefined') throw new Error('InvalidDataObject');
decrypted[idx] = await core.decrypt(this[idx]);
// assign decrypted message as previous step decryption key
if(idx > 0){
const suiteObject = {encrypt_decrypt: this[idx-1].data.message.suite};
const modeArray = ['decrypt'];
let nextDecryptionKeyObject;
if (this[idx-1].data.message.keyType === 'session_key_encrypt') nextDecryptionKeyObject = {sessionKey: decrypted[idx].data};
else {
if (this[idx-1].data.message.suite === 'jscu'){
nextDecryptionKeyObject = {privateKeys: [await Jscu.importKey('der', decrypted[idx].data)]};
}
else throw new Error('UnknownSuite');
}
// updated config and key object for signing and key import
if (this[idx-1].data.signature instanceof Signature && typeof verificationKeys !== 'undefined'){
nextDecryptionKeyObject.publicKeys = verificationKeys;
suiteObject.sign_verify = this[idx-1].data.signature.suite;
modeArray.push('verify');
}
this[idx-1].keys = await importKeys('object', { keys: nextDecryptionKeyObject, suite: suiteObject, mode: modeArray });
}
}
return decrypted;
}
get mode () { return this._cascadeMode; }
get keys () { return this._orgKeys; }
// get allKeys () { return null; } // TODO
toArray() { return Array.from(this); }
map(callback) { return this.toArray().map(callback); }
slice (a, b) { return this.toArray().slice(a, b); }
}