Cipher.js

import ByteArray from './ByteArray.js';
/* eslint-disable no-unused-vars */
import { PublicKey, SharedKey256 } from './CryptoTypes.js';
/* eslint-enable no-unused-vars */
import crypto from 'crypto';

// ReactNative Buffer polyfill only allows Buffers to be passed to Buffer.concat
// in order to support ReactNative, all Uint8Arrays must be wrapped in Buffer when calling crypto cipher APIs
const toBufferView = input => {
	const typedByteArray = input instanceof ByteArray ? input.bytes : input;
	return Buffer.from(typedByteArray.buffer, typedByteArray.byteOffset, typedByteArray.length);
};

const concatArrays = (lhs, rhs) => {
	const result = new Uint8Array(lhs.length + rhs.length);
	result.set(lhs);
	result.set(rhs, lhs.length);
	return result;
};

// region AesCbcCipher

/**
 * Performs AES CBC encryption and decryption with a given key.
 */
export class AesCbcCipher {
	/**
	 * Creates a cipher around an aes shared key.
	 * @param {SharedKey256} aesKey AES shared key.
	 */
	constructor(aesKey) {
		/**
		 * @private
		 */
		this._key = aesKey;
	}

	/**
	 * Encrypts clear text.
	 * @param {Uint8Array} clearText Clear text to encrypt.
	 * @param {Uint8Array} iv IV bytes.
	 * @returns {Uint8Array} Cipher text.
	 */
	encrypt(clearText, iv) {
		const cipher = crypto.createCipheriv('aes-256-cbc', toBufferView(this._key), toBufferView(iv));

		const cipherText = cipher.update(toBufferView(clearText));
		const padding = cipher.final();

		return concatArrays(cipherText, padding);
	}

	/**
	 * Decrypts cipher text.
	 * @param {Uint8Array} cipherText Cipher text to decrypt.
	 * @param {Uint8Array} iv IV bytes.
	 * @returns {Uint8Array} Clear text.
	 */
	decrypt(cipherText, iv) {
		const decipher = crypto.createDecipheriv('aes-256-cbc', toBufferView(this._key), toBufferView(iv));

		const clearText = decipher.update(toBufferView(cipherText));
		const padding = decipher.final();

		return concatArrays(clearText, padding);
	}
}

// endregion

// region AesGcmCipher

/**
 * Performs AES GCM encryption and decryption with a given key.
 */
export class AesGcmCipher {
	/**
	 * Byte size of GCM tag.
	 * @type number
	 */
	static TAG_SIZE = 16;

	/**
	 * Creates a cipher around an aes shared key.
	 * @param {SharedKey256} aesKey AES shared key.
	 */
	constructor(aesKey) {
		/**
		 * @private
		 */
		this._key = aesKey;
	}

	/**
	 * Encrypts clear text and appends tag to encrypted payload.
	 * @param {Uint8Array} clearText Clear text to encrypt.
	 * @param {Uint8Array} iv IV bytes.
	 * @returns {Uint8Array} Cipher text with appended tag.
	 */
	encrypt(clearText, iv) {
		const cipher = crypto.createCipheriv('aes-256-gcm', toBufferView(this._key), toBufferView(iv));

		const cipherText = cipher.update(toBufferView(clearText));
		cipher.final(); // no padding for GCM

		const tag = cipher.getAuthTag();

		return concatArrays(cipherText, tag);
	}

	/**
	 * Decrypts cipher text with appended tag.
	 * @param {Uint8Array} cipherText Cipher text with appended tag to decrypt.
	 * @param {Uint8Array} iv IV bytes.
	 * @returns {Uint8Array} Clear text.
	 */
	decrypt(cipherText, iv) {
		const decipher = crypto.createDecipheriv('aes-256-gcm', toBufferView(this._key), toBufferView(iv));

		const tagStartOffset = cipherText.length - AesGcmCipher.TAG_SIZE;
		decipher.setAuthTag(Buffer.from(cipherText.buffer, tagStartOffset));

		const clearText = decipher.update(Buffer.from(cipherText.buffer, 0, tagStartOffset));
		decipher.final(); // no padding for GCM
		return clearText;
	}
}

// endregion