junkurihara/cascade

View on GitHub
src/cascade.js

Summary

Maintainability
C
1 day
Test Coverage
/**
 * 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); }
}