From a895b3ffa5443f3b48e3fee2526c6c3b32e6c26d Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Mon, 6 Nov 2023 08:43:35 -0500 Subject: [PATCH] Add tests for Pbkdf2 crypto primitive Signed-off-by: Frank Hinek --- .../crypto/src/crypto-primitives/pbkdf2.ts | 8 +- .../crypto/tests/crypto-primitives.spec.ts | 121 ++++++++++++++++++ 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/packages/crypto/src/crypto-primitives/pbkdf2.ts b/packages/crypto/src/crypto-primitives/pbkdf2.ts index e44b6fa12..d10e60cc6 100644 --- a/packages/crypto/src/crypto-primitives/pbkdf2.ts +++ b/packages/crypto/src/crypto-primitives/pbkdf2.ts @@ -33,10 +33,10 @@ export class Pbkdf2 { // Convert length from bits to bytes. const length = options.length / 8; - // Dynamically import node:crypto. + // Dynamically import the `crypto` package. const { pbkdf2 } = await import('node:crypto'); - return new Promise((resolve, reject) => { + return new Promise((resolve) => { pbkdf2( password, salt, @@ -44,9 +44,7 @@ export class Pbkdf2 { length, hash, (err, derivedKey) => { - if (err) { - reject(err); - } else { + if (!err) { resolve(new Uint8Array(derivedKey)); } } diff --git a/packages/crypto/tests/crypto-primitives.spec.ts b/packages/crypto/tests/crypto-primitives.spec.ts index a5de3877e..5c3c7d40d 100644 --- a/packages/crypto/tests/crypto-primitives.spec.ts +++ b/packages/crypto/tests/crypto-primitives.spec.ts @@ -1,5 +1,6 @@ import type { BytesKeyPair } from '../src/types/crypto-key.js'; +import sinon from 'sinon'; import chai, { expect } from 'chai'; import { Convert } from '@web5/common'; import chaiAsPromised from 'chai-as-promised'; @@ -13,6 +14,7 @@ import { AesGcm, ConcatKdf, Ed25519, + Pbkdf2, Secp256k1, X25519, XChaCha20, @@ -525,6 +527,125 @@ describe('Cryptographic Primitive Implementations', () => { }); }); + describe('Pbkdf2', () => { + const password = Convert.string('password').toUint8Array(); + const salt = Convert.string('salt').toUint8Array(); + const iterations = 1; + const length = 256; // 32 bytes + + describe('deriveKey', () => { + it('successfully derives a key using WebCrypto, if available', async () => { + const subtleDeriveBitsSpy = sinon.spy(crypto.subtle, 'deriveBits'); + + const derivedKey = await Pbkdf2.deriveKey({ hash: 'SHA-256', password, salt, iterations, length }); + + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(length / 8); + expect(subtleDeriveBitsSpy.called).to.be.true; + + subtleDeriveBitsSpy.restore(); + }); + + it('successfully derives a key using node:crypto when WebCrypto is not supported', async function () { + // Skip test in web browsers since node:crypto is not available. + if (typeof window !== 'undefined') this.skip(); + + // Ensure that WebCrypto is not available for this test. + sinon.stub(crypto, 'subtle').value(null); + + // @ts-expect-error because we're spying on a private method. + const nodeCryptoDeriveKeySpy = sinon.spy(Pbkdf2, 'deriveKeyWithNodeCrypto'); + + const derivedKey = await Pbkdf2.deriveKey({ hash: 'SHA-256', password, salt, iterations, length }); + + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(length / 8); + expect(nodeCryptoDeriveKeySpy.called).to.be.true; + + nodeCryptoDeriveKeySpy.restore(); + sinon.restore(); + }); + + it('derives the same value with node:crypto and WebCrypto', async function () { + // Skip test in web browsers since node:crypto is not available. + if (typeof window !== 'undefined') this.skip(); + + const options = { hash: 'SHA-256', password, salt, iterations, length }; + + // @ts-expect-error because we're testing a private method. + const webCryptoDerivedKey = await Pbkdf2.deriveKeyWithNodeCrypto(options); + // @ts-expect-error because we're testing a private method. + const nodeCryptoDerivedKey = await Pbkdf2.deriveKeyWithWebCrypto(options); + + expect(webCryptoDerivedKey).to.deep.equal(nodeCryptoDerivedKey); + }); + + const hashFunctions: ('SHA-256' | 'SHA-384' | 'SHA-512')[] = ['SHA-256', 'SHA-384', 'SHA-512']; + hashFunctions.forEach(hash => { + it(`handles ${hash} hash function`, async () => { + const options = { hash, password, salt, iterations, length }; + + const derivedKey = await Pbkdf2.deriveKey(options); + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(length / 8); + }); + }); + + it('throws an error when an invalid hash function is used with WebCrypto', async () => { + const options = { + hash: 'SHA-2' as const, password, salt, iterations, length + }; + + // @ts-expect-error for testing purposes + await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); + }); + + it('throws an error when an invalid hash function is used with node:crypto', async function () { + // Skip test in web browsers since node:crypto is not available. + if (typeof window !== 'undefined') this.skip(); + + // Ensure that WebCrypto is not available for this test. + sinon.stub(crypto, 'subtle').value(null); + + const options = { + hash: 'SHA-2' as const, password, salt, iterations, length + }; + + // @ts-expect-error for testing purposes + await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); + + sinon.restore(); + }); + + it('throws an error when iterations count is not a positive number with WebCrypto', async () => { + const options = { + hash : 'SHA-256' as const, password, salt, + iterations : -1, length + }; + + // Every browser throws a different error message so a specific message cannot be checked. + await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); + }); + + it('throws an error when iterations count is not a positive number with node:crypto', async function () { + // Skip test in web browsers since node:crypto is not available. + if (typeof window !== 'undefined') this.skip(); + + // Ensure that WebCrypto is not available for this test. + sinon.stub(crypto, 'subtle').value(null); + + const options = { + hash : 'SHA-256' as const, password, salt, + iterations : -1, length + }; + + await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error, 'out of range'); + + sinon.restore(); + }); + }); + }); + describe('Secp256k1', () => { describe('convertPublicKey method', () => { it('converts an uncompressed public key to a compressed format', async () => {