@sounisi5011/encrypted-archive

Convert data into a single encrypted archive data that contains all metadata needed for decryption

Usage no npm install needed!

<script type="module">
  import sounisi5011EncryptedArchive from 'https://cdn.skypack.dev/@sounisi5011/encrypted-archive';
</script>

README

@sounisi5011/encrypted-archive

Go to the latest release page on npm Supported Node.js version: ^12.17.x || 14.x || 15.x || 16.x || 17.x Tested with Jest Commitizen friendly Minified Bundle Size Details Install Size Details Dependencies Status Build Status Maintainability Status

Convert data into a single encrypted archive data that contains all metadata needed for decryption.

Features

  • Only password and target data are required

    Other data required for encryption (nonce, key derivation function, etc.) will be generated automatically.

  • Support for secure algorithms

    This package supports only the following algorithms that are currently (2021) recommended.

    • Encryption algorithm
      • AES-GCM (256-bit)
      • ChaCha20-Poly1305
    • Key derivation function
      • Argon2
  • A counter is used to generate the IV (Initialization Vector)

    When encrypting with the same key, the IV MUST NEVER be reused. However, there is a risk of generating the same IV when using random numbers. In this package, IVs are counter-generated to avoid unintentional reuse of IVs.

    See: nonce-disrespect/nonce-disrespect: Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in TLS

  • Backward compatibility

    The data format uses unsigned varint and Protocol Buffers. High backward compatibility is maintained even when new features are added in the future.

Example of use

  • Generating a backup file
  • Private data files accessible through public URLs

Not recommended for use

  • Encryption of transmission data

    If you encrypt a large amount of small data, there is a risk that the counter used to generate the IV will overflow.

  • Multi-processing and multi-threading

    Currently, the counter used to generate IVs does not support different processes or threads. If used in multiple processes or threads, there is a danger of duplicate IVs.

Attention

I am not a security expert. I have researched a lot of information in order to create this package. I believe that this package will be secure. However, there is a possibility that I am wrong.

Installation

npm install @sounisi5011/encrypted-archive
yarn add @sounisi5011/encrypted-archive
pnpm add @sounisi5011/encrypted-archive

Usage

Small data

If you have a short string or a small file of data to encrypt, you can use a simple function.

const { encrypt, decrypt } = require('@sounisi5011/encrypted-archive');

const cleartext = 'Hello World!';
const password = '1234';

encrypt(cleartext, password, {
    // These options are optional, but it is recommended that you specify the appropriate options for your application.
    algorithm: 'chacha20-poly1305',
    keyDerivation: {
        algorithm: 'argon2d',
        iterations: 3,
        memory: 12,
        parallelism: 1,
    },
    // If the data to be encrypted is text, you can also compress the data.
    // Binary data (e.g. images, videos, etc.) can also be compressed,
    // but the effect of compression is often small and is not recommended.
    compress: 'gzip',
})
    .then(encryptedData => {
        // ...
    })
    .catch(error => {
        // ...
    });

// ----- //

const encryptedData = Buffer.from( ... );
decrypt(encryptedData, password)
    .then(decryptedData => {
        // ...
    })
    .catch(error => {
        // ...
    });

Huge data

For huge files or data (e.g., hundreds of megabytes or tens of gigabytes), you can use Node.js Stream or Async Iteration.

Stream

const fs = require('fs');
const stream = require('stream');
const { encryptStream, decryptStream } = require('@sounisi5011/encrypted-archive');

const password = '1234';

const inputStream = fs.createReadStream('very-large.mp4');
const outputStream = fs.createWriteStream('very-large.mp4.enc');

stream.pipeline(
    inputStream,
    encryptStream(password, {
        // These options are optional, but it is recommended that you specify the appropriate options for your application.
        algorithm: 'aes-256-gcm',
        keyDerivation: {
            algorithm: 'argon2d',
            iterations: 3,
            memory: 12,
            parallelism: 1,
        },
        // If the data to be encrypted is text, you can also compress the data.
        // Binary data (e.g. images, videos, etc.) can also be compressed,
        // but the effect of compression is often small and is not recommended.
        //compress: 'gzip',
    }),
    outputStream,
    error => {
        if (error) {
            // ...
        } else {
            // ...
        }
    },
);

// ----- //

stream.pipeline(
    fs.createReadStream('very-large.mp4.enc'),
    decryptStream(password),
    fs.createWriteStream('very-large.mp4'),
    error => {
        if (error) {
            // ...
        } else {
            // ...
        }
    },
);

Async Iteration

const fs = require('fs');
const stream = require('stream');
const { encryptIterator, decryptIterator } = require('@sounisi5011/encrypted-archive');

const password = '1234';

const inputIterator = (async function*() {
    for await (const chunk of fs.createReadStream('very-large.mp4')) {
        yield chunk;
    }
})();
const encryptor = encryptIterator(password, {
    // These options are optional, but it is recommended that you specify the appropriate options for your application.
    algorithm: 'aes-256-gcm',
    keyDerivation: {
        algorithm: 'argon2d',
        iterations: 3,
        memory: 12,
        parallelism: 1,
    },
    // If the data to be encrypted is text, you can also compress the data.
    // Binary data (e.g. images, videos, etc.) can also be compressed,
    // but the effect of compression is often small and is not recommended.
    //compress: 'gzip',
});

(async () => {
    try {
        for await (const encryptedDataChunk of encryptor(inputIterator)) {
            // ...
        }
    } catch (error) {
        // ...
    }
})();

// ----- //

const inputEncryptedIterator = (async function*() {
    for await (const chunk of fs.createReadStream('very-large.mp4.enc')) {
        yield chunk;
    }
})();
const decryptor = decryptIterator(password);

(async () => {
    try {
        for await (const decryptedDataChunk of decryptor(inputEncryptedIterator)) {
            // ...
        }
    } catch (error) {
        // ...
    }
})();

API

encrypt(cleartext, password, options?)

Returns a Promise giving a Buffer object.

  • cleartext

    Type: string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer

  • password

    Type: string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer

  • options

    see EncryptOptions

decrypt(encryptedData, password)

Returns a Promise giving a Buffer object.

  • encryptedData

    Type: string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer

  • password

    Type: string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer

encryptStream(password, options?)

Returns a Transform stream.

  • password

    Type: string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer

  • options

    see EncryptOptions

decryptStream(password)

Returns a Transform stream.

  • password

    Type: string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer

encryptIterator(password, options?)

Returns an IteratorConverter function.

  • password

    Type: string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer

  • options

    see EncryptOptions

decryptIterator(password)

Returns an IteratorConverter function.

  • password

    Type: string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer

IteratorConverter(source)

Returns an AsyncIterableIterator giving a Buffer object.

  • source

    Type:

      Iterable<string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer>
    | AsyncIterable<string | Buffer | TypedArray | DataView | ArrayBuffer | SharedArrayBuffer>
    

EncryptOptions

An object with the following properties:

All properties are optional.

algorithm

Type: CryptoAlgorithmName

An encryption algorithm name string. Specify one of the following:

  • "aes-256-gcm"
  • "chacha20-poly1305" (default)

keyDerivation

Type: KeyDerivationOptions

An object with the key derivation function name and options. The key derivation function name is specified as a string in the algorithm property. The other properties are options for the key derivation function.

Currently, the following key derivation functions are supported:

  • Argon2

    • algorithm

      • "argon2d" (default)
      • "argon2id"
    • iterations

      Type: number

      the number of iterations. default: 3

    • memory

      Type: number

      used memory, in KiB. default: 12

    • parallelism

      Type: number

      desired parallelism. default: 1

    For more information on the values that should be specified for these parameters, please refer to the following article: Choose Argon2 Parameters for Secure Password Hashing and Login - ory.sh

compress

Type: CompressOptions | CompressOptions['algorithm']

A compression algorithm name string, or an options object for the compression algorithm. When specifying an object, the compression algorithm name is specified as a string in the algorithm property. The other properties are options for the compression algorithm.

Currently, the following compression algorithm are supported:

  • Gzip

    • algorithm
      • "gzip"

    Other properties are passed to zlib options. However, the following properties are not allowed:

    • flush
    • finishFlush
    • dictionary
    • info
    • maxOutputLength
  • Brotli

    • algorithm
      • "brotli"

    Other properties are passed to brotli options. However, the following properties are not allowed:

    • flush
    • finishFlush
    • maxOutputLength

Structure of the encrypted archive

see docs/encrypted-archive-structure.md