From 632ababbd49c7e6b0f3c32addca765088fbe330c Mon Sep 17 00:00:00 2001 From: microshine Date: Mon, 15 Jul 2024 10:37:50 +0200 Subject: [PATCH 1/4] chore: Add @noble/hashes dependency --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 59f51a750..a348ddf0b 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "typescript": "^5.4.5" }, "dependencies": { + "@noble/hashes": "^1.4.0", "asn1js": "^3.0.5", "bytestreamjs": "^2.0.0", "pvtsutils": "^1.3.2", diff --git a/yarn.lock b/yarn.lock index 6b45c5289..c61dcc4eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -294,6 +294,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@noble/hashes@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" From 4092607c62b38b9d82efd5309d3db4ac3c1edb1f Mon Sep 17 00:00:00 2001 From: microshine Date: Mon, 15 Jul 2024 10:40:01 +0200 Subject: [PATCH 2/4] feat: Update makePKCS12B2Key to use @noble/hashes for improved computation speed --- src/CryptoEngine/CryptoEngine.ts | 213 ++++++++++--------------------- 1 file changed, 68 insertions(+), 145 deletions(-) diff --git a/src/CryptoEngine/CryptoEngine.ts b/src/CryptoEngine/CryptoEngine.ts index 45f63f48f..53c8be812 100644 --- a/src/CryptoEngine/CryptoEngine.ts +++ b/src/CryptoEngine/CryptoEngine.ts @@ -1,3 +1,6 @@ +import { sha1 } from "@noble/hashes/sha1"; +import { sha256 } from "@noble/hashes/sha256"; +import { sha384, sha512 } from "@noble/hashes/sha512"; import * as asn1js from "asn1js"; import * as pvutils from "pvutils"; import * as pvtsutils from "pvtsutils"; @@ -18,185 +21,105 @@ import { ECNamedCurves } from "../ECNamedCurves"; /** * Making MAC key using algorithm described in B.2 of PKCS#12 standard. */ -async function makePKCS12B2Key(cryptoEngine: CryptoEngine, hashAlgorithm: string, keyLength: number, password: ArrayBuffer, salt: ArrayBuffer, iterationCount: number) { - //#region Initial variables - let u: number; - let v: number; +async function makePKCS12B2Key(hashAlgorithm: string, keyLength: number, password: ArrayBuffer, salt: ArrayBuffer, iterationCount: number) { + let u: number; // Output length of the hash function + let v: number; // Block size of the hash function + let md: (input: Uint8Array) => Uint8Array; // Hash function - const result: number[] = []; - //#endregion - - //#region Get "u" and "v" values + // Determine the hash algorithm parameters switch (hashAlgorithm.toUpperCase()) { case "SHA-1": - u = 20; // 160 - v = 64; // 512 + u = 20; // 160 bits + v = 64; // 512 bits + md = sha1; break; case "SHA-256": - u = 32; // 256 - v = 64; // 512 + u = 32; // 256 bits + v = 64; // 512 bits + md = sha256; break; case "SHA-384": - u = 48; // 384 - v = 128; // 1024 + u = 48; // 384 bits + v = 128; // 1024 bits + md = sha384; break; case "SHA-512": - u = 64; // 512 - v = 128; // 1024 + u = 64; // 512 bits + v = 128; // 1024 bits + md = sha512; break; default: throw new Error("Unsupported hashing algorithm"); } - //#endregion - //#region Main algorithm making key - //#region Transform password to UTF-8 like string + // Transform the password into a null-terminated UCS-2 encoded string const passwordViewInitial = new Uint8Array(password); - - const passwordTransformed = new ArrayBuffer((password.byteLength * 2) + 2); - const passwordTransformedView = new Uint8Array(passwordTransformed); - + const passwordTransformed = new Uint8Array((password.byteLength * 2) + 2); for (let i = 0; i < passwordViewInitial.length; i++) { - passwordTransformedView[i * 2] = 0x00; - passwordTransformedView[i * 2 + 1] = passwordViewInitial[i]; + passwordTransformed[i * 2] = 0x00; + passwordTransformed[i * 2 + 1] = passwordViewInitial[i]; } - passwordTransformedView[passwordTransformedView.length - 2] = 0x00; - passwordTransformedView[passwordTransformedView.length - 1] = 0x00; - - password = passwordTransformed.slice(0); - //#endregion - - //#region Construct a string D (the "diversifier") by concatenating v/8 copies of ID - const D = new ArrayBuffer(v); - const dView = new Uint8Array(D); - - for (let i = 0; i < D.byteLength; i++) - dView[i] = 3; // The ID value equal to "3" for MACing (see B.3 of standard) - //#endregion - - //#region Concatenate copies of the salt together to create a string S of length v * ceil(s / v) bytes (the final copy of the salt may be trunacted to create S) - const saltLength = salt.byteLength; - - const sLen = v * Math.ceil(saltLength / v); - const S = new ArrayBuffer(sLen); - const sView = new Uint8Array(S); + // Create a filled array D with the value 3 (ID for MACing) + const D = new Uint8Array(v).fill(3); + // Repeat the salt to fill the block size const saltView = new Uint8Array(salt); + const S = new Uint8Array(v * Math.ceil(saltView.length / v)).map((_, i) => saltView[i % saltView.length]); - for (let i = 0; i < sLen; i++) - sView[i] = saltView[i % saltLength]; - //#endregion + // Repeat the password to fill the block size + const P = new Uint8Array(v * Math.ceil(passwordTransformed.length / v)).map((_, i) => passwordTransformed[i % passwordTransformed.length]); - //#region Concatenate copies of the password together to create a string P of length v * ceil(p / v) bytes (the final copy of the password may be truncated to create P) - const passwordLength = password.byteLength; + // Concatenate S and P to form I + let I = new Uint8Array(S.length + P.length); + I.set(S); + I.set(P, S.length); - const pLen = v * Math.ceil(passwordLength / v); - const P = new ArrayBuffer(pLen); - const pView = new Uint8Array(P); - - const passwordView = new Uint8Array(password); - - for (let i = 0; i < pLen; i++) - pView[i] = passwordView[i % passwordLength]; - //#endregion - - //#region Set I=S||P to be the concatenation of S and P - const sPlusPLength = S.byteLength + P.byteLength; - - let I = new ArrayBuffer(sPlusPLength); - let iView = new Uint8Array(I); - - iView.set(sView); - iView.set(pView, sView.length); - //#endregion - - //#region Set c=ceil(n / u) + // Calculate the number of hash iterations needed const c = Math.ceil((keyLength >> 3) / u); - //#endregion - - //#region Initial variables - let internalSequence = Promise.resolve(I); - //#endregion - - //#region For i=1, 2, ..., c, do the following: - for (let i = 0; i <= c; i++) { - internalSequence = internalSequence.then(_I => { - //#region Create contecanetion of D and I - const dAndI = new ArrayBuffer(D.byteLength + _I.byteLength); - const dAndIView = new Uint8Array(dAndI); - - dAndIView.set(dView); - dAndIView.set(iView, dView.length); - //#endregion - - return dAndI; - }); - - //#region Make "iterationCount" rounds of hashing - for (let j = 0; j < iterationCount; j++) - internalSequence = internalSequence.then(roundBuffer => cryptoEngine.digest({ name: hashAlgorithm }, new Uint8Array(roundBuffer))); - //#endregion - - internalSequence = internalSequence.then(roundBuffer => { - //#region Concatenate copies of Ai to create a string B of length v bits (the final copy of Ai may be truncated to create B) - const B = new ArrayBuffer(v); - const bView = new Uint8Array(B); - - for (let j = 0; j < B.byteLength; j++) - bView[j] = (roundBuffer as any)[j % roundBuffer.byteLength]; // TODO roundBuffer is ArrayBuffer. It doesn't have indexed values - //#endregion + const result: number[] = []; - //#region Make new I value - const k = Math.ceil(saltLength / v) + Math.ceil(passwordLength / v); - const iRound = []; + // Main loop to generate the key material + for (let i = 0; i < c; i++) { + // Concatenate D and I + let A = new Uint8Array(D.length + I.length); + A.set(D); + A.set(I, D.length); - let sliceStart = 0; - let sliceLength = v; + // Perform hash iterations + for (let j = 0; j < iterationCount; j++) { + A = md(A); + } - for (let j = 0; j < k; j++) { - const chunk = Array.from(new Uint8Array(I.slice(sliceStart, sliceStart + sliceLength))); - sliceStart += v; - if ((sliceStart + v) > I.byteLength) - sliceLength = I.byteLength - sliceStart; + // Create a repeated block B from the hash output A + const B = new Uint8Array(v).map((_, i) => A[i % A.length]); - let x = 0x1ff; + // Determine the number of blocks + const k = Math.ceil(saltView.length / v) + Math.ceil(passwordTransformed.length / v); + const iRound: number[] = []; - for (let l = (B.byteLength - 1); l >= 0; l--) { - x >>= 8; - x += bView[l] + chunk[l]; - chunk[l] = (x & 0xff); - } + // Adjust I based on B + for (let j = 0; j < k; j++) { + const chunk = Array.from(I.slice(j * v, (j + 1) * v)); + let x = 0x1ff; - iRound.push(...chunk); + for (let l = B.length - 1; l >= 0; l--) { + x >>= 8; + x += B[l] + (chunk[l] || 0); + chunk[l] = x & 0xff; } - I = new ArrayBuffer(iRound.length); - iView = new Uint8Array(I); - - iView.set(iRound); - //#endregion + iRound.push(...chunk); + } - result.push(...(new Uint8Array(roundBuffer))); + // Update I for the next iteration + I = new Uint8Array(iRound); - return I; - }); + // Collect the result + result.push(...A); } - //#endregion - - //#region Initialize final key - internalSequence = internalSequence.then(() => { - const resultBuffer = new ArrayBuffer(keyLength >> 3); - const resultView = new Uint8Array(resultBuffer); - - resultView.set((new Uint8Array(result)).slice(0, keyLength >> 3)); - - return resultBuffer; - }); - //#endregion - //#endregion - return internalSequence; + return new Uint8Array(result.slice(0, keyLength >> 3)).buffer; } function prepareAlgorithm(data: globalThis.AlgorithmIdentifier | EcdsaParams): Algorithm & { hash?: Algorithm; } { @@ -1773,7 +1696,7 @@ export class CryptoEngine extends AbstractCryptoEngine { //#endregion //#region Create PKCS#12 key for integrity checking - const pkcsKey = await makePKCS12B2Key(this, parameters.hashAlgorithm, length, parameters.password, parameters.salt, parameters.iterationCount); + const pkcsKey = await makePKCS12B2Key(parameters.hashAlgorithm, length, parameters.password, parameters.salt, parameters.iterationCount); //#endregion //#region Import HMAC key @@ -1829,7 +1752,7 @@ export class CryptoEngine extends AbstractCryptoEngine { //#endregion //#region Create PKCS#12 key for integrity checking - const pkcsKey = await makePKCS12B2Key(this, parameters.hashAlgorithm, length, parameters.password, parameters.salt, parameters.iterationCount); + const pkcsKey = await makePKCS12B2Key(parameters.hashAlgorithm, length, parameters.password, parameters.salt, parameters.iterationCount); //#endregion //#region Import HMAC key From d9b555707934486f842f93e67d7a5cb8bb15efe6 Mon Sep 17 00:00:00 2001 From: microshine Date: Mon, 15 Jul 2024 10:41:15 +0200 Subject: [PATCH 3/4] test: Added speed test for stampDataWithPassword --- test/pkcs12SimpleExample.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/pkcs12SimpleExample.spec.ts b/test/pkcs12SimpleExample.spec.ts index 98264cfe9..16cee2f26 100644 --- a/test/pkcs12SimpleExample.spec.ts +++ b/test/pkcs12SimpleExample.spec.ts @@ -1,6 +1,8 @@ import * as assert from "assert"; import "./utils"; import * as example from "./pkcs12SimpleExample"; +import { CryptoEngine } from "../src"; +import { Convert } from "pvtsutils"; context("PKCS#12 Simple Example", () => { const password = "12345567890"; @@ -47,4 +49,16 @@ context("PKCS#12 Simple Example", () => { const pfx = await example.openSSLLike(password); await example.parsePKCS12(pfx, password); }); + + it("Speed test for stampDataWithPassword", async () => { + const engine = new CryptoEngine({ name: "node", crypto: crypto }); + const encData = await engine.stampDataWithPassword({ + password: Convert.FromUtf8String(password), + salt: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), + iterationCount: 6e5, + hashAlgorithm: "SHA-256", + contentToStamp: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), + }); + assert.strictEqual(Convert.ToBase64(encData), "4iwFEULKTVUoMs1fF6EQ9q+vhr+DFeT10IRnVVSqKdg="); + }); }); From 697c1a506c700785fc33ef3734a0787861c8796d Mon Sep 17 00:00:00 2001 From: microshine Date: Mon, 15 Jul 2024 10:52:13 +0200 Subject: [PATCH 4/4] test: Correct CryptoEngine initialization error --- test/pkcs12SimpleExample.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pkcs12SimpleExample.spec.ts b/test/pkcs12SimpleExample.spec.ts index 16cee2f26..941da0ffc 100644 --- a/test/pkcs12SimpleExample.spec.ts +++ b/test/pkcs12SimpleExample.spec.ts @@ -1,4 +1,5 @@ import * as assert from "assert"; +import * as crypto from "crypto"; import "./utils"; import * as example from "./pkcs12SimpleExample"; import { CryptoEngine } from "../src"; @@ -51,7 +52,7 @@ context("PKCS#12 Simple Example", () => { }); it("Speed test for stampDataWithPassword", async () => { - const engine = new CryptoEngine({ name: "node", crypto: crypto }); + const engine = new CryptoEngine({ name: "node", crypto: crypto.webcrypto as globalThis.Crypto }); const encData = await engine.stampDataWithPassword({ password: Convert.FromUtf8String(password), salt: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]),