Source: crypto/encryption.ts

/**
 * This file provides a collection of low level functions for encryption.
 *
 * Usually you won't use those directly, because you don't have access to the private keys.
 *
 * Everything is build on-top of tweetnacl. So you need to be familiar with how tweetnacl works in
 * order to use this safely. Symmetric encryption is done with tweetnacl.secretbox and asymmetric
 * encryption (attention: not asymmetric in the RSA sense) with tweetnacl.box.
 *
 * At the time of writing this, the 'tweetnacl.box' encryption had two steps:
 * 1) Derive symmetric key from asymmetric keys
 * 2) Use the symmetric key for encryption with 'tweetnacl.secretbox'
 *
 * Therefore, the nonce generation, nonce lengths are the same for symmetric and asymmetric
 * functions.
 *
 * @author Erik Haßlmeyer <erik.hasslmeyer@refinio.net>
 * @copyright REFINIO GmbH 2022
 * @license CC-BY-NC-SA-2.5; portions MIT License
 * @version 0.0.1
 */

import * as tweetnacl from 'tweetnacl';
import {createError} from '../errors';
import {deriveBinaryKey} from '../system/crypto-scrypt';

export type Nonce = Uint8Array & {_: 'nonce'};
export type Salt = Uint8Array & {_: 'salt'};
export type SymmetricKey = Uint8Array & {_: 'symmetricKey'};
export type PublicKey = Uint8Array & {_: 'publicKey'};
export type SecretKey = Uint8Array & {_: 'secretKey'};
export interface KeyPair {
    publicKey: PublicKey;
    secretKey: SecretKey;
}

const MINIMUM_SAFE_SALT_LENGTH_IN_BYTES = 16;

// ######## create* functions ########

/**
 * Create a random nonce that can be used for all functions in this file.
 *
 * @returns {Nonce}
 */
export function createRandomNonce(): Nonce {
    return tweetnacl.randomBytes(tweetnacl.secretbox.nonceLength) as Nonce;
}

/**
 * Create a random symmetric key.
 *
 * @returns {SymmetricKey}
 */
export function createSymmetricKey(): SymmetricKey {
    return tweetnacl.randomBytes(tweetnacl.secretbox.keyLength) as SymmetricKey;
}

/**
 * Create a new public/secret keypair.
 *
 * @returns {KeyPair}
 */
export function createKeyPair(): KeyPair {
    const keyPair = tweetnacl.box.keyPair();
    return {
        publicKey: keyPair.publicKey as PublicKey,
        secretKey: keyPair.secretKey as SecretKey
    };
}

/**
 * Create a suitable salt for the key derivation function in deriveSymmetricKeyFromSecret.
 *
 * @param {number} n - Length of salt in bytes. The requirement of the salt is to be unique over all
 * usages to prevent certain attacks that rely on precomputed values. The current consensus seems
 * to be that 16 bytes (128 bit) should be enough. Some sources say it shouldn't be less than
 * this value, so as default we use 16 bytes.
 * @returns {Salt}
 */
export function createRandomSalt(n: number = 16): Salt {
    return ensureSalt(tweetnacl.randomBytes(n));
}

// ######## ensure* functions ########

/**
 * Ensure that it is a public key by comparing the length of the data.
 *
 * Note that we cannot really check that it is a public key, we can just check that the single
 * requirement is met - the length. If the length is right we cast it to the right type.
 *
 * @param {Uint8Array} data - The Uint8Array with the public key in it.
 * @returns {SymmetricKey}
 */
export function ensureSymmetricKey(data: Uint8Array): SymmetricKey {
    if (data.length !== tweetnacl.secretbox.keyLength) {
        throw createError('CYENC-ENSSYM');
    }

    return data as SymmetricKey;
}

/**
 * Ensure that it is a secret key by comparing the length of the data.
 *
 * Note that we cannot really check that it is a secret key, we can just check that the single
 * requirement is met - the length. If the length is right we cast it to the right type.
 *
 * @param {Uint8Array} data - The Uint8Array with the secret key in it.
 * @returns {SecretKey}
 */
export function ensureSecretKey(data: Uint8Array): SecretKey {
    if (data.length !== tweetnacl.box.secretKeyLength) {
        throw createError('CYENC-ENSSEC');
    }

    return data as SecretKey;
}

/**
 * Ensure that it is a public key by comparing the length of the data.
 *
 * Note that we cannot really check that it is a public key, we can just check that the single
 * requirement is met - the length. If the length is right we cast it to the right type.
 *
 * @param {Uint8Array} data - The Uint8Array with the public key in it.
 * @returns {PublicKey}
 */
export function ensurePublicKey(data: Uint8Array): PublicKey {
    if (data.length !== tweetnacl.box.publicKeyLength) {
        throw createError('CYENC-ENSPUB');
    }

    return data as PublicKey;
}

/**
 * Ensure that it is a nonce by comparing the length of the data.
 *
 * Note that we cannot really check that it is a nonce, we can just check that the single
 * requirement is met - the length. If the length is right we cast it to the right type.
 *
 * @param {Uint8Array} data - The Uint8Array with the nonce in it.
 * @returns {Nonce}
 */
export function ensureNonce(data: Uint8Array): Nonce {
    if (data.length !== tweetnacl.secretbox.nonceLength) {
        throw createError('CYENC-ENSNCE');
    }

    return data as Nonce;
}

/**
 * Ensure that it is a suitable salt by ensuring that it is longer than a safe threshold.
 *
 * Note that we cannot really check that it is a salt, we can just check that the single
 * requirement is met - the length. If the length is right we cast it to the right type.
 *
 * @param {Uint8Array} data - The Uint8Array with the nonce in it.
 * @returns {Salt}
 */
export function ensureSalt(data: Uint8Array): Salt {
    if (data.length < MINIMUM_SAFE_SALT_LENGTH_IN_BYTES) {
        throw createError('CYENC-ENSSLT');
    }

    return data as Salt;
}

// ######## symmetric key derivation functions ########

/**
 * Derive a symmetric key pair from the public key of somebody else and your own private key.
 *
 * @param {SecretKey} mySecretKey - My own secret key
 * @param {PublicKey} otherPublicKey - The others public key
 * @returns {SymmetricKey}
 */
export function deriveSymmetricKeyFromKeypair(
    mySecretKey: SecretKey,
    otherPublicKey: PublicKey
): SymmetricKey {
    return tweetnacl.box.before(otherPublicKey, mySecretKey) as SymmetricKey;
}

/**
 * Derive a symmetric key from a password.
 *
 * @param {string} secret
 * @param {Salt} salt
 * @returns {Promise<SymmetricKey>}
 */
export async function deriveSymmetricKeyFromSecret(
    secret: string,
    salt: Salt
): Promise<SymmetricKey> {
    return (await deriveBinaryKey(secret, salt, tweetnacl.secretbox.keyLength)) as SymmetricKey;
}

// ######## symmetric encryption functions (tweetnacl.secretbox) ########

/**
 * Encrypt data with a symmetric key.
 *
 * @param {Uint8Array} data - The data to encrypt
 * @param {SymmetricKey} symmetricKey - The key used for encryption
 * @param {Nonce} nonce - The nonce to use for encryption
 * @returns {Uint8Array} - Encrypted data
 */
export function symmetricEncrypt(
    data: Uint8Array,
    symmetricKey: SymmetricKey,
    nonce: Nonce
): Uint8Array {
    return tweetnacl.secretbox(data, nonce, symmetricKey);
}

/**
 * Encrypt data and store the nonce along the cypher in the result.
 *
 * Storing the nonce along the cypher has the advantage, that you don't have to remember the
 * nonce for decryption later.
 *
 * @param {Uint8Array} data - The data to encrypt
 * @param {SymmetricKey} symmetricKey - The key used for encryption
 * @param {Nonce} nonce - The nonce to use for encryption and embedding. If not specified, then
 * create a random nonce.
 * @returns {Uint8Array} - Nonce concatenated with the cypher
 */
export function symmetricEncryptAndEmbedNonce(
    data: Uint8Array,
    symmetricKey: SymmetricKey,
    nonce: Nonce = createRandomNonce()
): Uint8Array {
    const cypher = symmetricEncrypt(data, symmetricKey, nonce);

    // Write the nonce and key into a single array
    const nonceAndCypher = new Uint8Array(tweetnacl.box.nonceLength + cypher.byteLength);
    nonceAndCypher.set(nonce, 0);
    nonceAndCypher.set(cypher, nonce.byteLength);

    return nonceAndCypher;
}

/**
 * Decrypt encrypted data.
 *
 * @param {Uint8Array} cypher - The encrypted data
 * @param {SymmetricKey} symmetricKey - The key used for decryption (must be the same that was
 * used for decryption)
 * @param {Nonce} nonce - The same nonce for decryption (must be the same that was used for
 * decryption)
 * @returns {Uint8Array} - Decrypted data
 */
export function symmetricDecrypt(
    cypher: Uint8Array,
    symmetricKey: SymmetricKey,
    nonce: Nonce
): Uint8Array {
    const data = tweetnacl.secretbox.open(cypher, nonce, symmetricKey);

    if (data === null) {
        throw createError('CYENC-SYMDEC');
    }

    return data;
}

/**
 * Decrypt encrypted data.
 *
 * Since the nonce is embedded in the cypher there is no need to specify it.
 *
 * @param {Uint8Array} cypherAndNonce - cypher with embedded nonce that was generated by using
 * '[symmetric]EncryptAndEmbedNonce'
 * @param {SymmetricKey} symmetricKey - The key used for decryption (must be the same that was
 * used for decryption)
 * @returns {Uint8Array} - Decrypted data
 */
export function symmetricDecryptWithEmbeddedNonce(
    cypherAndNonce: Uint8Array,
    symmetricKey: SymmetricKey
): Uint8Array {
    const nonce = cypherAndNonce.slice(0, tweetnacl.secretbox.nonceLength) as Nonce;
    const cypher = cypherAndNonce.slice(tweetnacl.secretbox.nonceLength);
    return symmetricDecrypt(cypher, symmetricKey, nonce);
}

// ######## encryption functions (tweetnacl.box) ########

/**
 * Encrypt data with a symmetric key derived from a public key of someone else and my own secret
 * key.
 *
 * When you have two key pairs 'myKeypair' and 'otherKeypair', then the symmetric key used for
 * encryption will be the same for (myKeypair.secretKey, otherKeyPair.publicKey) and
 * (myKeypair.publicKey, otherKeyPair.secretKey). That's how two communication partners can
 * encrypt / decrypt the same steam, because they can derive the same symmetric key, without
 * having the same secret key.
 *
 * @param {Uint8Array} data - The data to encrypt
 * @param {SecretKey} mySecretKey - My own secret key
 * @param {PublicKey} otherPublicKey - The others public key
 * @param {Nonce} nonce - The nonce to use for encryption
 * @returns {Uint8Array} - Encrypted data
 */
export function encrypt(
    data: Uint8Array,
    mySecretKey: SecretKey,
    otherPublicKey: PublicKey,
    nonce: Nonce
): Uint8Array {
    const symmetricKey = deriveSymmetricKeyFromKeypair(mySecretKey, otherPublicKey);
    return symmetricEncrypt(data, symmetricKey, nonce);
}

/**
 * Encrypt data and store the nonce along the cypher in the result.
 *
 * Storing the nonce along the cypher has the advantage, that you don't have to remember the
 * nonce for decryption later.
 *
 * @param {Uint8Array} data - The data to encrypt
 * @param {SecretKey} mySecretKey - My own secret key
 * @param {PublicKey} otherPublicKey - The others public key
 * @param {Nonce} nonce - The nonce to use for encryption and embedding. If not specified, then
 * create a random nonce.
 * @returns {Uint8Array} - Nonce concatenated with the cypher
 */
export function encryptAndEmbedNonce(
    data: Uint8Array,
    mySecretKey: SecretKey,
    otherPublicKey: PublicKey,
    nonce?: Nonce
): Uint8Array {
    const symmetricKey = deriveSymmetricKeyFromKeypair(mySecretKey, otherPublicKey);
    return symmetricEncryptAndEmbedNonce(data, symmetricKey, nonce);
}

/**
 * Decrypt encrypted data.
 *
 * @param {Uint8Array} cypher - The encrypted data
 * @param {SecretKey} mySecretKey - My own secret key
 * @param {PublicKey} otherPublicKey - The others public key
 * @param {Nonce} nonce - The same nonce for decryption (must be the same that was used for
 * decryption)
 * @returns {Uint8Array} - Decrypted data
 */
export function decrypt(
    cypher: Uint8Array,
    mySecretKey: SecretKey,
    otherPublicKey: PublicKey,
    nonce: Nonce
): Uint8Array {
    const symmetricKey = deriveSymmetricKeyFromKeypair(mySecretKey, otherPublicKey);
    return symmetricDecrypt(cypher, symmetricKey, nonce);
}

/**
 * Decrypt encrypted data.
 *
 * Since the nonce is embedded in the cypher there is no need to specify it.
 *
 * @param {Uint8Array} cypherAndNonce - cypher with embedded nonce that was generated by using
 * '[symmetric]EncryptAndEmbedNonce'
 * @param {SecretKey} mySecretKey - My own secret key
 * @param {PublicKey} otherPublicKey - The others public key
 * @returns {Uint8Array} - Decrypted data
 */
export function decryptWithEmbeddedNonce(
    cypherAndNonce: Uint8Array,
    mySecretKey: SecretKey,
    otherPublicKey: PublicKey
): Uint8Array {
    const symmetricKey = deriveSymmetricKeyFromKeypair(mySecretKey, otherPublicKey);
    return symmetricDecryptWithEmbeddedNonce(cypherAndNonce, symmetricKey);
}