Skip to content

Commit

Permalink
Merge pull request #411 from PeculiarVentures/fix-pfx-speed
Browse files Browse the repository at this point in the history
Improve computation speed for makePKCS12B2Key
  • Loading branch information
microshine authored Jul 15, 2024
2 parents 4d659bf + 697c1a5 commit b3a107f
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 145 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
213 changes: 68 additions & 145 deletions src/CryptoEngine/CryptoEngine.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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; } {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions test/pkcs12SimpleExample.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as assert from "assert";
import * as crypto from "crypto";
import "./utils";
import * as example from "./pkcs12SimpleExample";
import { CryptoEngine } from "../src";
import { Convert } from "pvtsutils";

context("PKCS#12 Simple Example", () => {
const password = "12345567890";
Expand Down Expand Up @@ -47,4 +50,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.webcrypto as globalThis.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=");
});
});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
Expand Down

0 comments on commit b3a107f

Please sign in to comment.