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.
This commit is contained in:
larabr 2024-08-21 12:59:23 +02:00 committed by GitHub
parent 79014f00f0
commit 5fd7ef370f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 138 additions and 134 deletions

29
package-lock.json generated
View File

@ -9,9 +9,9 @@
"version": "6.0.0-beta.2", "version": "6.0.0-beta.2",
"license": "LGPL-3.0+", "license": "LGPL-3.0+",
"devDependencies": { "devDependencies": {
"@noble/ciphers": "^0.6.0",
"@noble/curves": "^1.4.0", "@noble/curves": "^1.4.0",
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@openpgp/asmcrypto.js": "^3.1.0",
"@openpgp/jsdoc": "^3.6.11", "@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.4-1", "@openpgp/tweetnacl": "^1.0.4-1",
@ -801,6 +801,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@noble/curves": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz",
@ -860,12 +869,6 @@
"node": ">= 8" "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": { "node_modules/@openpgp/jsdoc": {
"version": "3.6.11", "version": "3.6.11",
"resolved": "https://registry.npmjs.org/@openpgp/jsdoc/-/jsdoc-3.6.11.tgz", "resolved": "https://registry.npmjs.org/@openpgp/jsdoc/-/jsdoc-3.6.11.tgz",
@ -9041,6 +9044,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "@noble/curves": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz",
@ -9082,12 +9091,6 @@
"fastq": "^1.6.0" "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": { "@openpgp/jsdoc": {
"version": "3.6.11", "version": "3.6.11",
"resolved": "https://registry.npmjs.org/@openpgp/jsdoc/-/jsdoc-3.6.11.tgz", "resolved": "https://registry.npmjs.org/@openpgp/jsdoc/-/jsdoc-3.6.11.tgz",

View File

@ -62,9 +62,9 @@
"postversion": "git push && git push --tags && npm publish" "postversion": "git push && git push --tags && npm publish"
}, },
"devDependencies": { "devDependencies": {
"@noble/ciphers": "^0.6.0",
"@noble/curves": "^1.4.0", "@noble/curves": "^1.4.0",
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@openpgp/asmcrypto.js": "^3.1.0",
"@openpgp/jsdoc": "^3.6.11", "@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.4-1", "@openpgp/tweetnacl": "^1.0.4-1",

View File

@ -21,7 +21,7 @@
* @module crypto/aes_kw * @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 { getCipherParams } from './cipher';
import util from '../util'; import util from '../util';
@ -55,7 +55,7 @@ export async function wrap(algo, key, dataToWrap) {
util.printDebugError('Browser did not support operation: ' + err.message); 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; throw err;
} }
util.printDebugError('Browser did not support operation: ' + err.message); util.printDebugError('Browser did not support operation: ' + err.message);
return asmcryptoUnwrap(algo, key, wrappedData); return nobleAesKW(key).decrypt(wrappedData);
} }
try { try {
@ -95,95 +95,3 @@ export async function unwrap(algo, key, wrappedData) {
throw err; 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);
}

View File

@ -4,7 +4,7 @@
* @module crypto/cmac * @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'; import util from '../util';
const webCrypto = util.getWebCrypto(); const webCrypto = util.getWebCrypto();
@ -97,8 +97,7 @@ async function CBC(key) {
} }
} }
// asm.js fallback
return async function(pt) { return async function(pt) {
return AES_CBC.encrypt(pt, key, false, zeroBlock); return nobleAesCbc(key, zeroBlock, { disablePadding: true }).encrypt(pt);
}; };
} }

View File

@ -21,7 +21,8 @@
* @module crypto/mode/cfb * @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 * as stream from '@openpgp/web-stream-tools';
import util from '../../util'; import util from '../../util';
import enums from '../../enums'; import enums from '../../enums';
@ -174,17 +175,17 @@ class WebCryptoEncryptor {
const encryptedBlocks = await this._runCBC(toEncrypt); const encryptedBlocks = await this._runCBC(toEncrypt);
xorMut(encryptedBlocks, plaintext); xorMut(encryptedBlocks, plaintext);
this.prevBlock = encryptedBlocks.subarray(-this.blockSize).slice(); this.prevBlock = encryptedBlocks.slice(-this.blockSize);
// take care of leftover data // 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; this.i = leftover;
return encryptedBlocks; return encryptedBlocks;
} }
this.i += added.length; this.i += added.length;
let encryptedBlock = new Uint8Array(); let encryptedBlock;
if (this.i === this.nextBlock.length) { // block ready to be encrypted if (this.i === this.nextBlock.length) { // block ready to be encrypted
const curBlock = this.nextBlock; const curBlock = this.nextBlock;
encryptedBlock = await this._runCBC(this.prevBlock); encryptedBlock = await this._runCBC(this.prevBlock);
@ -195,6 +196,8 @@ class WebCryptoEncryptor {
const remaining = value.subarray(added.length); const remaining = value.subarray(added.length);
this.nextBlock.set(remaining, this.i); this.nextBlock.set(remaining, this.i);
this.i += remaining.length; this.i += remaining.length;
} else {
encryptedBlock = new Uint8Array();
} }
return encryptedBlock; 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) { async function aesEncrypt(algo, key, pt, iv) {
if (webCrypto && await WebCryptoEncryptor.isSupported(algo)) { // Chromium does not implement AES with 192-bit keys if (webCrypto && await WebCryptoEncryptor.isSupported(algo)) { // Chromium does not implement AES with 192-bit keys
const cfb = new WebCryptoEncryptor(algo, key, iv); const cfb = new WebCryptoEncryptor(algo, key, iv);
return util.isStream(pt) ? stream.transform(pt, value => cfb.encryptChunk(value), () => cfb.finish()) : cfb.encrypt(pt); return util.isStream(pt) ? stream.transform(pt, value => cfb.encryptChunk(value), () => cfb.finish()) : cfb.encrypt(pt);
} else { } else if (util.isStream(pt)) { // async callbacks are not accepted by stream.transform unless the input is a stream
const cfb = new AES_CFB(key, iv); const cfb = new NobleStreamProcessor(true, algo, key, iv);
return stream.transform(pt, value => cfb.aes.AES_Encrypt_process(value), () => cfb.aes.AES_Encrypt_finish()); 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)) { if (util.isStream(ct)) {
const cfb = new AES_CFB(key, iv); const cfb = new NobleStreamProcessor(false, algo, key, iv);
return stream.transform(ct, value => cfb.aes.AES_Decrypt_process(value), () => cfb.aes.AES_Decrypt_finish()); 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) { 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) { function nodeEncrypt(algo, key, pt, iv) {
const algoName = enums.read(enums.symmetric, algo); const algoName = enums.read(enums.symmetric, algo);
const cipherObj = new nodeCrypto.createCipheriv(nodeAlgos[algoName], key, iv); const cipherObj = new nodeCrypto.createCipheriv(nodeAlgos[algoName], key, iv);

View File

@ -21,7 +21,7 @@
* @module crypto/mode/eax * @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 CMAC from '../cmac';
import util from '../../util'; import util from '../../util';
import enums from '../../enums'; import enums from '../../enums';
@ -72,9 +72,8 @@ async function CTR(key) {
} }
} }
// asm.js fallback
return async function(pt, iv) { return async function(pt, iv) {
return AES_CTR.encrypt(pt, key, iv); return nobleAesCtr(key, iv).encrypt(pt);
}; };
} }

View File

@ -21,7 +21,7 @@
* @module crypto/mode/gcm * @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 util from '../../util';
import enums from '../../enums'; import enums from '../../enums';
@ -74,7 +74,7 @@ async function GCM(cipher, key) {
return { return {
encrypt: async function(pt, iv, adata = new Uint8Array()) { encrypt: async function(pt, iv, adata = new Uint8Array()) {
if (webcryptoEmptyMessagesUnsupported && !pt.length) { 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); const ct = await webCrypto.encrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, pt);
return new Uint8Array(ct); return new Uint8Array(ct);
@ -82,7 +82,7 @@ async function GCM(cipher, key) {
decrypt: async function(ct, iv, adata = new Uint8Array()) { decrypt: async function(ct, iv, adata = new Uint8Array()) {
if (webcryptoEmptyMessagesUnsupported && ct.length === tagLength) { if (webcryptoEmptyMessagesUnsupported && ct.length === tagLength) {
return AES_GCM.decrypt(ct, key, iv, adata); return nobleAesGcm(key, iv, adata).decrypt(ct);
} }
try { try {
const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, ct); 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 { return {
encrypt: async function(pt, iv, adata) { 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) { decrypt: async function(ct, iv, adata) {
return AES_GCM.decrypt(ct, key, iv, adata); return nobleAesGcm(key, iv, adata).decrypt(ct);
} }
}; };
} }

View File

@ -20,7 +20,7 @@
* @module crypto/mode/ocb * @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 { getCipherParams } from '../cipher';
import util from '../../util'; 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, // `encipher` and `decipher` cannot be async, since `crypt` shares state across calls,
// hence its execution cannot be broken up. // hence its execution cannot be broken up.
// As a result, WebCrypto cannot currently be used for `encipher`. // As a result, WebCrypto cannot currently be used for `encipher`.
const encipher = block => AES_CBC.encrypt(block, key, false); const aes = nobleAesCbc(key, zeroBlock, { disablePadding: true });
const decipher = block => AES_CBC.decrypt(block, key, false); const encipher = block => aes.encrypt(block);
const decipher = block => aes.decrypt(block);
let mask; let mask;
constructKeyVariables(cipher, key); constructKeyVariables(cipher, key);