From 5fd7ef370fc19043ffb6859c7b451c73db233d3e Mon Sep 17 00:00:00 2001 From: larabr Date: Wed, 21 Aug 2024 12:59:23 +0200 Subject: [PATCH] Drop asmcrypto.js for AES fallbacks in favor of noble-ciphers (#1785) Asm.js has now been deprecated for many years, and no performance gain is recorded for AES compared to vanilla JS. The relevant AES fallback code is primarily used if the WebCrypto (resp. NodeCrypto) implementation is not available. --- package-lock.json | 29 ++++++----- package.json | 2 +- src/crypto/aes_kw.js | 98 ++-------------------------------- src/crypto/cmac.js | 5 +- src/crypto/mode/cfb.js | 116 +++++++++++++++++++++++++++++++++++++---- src/crypto/mode/eax.js | 5 +- src/crypto/mode/gcm.js | 10 ++-- src/crypto/mode/ocb.js | 7 +-- 8 files changed, 138 insertions(+), 134 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd99b765..3462369f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "6.0.0-beta.2", "license": "LGPL-3.0+", "devDependencies": { + "@noble/ciphers": "^0.6.0", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", - "@openpgp/asmcrypto.js": "^3.1.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/tweetnacl": "^1.0.4-1", @@ -801,6 +801,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/ciphers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.6.0.tgz", + "integrity": "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==", + "dev": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", @@ -860,12 +869,6 @@ "node": ">= 8" } }, - "node_modules/@openpgp/asmcrypto.js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@openpgp/asmcrypto.js/-/asmcrypto.js-3.1.0.tgz", - "integrity": "sha512-LlQZE/Vtkx/KFnJxg7BB0iwD7oYKDeC8eRECHxKLhYyL2Ad0+xT137VZwv8SZTJB2euPqpx7xkj04ieV0Q665w==", - "dev": true - }, "node_modules/@openpgp/jsdoc": { "version": "3.6.11", "resolved": "https://registry.npmjs.org/@openpgp/jsdoc/-/jsdoc-3.6.11.tgz", @@ -9041,6 +9044,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@noble/ciphers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.6.0.tgz", + "integrity": "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==", + "dev": true + }, "@noble/curves": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", @@ -9082,12 +9091,6 @@ "fastq": "^1.6.0" } }, - "@openpgp/asmcrypto.js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@openpgp/asmcrypto.js/-/asmcrypto.js-3.1.0.tgz", - "integrity": "sha512-LlQZE/Vtkx/KFnJxg7BB0iwD7oYKDeC8eRECHxKLhYyL2Ad0+xT137VZwv8SZTJB2euPqpx7xkj04ieV0Q665w==", - "dev": true - }, "@openpgp/jsdoc": { "version": "3.6.11", "resolved": "https://registry.npmjs.org/@openpgp/jsdoc/-/jsdoc-3.6.11.tgz", diff --git a/package.json b/package.json index 51f026cb..89ad6293 100644 --- a/package.json +++ b/package.json @@ -62,9 +62,9 @@ "postversion": "git push && git push --tags && npm publish" }, "devDependencies": { + "@noble/ciphers": "^0.6.0", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", - "@openpgp/asmcrypto.js": "^3.1.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/tweetnacl": "^1.0.4-1", diff --git a/src/crypto/aes_kw.js b/src/crypto/aes_kw.js index 52288b31..0ef54f78 100644 --- a/src/crypto/aes_kw.js +++ b/src/crypto/aes_kw.js @@ -21,7 +21,7 @@ * @module crypto/aes_kw */ -import { AES_CBC } from '@openpgp/asmcrypto.js/aes/cbc.js'; +import { aeskw as nobleAesKW } from '@noble/ciphers/aes'; import { getCipherParams } from './cipher'; import util from '../util'; @@ -55,7 +55,7 @@ export async function wrap(algo, key, dataToWrap) { util.printDebugError('Browser did not support operation: ' + err.message); } - return asmcryptoWrap(algo, key, dataToWrap); + return nobleAesKW(key).encrypt(dataToWrap); } /** @@ -82,7 +82,7 @@ export async function unwrap(algo, key, wrappedData) { throw err; } util.printDebugError('Browser did not support operation: ' + err.message); - return asmcryptoUnwrap(algo, key, wrappedData); + return nobleAesKW(key).decrypt(wrappedData); } try { @@ -95,95 +95,3 @@ export async function unwrap(algo, key, wrappedData) { throw err; } } - -function asmcryptoWrap(aesAlgo, key, data) { - const aesInstance = new AES_CBC(key, new Uint8Array(16), false); - const IV = new Uint32Array([0xA6A6A6A6, 0xA6A6A6A6]); - const P = unpack(data); - let A = IV; - const R = P; - const n = P.length / 2; - const t = new Uint32Array([0, 0]); - let B = new Uint32Array(4); - for (let j = 0; j <= 5; ++j) { - for (let i = 0; i < n; ++i) { - t[1] = n * j + (1 + i); - // B = A - B[0] = A[0]; - B[1] = A[1]; - // B = A || R[i] - B[2] = R[2 * i]; - B[3] = R[2 * i + 1]; - // B = AES(K, B) - B = unpack(aesInstance.encrypt(pack(B))); - // A = MSB(64, B) ^ t - A = B.subarray(0, 2); - A[0] ^= t[0]; - A[1] ^= t[1]; - // R[i] = LSB(64, B) - R[2 * i] = B[2]; - R[2 * i + 1] = B[3]; - } - } - return pack(A, R); -} - -function asmcryptoUnwrap(aesAlgo, key, data) { - const aesInstance = new AES_CBC(key, new Uint8Array(16), false); - const IV = new Uint32Array([0xA6A6A6A6, 0xA6A6A6A6]); - const C = unpack(data); - let A = C.subarray(0, 2); - const R = C.subarray(2); - const n = C.length / 2 - 1; - const t = new Uint32Array([0, 0]); - let B = new Uint32Array(4); - for (let j = 5; j >= 0; --j) { - for (let i = n - 1; i >= 0; --i) { - t[1] = n * j + (i + 1); - // B = A ^ t - B[0] = A[0] ^ t[0]; - B[1] = A[1] ^ t[1]; - // B = (A ^ t) || R[i] - B[2] = R[2 * i]; - B[3] = R[2 * i + 1]; - // B = AES-1(B) - B = unpack(aesInstance.decrypt(pack(B))); - // A = MSB(64, B) - A = B.subarray(0, 2); - // R[i] = LSB(64, B) - R[2 * i] = B[2]; - R[2 * i + 1] = B[3]; - } - } - if (A[0] === IV[0] && A[1] === IV[1]) { - return pack(R); - } - throw new Error('Key Data Integrity failed'); -} - -function unpack(data) { - const buffer = data.buffer; - const view = new DataView(buffer); - const arr = new Uint32Array(data.length / 4); - for (let i = 0; i < data.length / 4; ++i) { - arr[i] = view.getUint32(4 * i); - } - return arr; -} - -function pack() { - let length = 0; - for (let k = 0; k < arguments.length; ++k) { - length += 4 * arguments[k].length; - } - const buffer = new ArrayBuffer(length); - const view = new DataView(buffer); - let offset = 0; - for (let i = 0; i < arguments.length; ++i) { - for (let j = 0; j < arguments[i].length; ++j) { - view.setUint32(offset + 4 * j, arguments[i][j]); - } - offset += 4 * arguments[i].length; - } - return new Uint8Array(buffer); -} diff --git a/src/crypto/cmac.js b/src/crypto/cmac.js index 19d996a8..6159cdf7 100644 --- a/src/crypto/cmac.js +++ b/src/crypto/cmac.js @@ -4,7 +4,7 @@ * @module crypto/cmac */ -import { AES_CBC } from '@openpgp/asmcrypto.js/aes/cbc.js'; +import { cbc as nobleAesCbc } from '@noble/ciphers/aes'; import util from '../util'; const webCrypto = util.getWebCrypto(); @@ -97,8 +97,7 @@ async function CBC(key) { } } - // asm.js fallback return async function(pt) { - return AES_CBC.encrypt(pt, key, false, zeroBlock); + return nobleAesCbc(key, zeroBlock, { disablePadding: true }).encrypt(pt); }; } diff --git a/src/crypto/mode/cfb.js b/src/crypto/mode/cfb.js index 128b28e9..829a0d29 100644 --- a/src/crypto/mode/cfb.js +++ b/src/crypto/mode/cfb.js @@ -21,7 +21,8 @@ * @module crypto/mode/cfb */ -import { AES_CFB } from '@openpgp/asmcrypto.js/aes/cfb.js'; +import { cfb as nobleAesCfb, unsafe as nobleAesHelpers } from '@noble/ciphers/aes'; + import * as stream from '@openpgp/web-stream-tools'; import util from '../../util'; import enums from '../../enums'; @@ -174,17 +175,17 @@ class WebCryptoEncryptor { const encryptedBlocks = await this._runCBC(toEncrypt); xorMut(encryptedBlocks, plaintext); - this.prevBlock = encryptedBlocks.subarray(-this.blockSize).slice(); + this.prevBlock = encryptedBlocks.slice(-this.blockSize); // take care of leftover data - if (leftover > 0) this.nextBlock.set(value.subarray(-leftover).slice()); + if (leftover > 0) this.nextBlock.set(value.subarray(-leftover)); this.i = leftover; return encryptedBlocks; } this.i += added.length; - let encryptedBlock = new Uint8Array(); + let encryptedBlock; if (this.i === this.nextBlock.length) { // block ready to be encrypted const curBlock = this.nextBlock; encryptedBlock = await this._runCBC(this.prevBlock); @@ -195,6 +196,8 @@ class WebCryptoEncryptor { const remaining = value.subarray(added.length); this.nextBlock.set(remaining, this.i); this.i += remaining.length; + } else { + encryptedBlock = new Uint8Array(); } return encryptedBlock; @@ -237,22 +240,111 @@ class WebCryptoEncryptor { } } +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; + } + + 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; + } + + 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) ? stream.transform(pt, value => cfb.encryptChunk(value), () => cfb.finish()) : cfb.encrypt(pt); - } else { - const cfb = new AES_CFB(key, iv); - return stream.transform(pt, value => cfb.aes.AES_Encrypt_process(value), () => cfb.aes.AES_Encrypt_finish()); + } else if (util.isStream(pt)) { // async callbacks are not accepted by stream.transform unless the input is a stream + const cfb = new NobleStreamProcessor(true, algo, key, iv); + return stream.transform(pt, value => cfb.processChunk(value), () => cfb.finish()); } + return nobleAesCfb(key, iv).encrypt(pt); } -function aesDecrypt(algo, key, ct, iv) { +async function aesDecrypt(algo, key, ct, iv) { if (util.isStream(ct)) { - const cfb = new AES_CFB(key, iv); - return stream.transform(ct, value => cfb.aes.AES_Decrypt_process(value), () => cfb.aes.AES_Decrypt_finish()); + const cfb = new NobleStreamProcessor(false, algo, key, iv); + return stream.transform(ct, value => cfb.processChunk(value), () => cfb.finish()); } - return AES_CFB.decrypt(ct, key, iv); + return nobleAesCfb(key, iv).decrypt(ct); } function xorMut(a, b) { @@ -262,6 +354,8 @@ function xorMut(a, b) { } } +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); diff --git a/src/crypto/mode/eax.js b/src/crypto/mode/eax.js index ab64ec6b..1ea2284d 100644 --- a/src/crypto/mode/eax.js +++ b/src/crypto/mode/eax.js @@ -21,7 +21,7 @@ * @module crypto/mode/eax */ -import { AES_CTR } from '@openpgp/asmcrypto.js/aes/ctr.js'; +import { ctr as nobleAesCtr } from '@noble/ciphers/aes'; import CMAC from '../cmac'; import util from '../../util'; import enums from '../../enums'; @@ -72,9 +72,8 @@ async function CTR(key) { } } - // asm.js fallback return async function(pt, iv) { - return AES_CTR.encrypt(pt, key, iv); + return nobleAesCtr(key, iv).encrypt(pt); }; } diff --git a/src/crypto/mode/gcm.js b/src/crypto/mode/gcm.js index b482a5dc..b1d2cfe4 100644 --- a/src/crypto/mode/gcm.js +++ b/src/crypto/mode/gcm.js @@ -21,7 +21,7 @@ * @module crypto/mode/gcm */ -import { AES_GCM } from '@openpgp/asmcrypto.js/aes/gcm.js'; +import { gcm as nobleAesGcm } from '@noble/ciphers/aes'; import util from '../../util'; import enums from '../../enums'; @@ -74,7 +74,7 @@ async function GCM(cipher, key) { return { encrypt: async function(pt, iv, adata = new Uint8Array()) { if (webcryptoEmptyMessagesUnsupported && !pt.length) { - return AES_GCM.encrypt(pt, key, iv, adata); + return nobleAesGcm(key, iv, adata).encrypt(pt); } const ct = await webCrypto.encrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, pt); return new Uint8Array(ct); @@ -82,7 +82,7 @@ async function GCM(cipher, key) { decrypt: async function(ct, iv, adata = new Uint8Array()) { if (webcryptoEmptyMessagesUnsupported && ct.length === tagLength) { - return AES_GCM.decrypt(ct, key, iv, adata); + return nobleAesGcm(key, iv, adata).decrypt(ct); } try { const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, ct); @@ -106,11 +106,11 @@ async function GCM(cipher, key) { return { encrypt: async function(pt, iv, adata) { - return AES_GCM.encrypt(pt, key, iv, adata); + return nobleAesGcm(key, iv, adata).encrypt(pt); }, decrypt: async function(ct, iv, adata) { - return AES_GCM.decrypt(ct, key, iv, adata); + return nobleAesGcm(key, iv, adata).decrypt(ct); } }; } diff --git a/src/crypto/mode/ocb.js b/src/crypto/mode/ocb.js index 868c67b9..e8b8cabf 100644 --- a/src/crypto/mode/ocb.js +++ b/src/crypto/mode/ocb.js @@ -20,7 +20,7 @@ * @module crypto/mode/ocb */ -import { AES_CBC } from '@openpgp/asmcrypto.js/aes/cbc.js'; +import { cbc as nobleAesCbc } from '@noble/ciphers/aes'; import { getCipherParams } from '../cipher'; import util from '../../util'; @@ -73,8 +73,9 @@ async function OCB(cipher, key) { // `encipher` and `decipher` cannot be async, since `crypt` shares state across calls, // hence its execution cannot be broken up. // As a result, WebCrypto cannot currently be used for `encipher`. - const encipher = block => AES_CBC.encrypt(block, key, false); - const decipher = block => AES_CBC.decrypt(block, key, false); + const aes = nobleAesCbc(key, zeroBlock, { disablePadding: true }); + const encipher = block => aes.encrypt(block); + const decipher = block => aes.decrypt(block); let mask; constructKeyVariables(cipher, key);