crypto-cascade

0.4.9 • Public • Published

Cascade - Encryption and signing library for x-brid encryption via several cryptographic suites.

npm version codecov Maintainability License: MIT Unit Test

WARNING: At this time this solution should be considered suitable for research and experimentation, further code and security review is needed before utilization in a production application.

Introduction and Overview

Considering existing cryptographic libraries and native APIs for JavaScript, as far as we know, fundamental implementations of primitives have been developed separately in multiple environments such as Node.js and various browsers. In order to fill gaps among those different environments, there exist several nice universal cryptographic suites that flawlessly work in most of modern JavaScript environments. Here we have defined 'suites' as ones providing encryption, singing and other supplemental functions like OpenPGP library.

However, they are still too primitive to realize a bit more modern cryptographic services in JavaScript. In other words, we see that simple encryption and signing provided by those primitives are insufficient to directly satisfy more complex demands, e.g., revocation of decryption rights after encryption of data. Considering such situation, the aim of this project called Cascade is to provide a flexible cryptographic application library in JavaScript that realizes the x-brid encryption and signing by utilizing multiple cryptographic suites. This enables us to, for instance, realize complex structures of access rights to encrypted data, e.g., revocation after encryption as mentioned above.

X-brid Encryption

Here we shall explain the detailed mechanism of x-brid encryption by illustrating the simplest example instance of hybrid encryption, i.e., x = 2, which is a well-known cryptosystem in the current security technology. The following is a schematic block diagram of the hybrid encryption.

Schematic Diagram of Hybrid Encryption

As we see, this hybrid procedure consists of two steps where the step 1 encrypts the given plaintext message under (one-time) session key in a certain symmetric key encryption, and the step 2 encrypts the previously-used session key under a given public key(s) as a plaintext in a public key encryption. Although this looks somewhat redundant and waste of computing resource, it has a great advantage in terms of storage usage in the case where we have multiple receivers, i.e., multiple public keys. Namely, the encrypted message body that is likely big would be common and recycled to all the receivers, and only encrypted session key that should be small is 'personalized' to each receivers.

Moreover, this cryptosystem could yield another merit, which is the revocation after encryption of data. Assume that we first provide receivers the encrypted message body, and recall that at this point, no one can decrypt it. This implies that you can provide 'personalized' encrypted session keys to only authorized person later and freely discard the encrypted session keys, i.e., granting and revoking decryption rights and access rights to the data. We may know that this is a very basic and fundamental concept of encryption-based access control that is likely to be a part of well-known digital rights management (DRM).

Cascade project can instantiate the above mentioned hybrid encryption by its nature, and it also generalizes this basic 2-step cryptosystem to x-step one (x > 0), namely, x-brid encryption.

Cryptographic Functions Employed in X-brid Encryption and Future Extension

We briefly explained our concept of x-brid encryption by providing a fairly simple hybrid instance as above. As a natural consequence of the generalization by Cascade, we can simply increase the value x and compose, say, tri-brid (x = 3) or tetra-brid (x = 4) encryption by cascading symmetric encryption steps. (We are honestly unsure this technically sounds at this point, but such structures may fit a certain type of application like the relationship of hybrid encryption and DRM.)

On the other hand, there is another room to generalize the cryptosystem from the viewpoint of encryption function at each step of x-brid encryption. The current implementation of Cascade can utilize encryption functions of a couple of cryptographic suites, and they supports only basic public key encryption (RSA and elliptic curve cryptosystems) and symmetric key encryption (AES). We mean that as additions to the public/private key pair based cryptosystem, we should plan to supports other types of modern cryptography as suites. In fact, the concept and current implementation can accept more interesting and modern cryptographic primitive functions as a step of x-brid encryption. For instance:

  • Broadcast encryption
  • Attribute-based encryption
  • Secret sharing (e.g., split our session key at the final step!)

We can see that by employing those functions at some steps, new types of cryptographic application could be realized.

We also mention that a classical broadcast encryption based on tree can be possibly instantiated in the context of x-brid encryption. This is from the following observation. First consider to attach public key encryption to all steps, and assume that the plaintext message at each step is the private key used in the previous step. This composes a tree of multiple layers of private key encapsulation that is the core of tree-based broadcast encryption.

Supported Crypto Suites

This library currently supports one cryptographic suite, js-crypto-utils. js-crypto-utils is a simple crypto suite for plain implementations of cryptographic functions unlike fully-specified suites like OpenPGP. We should note that js-crypto-utils can be viewed as a integrated wrapper or interfaces of RFC standardized functions that are mostly built-in ones of browsers and Node.js. At the initial implementation, although we had adopted openpgpjs as a suite, it has been disruptively changing its (internal) APIs to follow the OpenPGP RFC standards and hence we purged the OpenPGP support from cascade due to the toughness of tracking its changes.

  • Encryption and decryption:
    • js-crypto-utils
      • Public key encryption (ECDH, HKDF and AES256-GCM combination)
      • Public key encryption (RSA-OAEP)
      • Session key encryption (AES-GCM)
  • Signing and verification:
    • js-crypto-utils
      • RSA-PSS signature (May not work in IE11 and Edge.)
      • RSASSA-PKCS1-v1_5 signature
      • ECDSA signature
  • Key generation:
    • js-crypto-utils
      • Public and private key pair generation w/ and w/o passphrase in PEM armored format (ECC and RSA)
      • Session key generation

Installation and Setup

Installation

At your project directory, first do either one of the following.

$ npm install --save crypto-cascade # from npm
$ yarn add crypto-cascade # from yarn

Then import the Cascade library as follows.

import cascade from 'crypto-cascade'

Of cource, you can also directly import the source code by cloning this Github repo.

Finishing up the setup

The Cascade library doesn't internally import cryptographic suites, i.e., js-crypto-utils in a static manner, but it loads them in a dynamic manner. In particular, it calls those suites via require for Node.js and as window objects for browsers. This means that for browsers, js-crypto-utils (jscu.bundle.js) must be pre-loaded by <script> tags in html files.

Usage

Here we give some basic example of usecases of Cascade. This section is organized as follows. First, we explain how to generate keys in Cascade. Then as a function employed at each step of x-brid encryption, we describe a very basic single encryption and signing operations in Cascade. This can be also viewed as the case where x = 1. After these warmp-ups, we finally show how to employ the x-brid encryption in Cascade. We should really note that this is just an example and the source/test codes and JSDoc is useful to understand the detailed mechanism and usage of Cascade.

Key generation

Cascade provides a basic function to generate PEM-formatted public/private key pairs. The following example describes an example to generate PEM-formatted public and private keys of elliptic curve cryptography using js-crypto-utils.

const keyParam = {
  suite: 'jscu', // use 'js-crypto-utils'
  keyParams: { type: 'ec', curve: 'P-256' }
};
const keyPair = await cascade.generateKey(keyParam);
const publicKeyPEM = keyPair.publicKey.keyString; // EC public key in PEM format
const privateKeyPEM = keyPair.privateKey.keyString; // EC private key in PEM format

Here we should note that for the key generation using js-crypto-utils, the generated public key is encoded as SubjectPublicKeyInfo specified as a part of X.509 public key certificate (RFC5280). On the other hand, the generated private key is encoded as PrivateKeyInfo/OneAsymmetricKey defined in PKCS#8 (RFC5958). Hence the private key can be encrypted with a passphrase just by passing API the passphrase string as given below.

const keyParam = {
  suite: 'jscu',
  keyParams: { type: 'ec', curve: 'P-256' },
  passphrase: 'secret passphrase'
};
const keyPair = await cascade.generateKey(keyParam);

Then, the protected private key is encoded as EncryptedPrivateKeyInfo.

The key generation API can generate not only EC public and private key strings but also RSA ones and session keys, where generated session keys are just random bytes given in Uint8Array unlike formatted strings of public and private keys.

Basic encryption simultaneously with signing

The following example describes how to simply encrypt a message in Uint8Array (String is also accepted) simultaneously with signing on the given plaintext message in Cascade. Since the cascaded encryption, i.e., x-brid encryption, will be employed by chaining this basic encryption and decryption function, we shall firstly explain this basic function and its usage in a step-by-step manner.

First of all, we need to import keys to be used, and obtain Keys object that will be used to encrypt and decrypt in Cascade.

const encryptionKeys = {
  publicKeys: [ keys.publicKey.keyString ],
  privateKeyPassSets:[ { privateKey: keys.privateKey.keyString, passphrase: '' } ] // for Signing
};

// import encryption key strings
const encryptionKeyImported = await cascade.importKeys(
  'string',
  {keys: encryptionKeys, suite: {encrypt_decrypt: 'jscu', sign_verify: 'jscu'}, mode: ['encrypt', 'sign']}
);

The configuration object for encryption is also required like the following form that must be matched with the type of given encryption and signing keys imported. The following is an example for the case where the given public and private keys are PEM-formatted EC keys, i.e., ECDH+HKDF public key encryption and ECDSA signing, via js-crypto-utils (as referred to as jscu in the code block). In the configuration, the encrypt object must have externalKey entry to explicitly specify the origin of the encryption key in the step.

// Encryption and signing configuration
const encryptionConfig = {
  encrypt: {
    externalKey: true,
    suite: 'jscu',
    options: {
      // HKDF with SHA-256 is employed on the master secret derived from ECDH.
      hash: 'SHA-256',
      info: '',
      keyLength: 32,
      // The session key HKDF derives is used to encrypt the message via AES-GCM.
      encrypt: 'AES-GCM'
    }
  },
  sign: {
    suite: 'jscu',
    // Signature is required and computed simultaneously with encryption.
    required: true,
    options: {
      hash: 'SHA-256'
    }
  }
};

With the imported encryption/signing keys and encryption config, the encryption API cascade.encrypt employs a single-phase encryption with signing, and it returns an object consisting of message and signature sub-objects. Those sub-objects are able to be respectively serialized with their instance method serialize().

// encrypt
const encryptionResult = await cascade.encrypt({
  message: messageSomeHow, // in Uint8Array or String
  keys: encryptionKeyImported,
  config: encryptionConfig
});

// serialize
const serializedEncrypted = encryptionResult.message.serialize();
const serializedSignature = encryptionResult.signature.serialize();

Serialized objects must be de-serialized, i.e., ones in object forms, for decryption in Cascade. Serialized encrypted message objects and signature objects can be de-serialized with cascade.importEncryptedBuffer and cascade.importSignatureBuffer functions and encrypted message and signature objects are obtained.

// de-serialize
const deserializedEncrypted = cascade.importEncryptedBuffer(serializedEncrypted);
const deserializedSignature = cascade.importSignatureBuffer(serializedSignature);

Much like basic encryption, the decryption and verification key strings must be imported and the Keys object is required to decrypt de-serialized message objects.

const decryptionKeys = {
  privateKeyPassSets:[ { privateKey: keys.privateKey.keyString, passphrase: '' } ],
  publicKeys: [ keys.publicKey.keyString ] // for verification
};

// import decryption key strings
const decryptionKeyImported = await cascade.importKeys(
  'string',
  {keys: decryptionKeys, suite: {encrypt_decrypt: 'jscu', sign_verify: 'jscu'}, mode: ['decrypt', 'verify']}
);

By putting de-serialized message and signature objects with imported decryption keys as given above, the Cascade API cascade.decrypt returns a decrypted data and the result of signature verification.

// decrypt and verify
const decryptionResult = await cascade.decrypt({
  data: { message: deserializedEncrypted, signature: deserializedSignature },
  keys: decryptionKeyImported
});

That's all the basic encryption and decryption steps, and the cascaded encryption/decryption in Cascade are composed of multiple basic ones that chained sequentially. Next section will briefly explain this step with some exemplary operations.

Cascaded x-brid encryption with signing

Here we describe how to employ cascaded x-brid encryption simultaneously with signing by showing a simple example.

All we need to prepare for the cascaded x-brid encryption/decryption is exactly similar to the basic encryption described in the previous section. One main difference from basic ones is that we have to define an encryption procedure given as an array of encryption configuration objects. The following is an sample encryption procedure that will be used in this section.

const encryptionProcedure = [
  { // step 1
    encrypt: {
      externalKey: false,
      suite: 'jscu',
      onetimeKey: {keyParams: {type: 'session', length: 32}}, options: {name: 'AES-GCM'}
    },
    sign: { required: true }
  },
  { // step 2
    encrypt: {
      externalKey: true,
      suite: 'jscu', options: { hash: 'SHA-256', info: '', keyLength: 32, encrypt: 'AES-GCM' }
    },
    sign: { suite: 'jscu', required: true, options: { hash: 'SHA-256' } }
  }
];

The above example describes a procedure of hybrid encryption where the given message is first encrypted under a one-time session key generated internally at Cascade (step 1), and the session key is then encrypted under the externally given public key (step 2). We can see that the encrypt.onetimeKey specifies the key parameters generated at the step 1, and that the step 2 does not require the entry since public key(s) are given externally. In order to explain the origin of the encryption key explicitly externalKey must be given in each step. In terms of signatures, the signing parameters and keys given the final step, i.e., step 2, will be applied all the other steps if sign.required = true.

After setting up an encryption procedure, we then obtain a Keys object by importing key strings in an exactly same manner as the basic encryption given above. This Keys object must be matched the parameters of the final step in the given encryption procedure. We then instantiate a Cascade object with the Keys object and the given encryption procedure.

const encryptionKeys = {
  publicKeys: [ keys.publicKey.keyString ], // for encryption
  privateKeyPassSets: [ { privateKey: keys.privateKey.keyString, passphrase: '' } ] // for Signing
};

// import encryption keys
const encryptionKeyImported = await cascade.importKeys(
  'string',
  {
    keys: encryptionKeys,
    suite: {encrypt_decrypt: 'jscu', sign_verify: 'jscu'},
    mode: ['encrypt', 'sign']
  }
);

// instantiate encryption process
const eProcess = await cascade.createEncryptionCascade({
  keys: encryptionKeyImported,
  procedure: encryptionProcedure
});

Now all the encryption setup has done and we can encrypt a message in Uint8Array (or string) via encrypt method of the Cascade object. The ciphertext is given as an EncryptedMessage object, and the object can be viewed as an array in which each element exactly corresponds to each step of the encryption procedure. Its serialized data can be obtained through serialize method, and conversely, we can de-serialize the serialized data through importCascadeBuffer function.

// encrypt
const encrypted = await eProcess.encrypt(message);

// serialize
const serialized = encrypted.serialize();

// de-serialize
const deserialized = cascade.importCascadedBuffer(serialized);

Decryption operation is exactly inverse of the above encryption operation. First we must obtain a Keys object by importing decryption and verification keys, and instantiate the decryption Cascade object to setup the decryption process by the obtained EncryptedMessage object. Then, the plaintext message is finally obtained through decrypt method.

const decryptionKeys = {
  privateKeyPassSets:[ { privateKey: keys.privateKey.keyString, passphrase: '' } ], // for decryption
  publicKeys: [ keys.publicKey.keyString ] // for verification
};

// import decryption keys
const decryptionKeyImported = await cascade.importKeys(
  'string',
  {
    keys: decryptionKeys,
    suite: { encrypt_decrypt: 'jscu', sign_verify: 'jscu' },
    mode: ['decrypt', 'verify']
  }
);

// instantiate decryption process
const dProcess = await cascade.createDecryptionCascade({
  keys: decryptionKeyImported,
  encrypted: deserialized
});

// decrypt
const decrypted = await dProcess.decrypt();

Drop and extract a part of ciphertext

We can also drop and extract a part of ciphertext, namely some elements of the array EncryptedMessage. This enables us to control access rights of users who received the ciphertext by giving them the extracted part separately.

const idx = 0; // 0 to length-1 of encryption procedure
const extracted = encrypted.extract(idx); // drop and extract the indicated part from EncryptedMessage object

// still serializable after extraction
const serialized = encrypted.serialize();

// extracted part is an array where each element is serializable as well
const serializedExtracted = extracted.map( (obj) => obj.serialize() );

// de-serialize
const deserialized = cascade.importCascadedBuffer(serialized);

// de-serialize each extracted part.
const deserializedExtracted = cascade.importRawEncryptedBufferList(serializedExtracted);

// recover original EncryptedMessage object]
deserialized.insert(idx, deserializedExtracted);

Note

At this point, limitations of Cascade are basically from those of js-crypto-utils. Please refer to their documents first.

Lisence

Licensed under the MIT license, see LICENSE file.

Package Sidebar

Install

npm i crypto-cascade

Weekly Downloads

2

Version

0.4.9

License

MIT

Unpacked Size

163 kB

Total Files

24

Last publish

Collaborators

  • kurihara