Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slow PKCS #12 generation #403

Closed
pboguslawski opened this issue Apr 9, 2024 · 9 comments · Fixed by #411
Closed

Slow PKCS #12 generation #403

pboguslawski opened this issue Apr 9, 2024 · 9 comments · Fixed by #411
Assignees
Labels

Comments

@pboguslawski
Copy link

PKCS #12 generating with 600k iterations with code below (similar to openSSLLike example) takes 49-60s (tested in Firefox 115.9.1esr 64bit and Chromium 90 64-bit). Tested that most of the execution time is eaten by last makeInternalValues call (integrity protection envelope).

PKCS #12 generating with 600k iterations on same machine with openssl 3 from Debian 12 takes less than 1s:

$ time openssl pkcs12 -export -in test.crt -inkey test.key -out test.p12 -name 'test' -password pass:1234 -iter 600000

real	0m0.917s
user	0m0.894s
sys	0m0.000s

Is it expected or PKI.js bug?

PKCS #12 generation function code using PKI.js (PBKDF2_ITERATION_COUNT is set to 600000 during test):

// downloadPKCS12 downloads given private key and certificate in encrypted PKCS #12 file.
export async function downloadPKCS12(
	keyPair: CryptoKeyPair,
	certificate: Certificate,
	ownerId: string,
	password: string,
	filename: string
) {
	if (!password) {
		throw new Error('password cannot be empty');
	}
	if (!ownerId) {
		throw new Error('ownerId cannot be empty');
	}
	if (!filename) {
		throw new Error('filename cannot be empty');
	}

	const crypto = getCrypto(true);
	const passwordConverted = Convert.FromUtf8String(password);
	const certFingerprint = await crypto.digest('SHA-1', certificate.toSchema().toBER(false));
	const privateKeyBinary = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
	const pkcs8Simpl = new PrivateKeyInfo({ schema: fromBER(privateKeyBinary).result });

	// Put initial values for PKCS#12 structures.
	const pkcs12 = new PFX({
		parsedValue: {
			integrityMode: 0, // Password-Based Integrity Mode.
			authenticatedSafe: new AuthenticatedSafe({
				parsedValue: {
					safeContents: [
						{
							privacyMode: 1, // Password-Based Privacy Protection Mode.
							value: new SafeContents({
								safeBags: [
									new SafeBag({
										bagId: '1.2.840.113549.1.12.10.1.3',
										bagValue: new CertBag({
											parsedValue: certificate
										}),
										bagAttributes: [
											new Attribute({
												type: '1.2.840.113549.1.9.21', // localKeyID
												values: [new OctetString({ valueHex: certFingerprint })]
											}),
											new Attribute({
												type: '1.2.840.113549.1.9.20', // friendlyName
												values: [new BmpString({ value: ownerId })]
											})
										]
									})
								]
							})
						},
						{
							privacyMode: 0, // No-privacy Protection Mode.
							value: new SafeContents({
								safeBags: [
									new SafeBag({
										bagId: '1.2.840.113549.1.12.10.1.2',
										bagValue: new PKCS8ShroudedKeyBag({
											parsedValue: pkcs8Simpl
										}),
										bagAttributes: [
											new Attribute({
												type: '1.2.840.113549.1.9.21', // localKeyID
												values: [new OctetString({ valueHex: certFingerprint })]
											}),
											new Attribute({
												type: '1.2.840.113549.1.9.20', // friendlyName
												values: [new BmpString({ value: ownerId })]
											})
										]
									})
								]
							})
						}
					]
				}
			})
		}
	});

	// Encode internal values for PKCS8ShroudedKeyBag.
	if (!(pkcs12.parsedValue && pkcs12.parsedValue.authenticatedSafe)) {
		throw new Error('pkcs12.parsedValue.authenticatedSafe is empty');
	}
	await pkcs12.parsedValue.authenticatedSafe.parsedValue.safeContents[1].value.safeBags[0].bagValue.makeInternalValues(
		{
			password: passwordConverted,
			contentEncryptionAlgorithm: {
				name: 'AES-CBC', // OpenSSL can handle AES-CBC only.
				length: 256
			},
			hmacHashAlgorithm: 'SHA-256',
			iterationCount: PBKDF2_ITERATION_COUNT
		}
	);

	// Encode internal values for all SafeContents first (create all Privacy Protection envelopes).
	await pkcs12.parsedValue.authenticatedSafe.makeInternalValues({
		safeContents: [
			{
				password: passwordConverted,
				contentEncryptionAlgorithm: {
					name: 'AES-CBC', // OpenSSL can handle AES-CBC only.
					length: 256
				},
				hmacHashAlgorithm: 'SHA-256',
				iterationCount: PBKDF2_ITERATION_COUNT
			},
			{
				// Empty parameters for second SafeContent since No Privacy protection mode there.
			}
		]
	});

	// Encode internal values for Integrity Protection envelope.
	await pkcs12.makeInternalValues({
		password: passwordConverted,
		iterations: PBKDF2_ITERATION_COUNT, // Big value here causes long generation time.
		pbkdf2HashAlgorithm: 'SHA-256', // Least two parameters are equal because at the moment it is not clear how to use PBMAC1 schema with PKCS#12 integrity protection.
		hmacHashAlgorithm: 'SHA-256'
	});

	// Download prepared PKCS #12 content to file.
	downloadFile(filename, 'application/pkcs12', pkcs12.toSchema().toBER(false));
}
@janlingen
Copy link

I would also have high interest in a fix :D

@microshine microshine added the bug label Jul 13, 2024
@microshine microshine self-assigned this Jul 13, 2024
@microshine
Copy link
Contributor

I confirm that there is an issue with the implementation of the makePKCS12B2Key function. pkijs has two implementations, one for the browser and one for Node.js. I ran these functions with the same parameters and got different execution times:

node: 5.597s
webcrypto: 40.261s

We need to identify the cause of the slowdown and fix it. I will work on this.

@microshine
Copy link
Contributor

I made a small optimization to the code and managed to double the performance. However, it seems that the main cause is the implementation of WebCrypto. If you run the following script in the browser:

async function test() {
    const iterations = 6e5;
    const data = new Uint8Array(4);
    
    console.time("browser");
    for (let i = 0; i < iterations; i++) {
      await crypto.subtle.digest("SHA-256", data);
    }
    console.timeEnd("browser");
}

it completes in 14 seconds. The NodeJS implementation using the Crypto API finishes in 3 seconds.

@janlingen
Copy link

That sounds unfortunate, since I am running it in the browser. Thanks for your investigation so far, I am looking forward for a potential fix.

@microshine
Copy link
Contributor

I have just implemented a custom SHA-256 mechanism and used it instead of WebCrypto. This has significantly improved the execution speed, bringing it closer to the performance of the NodeJS Crypto API.

node: 3.079s

custom: 3.080s

webcrypto: 19.755s

I am considering applying this approach (adding custom SHA mechanisms) in CryptoEngine to address the performance issue.

@janlingen
Copy link

@microshine but 3s also seems to be a very long time compared to openssl right?

@microshine
Copy link
Contributor

Yes, it is longer compared to OpenSSL, but I don't see a way to make it faster. Running 600K iterations with hash calculations using the native NodeJS Crypto API, which leverages OpenSSL, takes a couple of seconds.

I have prepared an update that addresses the speed issue in the function, and it now completes in 2 seconds:
#411

@microshine microshine linked a pull request Jul 15, 2024 that will close this issue
@microshine
Copy link
Contributor

I have published a new version v3.2.0

@janlingen
Copy link

@microshine Thanks a lot, works very smooth now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants