// Modified by ProtonTech AG // Modified by Recurity Labs GmbH // modified version of https://www.hanewin.net/encrypt/PGdecode.js: /* OpenPGP encryption using RSA/AES * Copyright 2005-2006 Herbert Hanewinkel, www.haneWIN.de * version 2.0, check www.haneWIN.de for the latest version * This software is provided as-is, without express or implied warranty. * Permission to use, copy, modify, distribute or sell this software, with or * without fee, for any purpose and by any individual or organization, is hereby * granted, provided that the above copyright notice and this paragraph appear * in all copies. Distribution as a part of an application or binary must * include the above copyright notice in the documentation and/or other * materials provided with the application or distribution. */ /** * @module crypto/mode/cfb */ import { cfb as nobleAesCfb, unsafe as nobleAesHelpers } from '@noble/ciphers/aes'; import { transform as streamTransform } from '@openpgp/web-stream-tools'; import util from '../../util'; import enums from '../../enums'; import { getLegacyCipher, getCipherParams } from '../cipher'; import { getRandomBytes } from '../random'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); const knownAlgos = nodeCrypto ? nodeCrypto.getCiphers() : []; const nodeAlgos = { idea: knownAlgos.includes('idea-cfb') ? 'idea-cfb' : undefined, /* Unused, not implemented */ tripledes: knownAlgos.includes('des-ede3-cfb') ? 'des-ede3-cfb' : undefined, cast5: knownAlgos.includes('cast5-cfb') ? 'cast5-cfb' : undefined, blowfish: knownAlgos.includes('bf-cfb') ? 'bf-cfb' : undefined, aes128: knownAlgos.includes('aes-128-cfb') ? 'aes-128-cfb' : undefined, aes192: knownAlgos.includes('aes-192-cfb') ? 'aes-192-cfb' : undefined, aes256: knownAlgos.includes('aes-256-cfb') ? 'aes-256-cfb' : undefined /* twofish is not implemented in OpenSSL */ }; /** * Generates a random byte prefix for the specified algorithm * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. * @param {module:enums.symmetric} algo - Symmetric encryption algorithm * @returns {Promise} Random bytes with length equal to the block size of the cipher, plus the last two bytes repeated. */ export function getPrefixRandom(algo) { const { blockSize } = getCipherParams(algo); const prefixrandom = getRandomBytes(blockSize); const repeat = new Uint8Array([prefixrandom[prefixrandom.length - 2], prefixrandom[prefixrandom.length - 1]]); return util.concat([prefixrandom, repeat]); } /** * CFB encryption * @param {enums.symmetric} algo - block cipher algorithm * @param {Uint8Array} key * @param {MaybeStream} plaintext * @param {Uint8Array} iv * @param {Object} config - full configuration, defaults to openpgp.config * @returns MaybeStream */ export async function encrypt(algo, key, plaintext, iv, config) { const algoName = enums.read(enums.symmetric, algo); if (util.getNodeCrypto() && nodeAlgos[algoName]) { // Node crypto library. return nodeEncrypt(algo, key, plaintext, iv); } if (util.isAES(algo)) { return aesEncrypt(algo, key, plaintext, iv, config); } const LegacyCipher = await getLegacyCipher(algo); const cipherfn = new LegacyCipher(key); const block_size = cipherfn.blockSize; const blockc = iv.slice(); let pt = new Uint8Array(); const process = chunk => { if (chunk) { pt = util.concatUint8Array([pt, chunk]); } const ciphertext = new Uint8Array(pt.length); let i; let j = 0; while (chunk ? pt.length >= block_size : pt.length) { const encblock = cipherfn.encrypt(blockc); for (i = 0; i < block_size; i++) { blockc[i] = pt[i] ^ encblock[i]; ciphertext[j++] = blockc[i]; } pt = pt.subarray(block_size); } return ciphertext.subarray(0, j); }; return streamTransform(plaintext, process, process); } /** * CFB decryption * @param {enums.symmetric} algo - block cipher algorithm * @param {Uint8Array} key * @param {MaybeStream} ciphertext * @param {Uint8Array} iv * @returns MaybeStream */ export async function decrypt(algo, key, ciphertext, iv) { const algoName = enums.read(enums.symmetric, algo); if (nodeCrypto && nodeAlgos[algoName]) { // Node crypto library. return nodeDecrypt(algo, key, ciphertext, iv); } if (util.isAES(algo)) { return aesDecrypt(algo, key, ciphertext, iv); } const LegacyCipher = await getLegacyCipher(algo); const cipherfn = new LegacyCipher(key); const block_size = cipherfn.blockSize; let blockp = iv; let ct = new Uint8Array(); const process = chunk => { if (chunk) { ct = util.concatUint8Array([ct, chunk]); } const plaintext = new Uint8Array(ct.length); let i; let j = 0; while (chunk ? ct.length >= block_size : ct.length) { const decblock = cipherfn.encrypt(blockp); blockp = ct.subarray(0, block_size); for (i = 0; i < block_size; i++) { plaintext[j++] = blockp[i] ^ decblock[i]; } ct = ct.subarray(block_size); } return plaintext.subarray(0, j); }; return streamTransform(ciphertext, process, process); } class WebCryptoEncryptor { constructor(algo, key, iv) { const { blockSize } = getCipherParams(algo); this.key = key; this.prevBlock = iv; this.nextBlock = new Uint8Array(blockSize); this.i = 0; // pointer inside next block this.blockSize = blockSize; this.zeroBlock = new Uint8Array(this.blockSize); } /** * @returns {Promise} */ static isSupported(algo) { const { keySize } = getCipherParams(algo); return webCrypto.importKey('raw', new Uint8Array(keySize), 'aes-cbc', false, ['encrypt']) .then(() => true, () => false); } async _runCBC(plaintext, nonZeroIV) { const mode = 'AES-CBC'; this.keyRef = this.keyRef || await webCrypto.importKey('raw', this.key, mode, false, ['encrypt']); const ciphertext = await webCrypto.encrypt( { name: mode, iv: nonZeroIV || this.zeroBlock }, this.keyRef, plaintext ); return new Uint8Array(ciphertext).subarray(0, plaintext.length); } async encryptChunk(value) { const missing = this.nextBlock.length - this.i; const added = value.subarray(0, missing); this.nextBlock.set(added, this.i); if ((this.i + value.length) >= (2 * this.blockSize)) { const leftover = (value.length - missing) % this.blockSize; const plaintext = util.concatUint8Array([ this.nextBlock, value.subarray(missing, value.length - leftover) ]); const toEncrypt = util.concatUint8Array([ this.prevBlock, plaintext.subarray(0, plaintext.length - this.blockSize) // stop one block "early", since we only need to xor the plaintext and pass it over as prevBlock ]); const encryptedBlocks = await this._runCBC(toEncrypt); xorMut(encryptedBlocks, plaintext); this.prevBlock = encryptedBlocks.slice(-this.blockSize); // take care of leftover data if (leftover > 0) this.nextBlock.set(value.subarray(-leftover)); this.i = leftover; return encryptedBlocks; } this.i += added.length; let encryptedBlock; if (this.i === this.nextBlock.length) { // block ready to be encrypted const curBlock = this.nextBlock; encryptedBlock = await this._runCBC(this.prevBlock); xorMut(encryptedBlock, curBlock); this.prevBlock = encryptedBlock.slice(); this.i = 0; const remaining = value.subarray(added.length); this.nextBlock.set(remaining, this.i); this.i += remaining.length; } else { encryptedBlock = new Uint8Array(); } return encryptedBlock; } async finish() { let result; if (this.i === 0) { // nothing more to encrypt result = new Uint8Array(); } else { this.nextBlock = this.nextBlock.subarray(0, this.i); const curBlock = this.nextBlock; const encryptedBlock = await this._runCBC(this.prevBlock); xorMut(encryptedBlock, curBlock); result = encryptedBlock.subarray(0, curBlock.length); } this.clearSensitiveData(); return result; } clearSensitiveData() { this.nextBlock.fill(0); this.prevBlock.fill(0); this.keyRef = null; this.key = null; } async encrypt(plaintext) { // plaintext is internally padded to block length before encryption const encryptedWithPadding = await this._runCBC( util.concatUint8Array([new Uint8Array(this.blockSize), plaintext]), this.iv ); // drop encrypted padding const ct = encryptedWithPadding.subarray(0, plaintext.length); xorMut(ct, plaintext); this.clearSensitiveData(); return ct; } } class NobleStreamProcessor { constructor(forEncryption, algo, key, iv) { this.forEncryption = forEncryption; const { blockSize } = getCipherParams(algo); this.key = nobleAesHelpers.expandKeyLE(key); if (iv.byteOffset % 4 !== 0) iv = iv.slice(); // aligned arrays required by noble-ciphers this.prevBlock = getUint32Array(iv); this.nextBlock = new Uint8Array(blockSize); this.i = 0; // pointer inside next block this.blockSize = blockSize; } _runCFB(src) { const src32 = getUint32Array(src); const dst = new Uint8Array(src.length); const dst32 = getUint32Array(dst); for (let i = 0; i + 4 <= dst32.length; i += 4) { const { s0: e0, s1: e1, s2: e2, s3: e3 } = nobleAesHelpers.encrypt(this.key, this.prevBlock[0], this.prevBlock[1], this.prevBlock[2], this.prevBlock[3]); dst32[i + 0] = src32[i + 0] ^ e0; dst32[i + 1] = src32[i + 1] ^ e1; dst32[i + 2] = src32[i + 2] ^ e2; dst32[i + 3] = src32[i + 3] ^ e3; this.prevBlock = (this.forEncryption ? dst32 : src32).slice(i, i + 4); } return dst; } // eslint-disable-next-line @typescript-eslint/require-await async processChunk(value) { const missing = this.nextBlock.length - this.i; const added = value.subarray(0, missing); this.nextBlock.set(added, this.i); if ((this.i + value.length) >= (2 * this.blockSize)) { const leftover = (value.length - missing) % this.blockSize; const toProcess = util.concatUint8Array([ this.nextBlock, value.subarray(missing, value.length - leftover) ]); const processedBlocks = this._runCFB(toProcess); // take care of leftover data if (leftover > 0) this.nextBlock.set(value.subarray(-leftover)); this.i = leftover; return processedBlocks; } this.i += added.length; let processedBlock; if (this.i === this.nextBlock.length) { // block ready to be encrypted processedBlock = this._runCFB(this.nextBlock); this.i = 0; const remaining = value.subarray(added.length); this.nextBlock.set(remaining, this.i); this.i += remaining.length; } else { processedBlock = new Uint8Array(); } return processedBlock; } // eslint-disable-next-line @typescript-eslint/require-await async finish() { let result; if (this.i === 0) { // nothing more to encrypt result = new Uint8Array(); } else { const processedBlock = this._runCFB(this.nextBlock); result = processedBlock.subarray(0, this.i); } this.clearSensitiveData(); return result; } clearSensitiveData() { this.nextBlock.fill(0); this.prevBlock.fill(0); this.key.fill(0); } } async function aesEncrypt(algo, key, pt, iv) { if (webCrypto && await WebCryptoEncryptor.isSupported(algo)) { // Chromium does not implement AES with 192-bit keys const cfb = new WebCryptoEncryptor(algo, key, iv); return util.isStream(pt) ? streamTransform(pt, value => cfb.encryptChunk(value), () => cfb.finish()) : cfb.encrypt(pt); } else if (util.isStream(pt)) { // async callbacks are not accepted by streamTransform unless the input is a stream const cfb = new NobleStreamProcessor(true, algo, key, iv); return streamTransform(pt, value => cfb.processChunk(value), () => cfb.finish()); } return nobleAesCfb(key, iv).encrypt(pt); } function aesDecrypt(algo, key, ct, iv) { if (util.isStream(ct)) { const cfb = new NobleStreamProcessor(false, algo, key, iv); return streamTransform(ct, value => cfb.processChunk(value), () => cfb.finish()); } return nobleAesCfb(key, iv).decrypt(ct); } function xorMut(a, b) { const aLength = Math.min(a.length, b.length); for (let i = 0; i < aLength; i++) { a[i] = a[i] ^ b[i]; } } const getUint32Array = arr => new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); function nodeEncrypt(algo, key, pt, iv) { const algoName = enums.read(enums.symmetric, algo); const cipherObj = new nodeCrypto.createCipheriv(nodeAlgos[algoName], key, iv); return streamTransform(pt, value => new Uint8Array(cipherObj.update(value))); } function nodeDecrypt(algo, key, ct, iv) { const algoName = enums.read(enums.symmetric, algo); const decipherObj = new nodeCrypto.createDecipheriv(nodeAlgos[algoName], key, iv); return streamTransform(ct, value => new Uint8Array(decipherObj.update(value))); }