iso-crypto
TypeScript icon, indicating that this package has built-in type declarations

1.1.0 • Public • Published

iso-crypto

Cryptographic methods that work in isomorphic (Browser + NodeJS) environments.

npm package License Quality

Contents

Introduction

IsoCrypto provides a common interface for cryptographic methods for both NodeJS and Browser environments. On NodeJS it generally relies on the node:crypto module, and on Browser based on window.crypto.subtle.

Some lower level isomorphic utilities are also provided, such as text encoding and random byte generation.

Supported Algorithms

Hash

  • sha1
  • sha256
  • sha384
  • sha512

Symmetric Encryption

  • aes-128-cbc
  • aes-192-cbc
  • aes-256-cbc
  • aes-128-ctr
  • aes-192-ctr
  • aes-256-ctr

ECDH (Asymmetric)

  • prime256v1
  • secp384r1
  • secp521r1

Install

npm i iso-crypto

Example

Encoding

import { decode, encode } from 'iso-crypto';

const buffer: Uint8Array = decode('This is my text');
const encoded = encode(buffer); // 'This is my text'

Random

import { randomBytes } from 'iso-crypto';

const rand: Uint8Array = await randomBytes(10);

Hash

import { encode, hash } from 'iso-crypto';

const hashed: Uint8Array = await hash('This is my text');
encode(hashed, 'hex'); // 04405de6b2dcefcd85a03503cefad765a88c8083c5fede2eb7e4354e5643e2df

const customHashed: Uint8Array = await hash(
    { text: 'abc123', encoding: 'hex' },
    {
        algorithm: 'SHA2',
        size: 512,
    }
);
encode(customHashed, 'base64'); // 8ff0IFBTdySF86lYF23zbCfmerD9ieLgid5Ak+5pRwffOgSY8yQ78HifX0lxZ0hKHZP4uZ0Nfr9dcwZmo5Mkfw==

Symmetric Encryption

import { decrypt, encode, encrypt } from 'iso-crypto';

const secret = 'Super duper secret password';

const encrypted = await encrypt({
    data: 'This is my message',
    secret,
});
const decrypted = encode(await decrypt({
    ...encrypted,
    secret,
})); // 'This is my message'

const customAlgEncrypted = await encrypt(
    {
        data: 'This is my response',
        secret,
    },
    {
        hash: { algorithm: 'SHA1' },
        encryption: {
            cipher: 'AES',
            size: 192,
            mode: 'CBC',
        },
    }
);
const customAlgDecrypted = encode(await decrypt(
    {
        ...customAlgEncrypted,
        secret,
    },
    {
        hash: { algorithm: 'SHA1' },
        encryption: {
            cipher: 'AES',
            size: 192,
            mode: 'CBC',
        },
    }
)); // 'This is my response'

ECC

import { eccDecrypt, eccEncrypt, encode, generateEccPrivateKey, generateEccPublicKey } from 'iso-crypto';

const anne: Uint8Array = await generateEccPrivateKey();
const bob: Uint8Array = await generateEccPrivateKey();

const fromAnneEncrypted = await eccEncrypt({
    data: 'Hello, would you like a cup to tea?',
    privateKey: anne,
    publicKey: generateEccPublicKey(bob),
});
const fromAnneDecrypted = encode(await eccDecrypt({
    ...fromAnneEncrypted,
    privateKey: bob,
})); // 'Hello, would you like a cup to tea?'

const fromBobEncrypted = await eccEncrypt(
    {
        data: 'I am more of a coffee drinker myself',
        privateKey: bob,
        publicKey: generateEccPublicKey(anne),
    },
    {
        encryption: {
            cipher: 'AES',
            size: 128,
            mode: 'CTR',
        },
    }
);
const fromBobDecrypted = encode(await eccDecrypt(
    {
        ...fromBobEncrypted,
        privateKey: anne,
    },
    {
        encryption: {
            cipher: 'AES',
            size: 128,
            mode: 'CTR',
        },
    }
)); // 'I am more of a coffee drinker myself'

Usage

iso-crypto is an ESM module. That means it must be imported. To load from a CJS module, use dynamic import const { eccEncrypt } = await import('iso-crypto');.

Most cryptographic methods return instances of Uint8Array, instead of strings. Those can encoded as text via the encode method. In NodeJS the response is often actually a Buffer which extends Uint8Array.

Any method that takes input "text" allows 3 formats:

  • string - inferred as UTF8 encoding
  • Uint8Array - "raw" encoding
  • { text: string; encoding: string } - text with a custom encoding
    • Supported encodings: hex, base64, base64url, utf8

Any method that performs hashing can take the Hash algorithm as an option. The default algorithm is SHA256. In any case where hashing should not be used (e.g. custom hashing already implemented), declare the raw option.

e.g.

import { encrypt } from 'iso-crypto';

await encrypt(
    {
        data: 'My message',
        secret: 'Already hashed',
    },
    {
        hash: 'raw',
    }
);

Any method that performs symmetric encryption can take the encryption algorithm as an option. The default algorithm is aes-256-ctr ({ cipher: 'AES', size: 256, mode: 'CTR' }).

Similarly the default ECC algorithm is prime256v1 ({ curve: 'p256' }).

Many algorithms required keys/buffers of fixed size. However there is no enforcement by this library that the provided values meet that size. The general approach is to hash the input, then adjust the bytes to fix. Buffers that are too small have 0 prepended, and similarly when too large are stripped from their beginning to match the desired size.

It is important to note that encryption IS NOT the same as compression. In fact encryption methods will generally be slightly larger than the original input.

Ideally any cryptographic output should have statistically uniform distribution of their outputs. That means every byte in the output is equally likely to be 00 as it is FF, and everything in between.

As such, if you intend to combine encryption with compression, it is important to apply compression before encryption, to take advantage of repeated and common patterns in the input.

Many methods in this module deal with "private" or "secret" values. Proper handling of those values are not explicitly covered by this module. Make sure you understand how to securely transfer and access these values before deploying encryption in production, as improper handling and data leaks can effectively render even the most secure encryption algorithms moot.

Implementation Details

These are far from thorough explanations of cryptographic functions, but hopefully serve as enough to justify design decisions and explain method interfaces.

Hashing

Hashing algorithms are a "one way" programs, that output a fixed-size buffer that is effectively random for a given input. Re-hashing the same input with the algorithm always returns the same output, but given the output it is virtually impossible to guess a valid input. Similarly it is virtually impossible to "guess" the output without fully applying the algorithm.

Symmetric Encryption

Symmetric encryption algorithms take a message (a.k.a. data) and a "key" (a.k.a. secret) and produce a seemingly random output that represents that message. This output is effectively safe to expose publicly, as the original message is cryptographically hidden.

The same key (this is what makes it "symmetric") can then be used decrypt the output and retrieve the original message.

The key length is specified by the algorithm (e.g. aes-256-ctr requires 32 bytes), which may not necessarily match the actual length of the secret provided. Similarly the provided secret may not have "uniform distribution" of bytes (e.g. Password123 only has bytes between 30-7A). To mediate, the secret is hashed, and has bytes prepended/removed from the beginning of the buffer to match.

These algorithms also require an effectively random "Initialization Vector" (a.k.a. iv). This buffer is not necessarily private, it just cannot be known ahead of time, and should never be reused. Encryption methods in iso-crypto will generate this iv internally from a random number generator (see randomBytes) and exposed in the output alongside the encrypted data.

Given that the same secret it used to both encrypt and decrypt a message, it is not appropriate for sending messages between multiple parties. Instead, it should be used only in cases where the data is being stored/transferred by an untrusted third party and then re-read by original party (e.g. storing sensitive files on a third party server).

Asymmetric Encryption

Asymmetric encryption differs from Symmetric encryption in regards to the "secret". In Asymmetric encryption, messages are secured with both a private key (known only to one party, treated as secret) and a public key (derived from a private key. Safe, if not encouraged, to share publicly).

ECDH

Presently, the only form of asymmetric encryption implemented by this module is Elliptic Curve Diffie-Hellman (ECDH).

How ECC works

Without going into extreme details of curve mathematics, the algorithm is based on points on curve. "Adding" (for simplicity, traditional mathematical terms/symbols are used, but the logic itself is not as trivial as traditional addition) points on a curve is a straightforward equation. "Multiplying" a point can only be achieved by adding the original point N times, which can be simplified into binary logic (doubling + adding). These new points also lie on the curve (and are therefore verifiable).

For example P1 * 10 can be simplified as P1 * ((8 * 1) + (4 * 0) + (2 * 1) + (1 * 0)) -> P1 * 8 + P1 * 2. Note that P1 * 2 can be rewritten as P1 + P1 (straightforward) and P1 * 4 can be rewritten as P2 = P1 * 2; P2 * 2 (re-using a cached value). As such, multiplying a Point can be solved in O(log(n)) time (multiplying by 2^256 takes ~ 256 "steps") which is very acceptable.

Given the complication of multiplication, there is no known "division" operator in ECC. As such, an algorithm can agree on an original point on a curve. Then the "private key" is simply a random number between the curves "domain" (prime256v1 domain is between 0 and 2^256). The "public key" is the resulting point on the curve after multiplication.

A public key technically represents a point (both X and Y coordinates). However, since the equation for the curve is known, it is possible to provide only the X coordinate, and a single byte indicating whether the associated Y coordinate is odd or even. This is known as the "compressed" form of a public key.

Given one parties public key (a point), and another's private key (an integer), these can again be multiplied to produce a "shared secret". Due to the communicative properties of addition/multiplication, both combinations of PublicA * PrivateB AND PrivateA * PublicB produces the same shared secret.

Implementation

This module focuses on the point mathematics required by ECC (relying on native modules as much as possible). Once a shared secret can be calculated, encryption is performed by the symmetric encryption algorithms provided by this module.

Since the secret is determined by the private key + public key combination, it can be used directly without any need for hashing. As such, no hash algorithm is used with ECC encryption.

Any public keys returned by this module will use the compressed form of a public key by default, however both forms are accepted as input.

API

Types

InputType

Type used to represent allowed text/data input.

string | Uint8Array | { text: string; encoding: string }.

To verify the "buffer" input, pass the text to decode (which is done internally).

A plain string is inferred to be UTF8 encoded.

Hash

Type used to represent a specific hash algorithm.

{ algorithm: string; size: number } | 'raw'.

To avoid hashing (as it is sometimes used internally by algorithms) provide the 'raw' option to return the input unchanged.

Generally defaults to { cipher: 'AES', size: 256, mode: 'CTR' }.

Encryption

Type used to represent a specific symmetric encryption algorithm.

{ cipher: string; size: number; mode: string }.

Curve

Type used to represent a specific ECC curve. string.

Generally defaults to p256.

atob

See MDN Docs.

Direct usage is generally discouraged (see decode) but has been isomorphically implemented internally, and exposed for potential convenience.

btoa

See MDN Docs.

Direct usage is generally discouraged (see encode) but has been isomorphically implemented internally, and exposed for potential convenience.

crypto

See MDN Docs.

Direct usage is generally discouraged but has been isomorphically implemented internally, and exposed for potential convenience.

randomBytes(size: number)

Produces a promise of Uint8Array of length size filled with random bytes.

decode(input: InputText)

Decodes any input as Uint8Array, given the provided encoding (see InputText).

encode(input: InputText, outputEncoding?: string)

Encodes any input (ideally as Uint8Array, but potentially as a differently encoded text) as the desired encoding.

Defaults to UTF8.

decodeObject(input: Record<string, string>)

Decodes each attribute in an object to Uint8Array. Convenience method around manually calling decode for each attribute.

Potentially useful for converting serialized text (e.g. JSON containing hex-decoded data) to usable buffers.

encodeObject(input: Record<string, Uint8Array>)

Encodes each attribute in an object to string. Convenience method around manually calling encode for each attribute.

Potentially useful for serializing decoded text (JSON does not like raw Uint8Array).

hash(input: InputText, algorithm?: Algorithm)

Asynchronously hashes the input, and returns the output buffer. Defaults to SHA256 algorithm.

encrypt(params: { data: InputText; secret: InputText }, options?: { hash?: Hash; encryption?: Encryption })

Asynchronously encrypts the input using the provided algorithms (defaults to aes-256-ctr). Hashes the secret internally with the provided hashing algorithm (defaults to SHA256).

Returns both the encrypted data as a Uint8Array, as well as the internally generated iv.

Both of these properties will need to be provided to the decrypt method in order to decrypt the original data.

decrypt(params: { encrypted: InputText; iv: InputText; secret: InputText }, options?: { hash?: Hash; encryption?: Encryption })

Asynchronously decrypts the data using the provided algorithms. Make sure that these are the same algorithms used to encrypt the data originally.

Both the encrypted + iv comes from the output of encrypt, alongside the original secret.

generateEccPrivateKey(curve?: Curve)

Asynchronously generates an ECC private key as a Uint8Array.

generateEccPublicKey(privateKey: InputText, curve?: Curve)

Generates the corresponding public key for the provided private key.

compressEccPublicKey(publicKey: InputText, curve?: Curve)

Calculates the corresponding compressed public key for the provided public key.

NOOP if public key is already compressed (idempotent).

Generally available for convenience. All public keys outputted by ecc methods are compressed by default, and accept both forms as input.

decompressEccPublicKey(publicKey: InputText, curve?: Curve)

Calculate the corresponding uncompressed public key for the provided public key.

NOOP if public key is already uncompressed (idempotent).

eccEncrypt(params: { data: InputText; privateKey: InputText; publicKey: InputText }, options?: { curve?: Curve; encryption?: Encryption })

Asynchronously determines the shared secret between public + private key, and encrypts the data using the specified encryption algorithm (defaults to aes-256-ctr).

Ensure that the keys are generated using the same curve algorithm provided.

Note that the publicKey should be the "receiver's" public key, not simply the public key for the provided privateKey ("sender").

Returns both the encrypted + iv data (see encrypt) as well as the publicKey corresponding to the input privateKey for convenience.

eccDecrypt(params: { encrypted: InputText; iv: InputText; privateKey: InputText; publicKey: InputText }, options?: { curve?: Curve; encryption?: Encryption })

Asynchronously decrypts the data from eccEncrypt using the receiver's privateKey and the sender's publicKey.

Ensure that the provided curve + encryption algorithm is the same as used to encrypt.

Package Sidebar

Install

npm i iso-crypto

Weekly Downloads

33

Version

1.1.0

License

MIT

Unpacked Size

63.2 kB

Total Files

69

Last publish

Collaborators

  • jacobley