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);