From 25f6e780bda2e7736340f6fcae97a3fe7fb3e7ff Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Sun, 5 Nov 2023 11:57:43 -0500 Subject: [PATCH 1/6] Add PBKDF2 to crypto package Signed-off-by: Frank Hinek --- packages/agent/src/types/managed-key.ts | 4 +- .../crypto/src/algorithms-api/crypto-key.ts | 4 +- packages/crypto/src/algorithms-api/index.ts | 1 + .../crypto/src/algorithms-api/pbkdf/index.ts | 1 + .../crypto/src/algorithms-api/pbkdf/pbkdf2.ts | 94 +++++++++++++++++++ .../crypto/src/crypto-algorithms/index.ts | 1 + .../crypto/src/crypto-algorithms/pbkdf2.ts | 50 ++++++++++ .../crypto/src/crypto-primitives/index.ts | 1 + .../crypto/src/crypto-primitives/pbkdf2.ts | 80 ++++++++++++++++ packages/crypto/src/jose.ts | 4 +- packages/crypto/src/types/web5-crypto.ts | 10 +- packages/crypto/src/utils.ts | 19 ++++ 12 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 packages/crypto/src/algorithms-api/pbkdf/index.ts create mode 100644 packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts create mode 100644 packages/crypto/src/crypto-algorithms/pbkdf2.ts create mode 100644 packages/crypto/src/crypto-primitives/pbkdf2.ts diff --git a/packages/agent/src/types/managed-key.ts b/packages/agent/src/types/managed-key.ts index 86f0aa84a..9a2e72503 100644 --- a/packages/agent/src/types/managed-key.ts +++ b/packages/agent/src/types/managed-key.ts @@ -73,7 +73,7 @@ export type DeriveBitsOptions = { /** * An object defining the derivation algorithm to use and its parameters. */ - algorithm: Web5Crypto.AlgorithmIdentifier | Web5Crypto.EcdhDeriveKeyOptions; + algorithm: Web5Crypto.AlgorithmIdentifier | Web5Crypto.EcdhDeriveKeyOptions | Web5Crypto.Pbkdf2Options; /** * An identifier of the ManagedKey that will be the input to the @@ -228,7 +228,7 @@ export interface ManagedKey { * An object detailing the algorithm for which the key can be used along * with additional algorithm-specific parameters. */ - algorithm: Web5Crypto.GenerateKeyOptions; + algorithm: Web5Crypto.KeyAlgorithm | Web5Crypto.GenerateKeyOptions; /** * An alternate identifier used to identify the key in a KMS. diff --git a/packages/crypto/src/algorithms-api/crypto-key.ts b/packages/crypto/src/algorithms-api/crypto-key.ts index 91d2477dc..fdc05a2c3 100644 --- a/packages/crypto/src/algorithms-api/crypto-key.ts +++ b/packages/crypto/src/algorithms-api/crypto-key.ts @@ -1,13 +1,13 @@ import type { Web5Crypto } from '../types/web5-crypto.js'; export class CryptoKey implements Web5Crypto.CryptoKey { - public algorithm: Web5Crypto.GenerateKeyOptions; + public algorithm: Web5Crypto.KeyAlgorithm | Web5Crypto.GenerateKeyOptions; public extractable: boolean; public material: Uint8Array; public type: Web5Crypto.KeyType; public usages: Web5Crypto.KeyUsage[]; - constructor (algorithm: Web5Crypto.GenerateKeyOptions, extractable: boolean, material: Uint8Array, type: Web5Crypto.KeyType, usages: Web5Crypto.KeyUsage[]) { + constructor (algorithm: Web5Crypto.Algorithm | Web5Crypto.GenerateKeyOptions, extractable: boolean, material: Uint8Array, type: Web5Crypto.KeyType, usages: Web5Crypto.KeyUsage[]) { this.algorithm = algorithm; this.extractable = extractable; this.material = material; diff --git a/packages/crypto/src/algorithms-api/index.ts b/packages/crypto/src/algorithms-api/index.ts index 2cc70ba28..fed29904f 100644 --- a/packages/crypto/src/algorithms-api/index.ts +++ b/packages/crypto/src/algorithms-api/index.ts @@ -2,4 +2,5 @@ export * from './errors.js'; export * from './ec/index.js'; export * from './aes/index.js'; export * from './crypto-key.js'; +export * from './pbkdf/index.js'; export * from './crypto-algorithm.js'; \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/pbkdf/index.ts b/packages/crypto/src/algorithms-api/pbkdf/index.ts new file mode 100644 index 000000000..d8f80e6e1 --- /dev/null +++ b/packages/crypto/src/algorithms-api/pbkdf/index.ts @@ -0,0 +1 @@ +export * from './pbkdf2.js'; \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts b/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts new file mode 100644 index 000000000..76c5e0e7d --- /dev/null +++ b/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts @@ -0,0 +1,94 @@ +import type { Web5Crypto } from '../../types/web5-crypto.js'; + +import { InvalidAccessError, OperationError } from '../errors.js'; +import { CryptoAlgorithm } from '../crypto-algorithm.js'; +import { checkRequiredProperty, checkValidProperty } from '../../utils.js'; +import { universalTypeOf } from '@web5/common'; + +export abstract class BasePbkdf2Algorithm extends CryptoAlgorithm { + + public readonly name: string = 'PBKDF2'; + + public readonly abstract hashAlgorithms: string[]; + + public keyUsages: Web5Crypto.KeyPairUsage = { + privateKey : ['deriveBits', 'deriveKey'], + publicKey : ['deriveBits', 'deriveKey'], + }; + + public checkAlgorithmOptions(options: { + algorithm: Web5Crypto.Pbkdf2Options, + baseKey: Web5Crypto.CryptoKey + }): void { + const { algorithm, baseKey } = options; + // Algorithm specified in the operation must match the algorithm implementation processing the operation. + this.checkAlgorithmName({ algorithmName: algorithm.name }); + // The algorithm object must contain a hash property. + checkRequiredProperty({ property: 'hash', inObject: algorithm }); + // The hash algorithm specified must be supported by the algorithm implementation processing the operation. + checkValidProperty({ property: algorithm.hash, allowedProperties: this.hashAlgorithms }); + // The algorithm object must contain a iterations property. + checkRequiredProperty({ property: 'iterations', inObject: algorithm }); + // The iterations value must a number. + if (!(universalTypeOf(algorithm.salt) === 'Number')) { + throw new TypeError(`Algorithm 'salt' is not of type: Number.`); + } + // The iterations value must be greater than 0. + if (algorithm.iterations < 1) { + throw new OperationError(`Algorithm 'iterations' must be > 0.`); + } + // The algorithm object must contain a salt property. + checkRequiredProperty({ property: 'salt', inObject: algorithm }); + // The salt must a Uint8Array. + if (!(universalTypeOf(algorithm.salt) === 'Uint8Array')) { + throw new TypeError(`Algorithm 'salt' is not of type: Uint8Array.`); + } + // The options object must contain a baseKey property. + checkRequiredProperty({ property: 'baseKey', inObject: options }); + // The baseKey object must be a CryptoKey. + this.checkCryptoKey({ key: baseKey }); + // The baseKey algorithm must match the algorithm implementation processing the operation. + this.checkKeyAlgorithm({ keyAlgorithmName: baseKey.algorithm.name }); + } + + public checkImportKey(options: { + algorithm: Web5Crypto.Algorithm, + format: Web5Crypto.KeyFormat, + extractable: boolean, + keyUsages: Web5Crypto.KeyUsage[] + }): void { + const { algorithm, format, extractable, keyUsages } = options; + // Algorithm specified in the operation must match the algorithm implementation processing the operation. + this.checkAlgorithmName({ algorithmName: algorithm.name }); + // The format specified must be 'raw'. + if (format !== 'raw') { + throw new SyntaxError(`Format '${format}' not supported. Only 'raw' is supported.`); + } + // The extractable value specified must be false. + if (extractable !== false) { + throw new SyntaxError(`Extractable '${extractable}' not supported. Only 'false' is supported.`); + } + // The key usages specified must be permitted by the algorithm implementation processing the operation. + this.checkKeyUsages({ keyUsages, allowedKeyUsages: this.keyUsages }); + } + + public override async decrypt(): Promise { + throw new InvalidAccessError(`Requested operation 'decrypt' is not valid for ${this.name} keys.`); + } + + public override async encrypt(): Promise { + throw new InvalidAccessError(`Requested operation 'encrypt' is not valid for ${this.name} keys.`); + } + + public override async generateKey(): Promise { + throw new InvalidAccessError(`Requested operation 'generateKey' is not valid for ${this.name} keys.`); + } + + public override async sign(): Promise { + throw new InvalidAccessError(`Requested operation 'sign' is not valid for ${this.name} keys.`); + } + + public override async verify(): Promise { + throw new InvalidAccessError(`Requested operation 'verify' is not valid for ${this.name} keys.`); + } +} \ No newline at end of file diff --git a/packages/crypto/src/crypto-algorithms/index.ts b/packages/crypto/src/crypto-algorithms/index.ts index 50b880f77..c8ce1fc84 100644 --- a/packages/crypto/src/crypto-algorithms/index.ts +++ b/packages/crypto/src/crypto-algorithms/index.ts @@ -1,4 +1,5 @@ export * from './ecdh.js'; export * from './ecdsa.js'; export * from './eddsa.js'; +export * from './pbkdf2.js'; export * from './aes-ctr.js'; \ No newline at end of file diff --git a/packages/crypto/src/crypto-algorithms/pbkdf2.ts b/packages/crypto/src/crypto-algorithms/pbkdf2.ts new file mode 100644 index 000000000..e579c8d07 --- /dev/null +++ b/packages/crypto/src/crypto-algorithms/pbkdf2.ts @@ -0,0 +1,50 @@ +import type { Web5Crypto } from '../types/web5-crypto.js'; + +import { BasePbkdf2Algorithm, CryptoKey, OperationError } from '../algorithms-api/index.js'; +import { Pbkdf2 } from '../crypto-primitives/pbkdf2.js'; + +export class Pbkdf2Algorithm extends BasePbkdf2Algorithm { + public readonly hashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512']; + + public async deriveBits(options: { + algorithm: Web5Crypto.Pbkdf2Options, + baseKey: Web5Crypto.CryptoKey, + length: number + }): Promise { + const { algorithm, baseKey, length } = options; + + this.checkAlgorithmOptions({ algorithm, baseKey }); + // The base key must be allowed to be used for deriveBits operations. + this.checkKeyUsages({ keyUsages: ['deriveBits'], allowedKeyUsages: baseKey.usages }); + // If the length is not a multiple of 8, throw. + if (length && length % 8 !== 0) { + throw new OperationError(`To be compatible with all browsers, 'length' must be a multiple of 8.`); + } + + const derivedBits = Pbkdf2.deriveKey({ + hash : algorithm.hash as 'SHA-256' | 'SHA-384' | 'SHA-512', + iterations : algorithm.iterations, + length : length, + password : baseKey.material, + salt : algorithm.salt + }); + + return derivedBits; + } + + public async importKey(options: { + format: Web5Crypto.KeyFormat, + keyData: Uint8Array, + algorithm: Web5Crypto.Algorithm, + extractable: boolean, + keyUsages: Web5Crypto.KeyUsage[] + }): Promise { + const { format, keyData, algorithm, extractable, keyUsages } = options; + + this.checkImportKey({ algorithm, format, extractable, keyUsages }); + + const cryptoKey = new CryptoKey(algorithm, extractable, keyData, 'secret', keyUsages); + + return cryptoKey; + } +} \ No newline at end of file diff --git a/packages/crypto/src/crypto-primitives/index.ts b/packages/crypto/src/crypto-primitives/index.ts index 790070b1b..0a7007862 100644 --- a/packages/crypto/src/crypto-primitives/index.ts +++ b/packages/crypto/src/crypto-primitives/index.ts @@ -1,3 +1,4 @@ +export * from './pbkdf2.js'; export * from './x25519.js'; export * from './aes-ctr.js'; export * from './aes-gcm.js'; diff --git a/packages/crypto/src/crypto-primitives/pbkdf2.ts b/packages/crypto/src/crypto-primitives/pbkdf2.ts new file mode 100644 index 000000000..e44b6fa12 --- /dev/null +++ b/packages/crypto/src/crypto-primitives/pbkdf2.ts @@ -0,0 +1,80 @@ +import { crypto } from '@noble/hashes/crypto'; + +import { isWebCryptoSupported } from '../utils.js'; + +type DeriveKeyOptions = { + hash: 'SHA-256' | 'SHA-384' | 'SHA-512', + password: Uint8Array, + salt: Uint8Array, + iterations: number, + length: number +}; + +export class Pbkdf2 { + public static async deriveKey(options: DeriveKeyOptions): Promise { + if (isWebCryptoSupported()) { + return Pbkdf2.deriveKeyWithWebCrypto(options); + } else { + return Pbkdf2.deriveKeyWithNodeCrypto(options); + } + } + + private static async deriveKeyWithNodeCrypto(options: DeriveKeyOptions): Promise { + const { password, salt, iterations } = options; + + // Map the hash string to the node:crypto equivalent. + const hashToNodeCryptoMap = { + 'SHA-256' : 'sha256', + 'SHA-384' : 'sha384', + 'SHA-512' : 'sha512' + }; + const hash = hashToNodeCryptoMap[options.hash]; + + // Convert length from bits to bytes. + const length = options.length / 8; + + // Dynamically import node:crypto. + const { pbkdf2 } = await import('node:crypto'); + + return new Promise((resolve, reject) => { + pbkdf2( + password, + salt, + iterations, + length, + hash, + (err, derivedKey) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(derivedKey)); + } + } + ); + }); + } + + private static async deriveKeyWithWebCrypto(options: DeriveKeyOptions): Promise { + const { hash, password, salt, iterations, length } = options; + + // Import the password as a raw key for use with the Web Crypto API. + const webCryptoKey = await crypto.subtle.importKey( + 'raw', + password, + { name: 'PBKDF2' }, + false, + ['deriveBits'] + ); + + const derivedKeyBuffer = await crypto.subtle.deriveBits( + { name: 'PBKDF2', hash, salt, iterations }, + webCryptoKey, + length + ); + + // Convert from ArrayBuffer to Uint8Array. + const derivedKey = new Uint8Array(derivedKeyBuffer); + + return derivedKey; + } +} \ No newline at end of file diff --git a/packages/crypto/src/jose.ts b/packages/crypto/src/jose.ts index 954337f02..bc9671f31 100644 --- a/packages/crypto/src/jose.ts +++ b/packages/crypto/src/jose.ts @@ -880,7 +880,7 @@ export class Jose { } public static webCryptoToJose(options: - Web5Crypto.GenerateKeyOptions + Web5Crypto.Algorithm | Web5Crypto.GenerateKeyOptions ): Partial { const params: string[] = []; @@ -900,7 +900,7 @@ export class Jose { * All symmetric encryption (AES) WebCrypto algorithms * set a value for the "length" parameter. */ - } else if (options.length !== undefined) { + } else if ('length' in options && options.length !== undefined) { params.push(options.length.toString()); /** diff --git a/packages/crypto/src/types/web5-crypto.ts b/packages/crypto/src/types/web5-crypto.ts index 03fa49f05..55d017009 100644 --- a/packages/crypto/src/types/web5-crypto.ts +++ b/packages/crypto/src/types/web5-crypto.ts @@ -21,7 +21,7 @@ export namespace Web5Crypto { export type AlgorithmIdentifier = Algorithm; export interface CryptoKey { - algorithm: Web5Crypto.GenerateKeyOptions; + algorithm: Web5Crypto.Algorithm; extractable: boolean; material: Uint8Array; type: KeyType; @@ -64,6 +64,8 @@ export namespace Web5Crypto { name: string; } + export type KeyFormat = 'jwk' | 'pkcs8' | 'raw' | 'spki'; + export interface KeyPairUsage { privateKey: KeyUsage[]; publicKey: KeyUsage[]; @@ -106,5 +108,11 @@ export namespace Web5Crypto { export type NamedCurve = string; + export interface Pbkdf2Options extends Algorithm { + hash: string; + iterations: number; + salt: Uint8Array; + } + export type PrivateKeyType = 'private' | 'secret'; } \ No newline at end of file diff --git a/packages/crypto/src/utils.ts b/packages/crypto/src/utils.ts index 5a9d3f165..a72312254 100644 --- a/packages/crypto/src/utils.ts +++ b/packages/crypto/src/utils.ts @@ -88,6 +88,25 @@ export function keyToMultibaseId(options: { return multibaseKeyId; } +export function isWebCryptoSupported(): boolean { + if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) { + // Web browser environment. + return true; + } else if (typeof global !== 'undefined' && global.crypto && global.crypto.subtle) { + // Node.js environment. + return true; + } else if (typeof self !== 'undefined' && self.crypto && self.crypto.subtle) { + // React Native environment. + return true; + } else if (typeof crypto !== 'undefined' && crypto.subtle) { + // Other environment (e.g. Web Worker). + return true; + } else { + // Web Crypto API is not supported. + return false; + } +} + export function multibaseIdToKey(options: { multibaseKeyId: string }): { key: Uint8Array, multicodecCode: number, multicodecName: string } { From 26daadba22907a04156554d66886e14c189ef780 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Sun, 5 Nov 2023 11:58:22 -0500 Subject: [PATCH 2/6] Use crypto package PBKDF2 in AppDataVault Signed-off-by: Frank Hinek --- packages/agent/src/app-data-store.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/agent/src/app-data-store.ts b/packages/agent/src/app-data-store.ts index 54e72928e..1d07068ae 100644 --- a/packages/agent/src/app-data-store.ts +++ b/packages/agent/src/app-data-store.ts @@ -5,10 +5,8 @@ import type { JweHeaderParams, PublicKeyJwk, Web5Crypto } from '@web5/crypto'; import { DidKeyMethod } from '@web5/dids'; import { hkdf } from '@noble/hashes/hkdf'; import { sha256 } from '@noble/hashes/sha256'; -import { sha512 } from '@noble/hashes/sha512'; -import { pbkdf2Async } from '@noble/hashes/pbkdf2'; import { Convert, MemoryStore } from '@web5/common'; -import { CryptoKey, Jose, utils as cryptoUtils, XChaCha20Poly1305 } from '@web5/crypto'; +import { CryptoKey, Jose, Pbkdf2, utils as cryptoUtils, XChaCha20Poly1305 } from '@web5/crypto'; export type AppDataBackup = { /** @@ -145,15 +143,13 @@ export class AppDataVault implements AppDataStore { /** The salt value derived in Step 3 and the passphrase entered by the * end-user are inputs to the PBKDF2 algorithm to derive a 32-byte secret * key that will be referred to as the Vault Unlock Key (VUK). */ - const vaultUnlockKey = await pbkdf2Async( - sha512, // hash function - passphrase, // password - salt, // salt - { - c : this._keyDerivationWorkFactor, // key derivation work factor - dkLen : 32 // derived key length, in bytes - } - ); + const vaultUnlockKey = await Pbkdf2.deriveKey({ + hash : 'SHA-512', + iterations : this._keyDerivationWorkFactor, + length : 256, + password : Convert.string(passphrase).toUint8Array(), + salt : salt + }); return vaultUnlockKey; } From a895b3ffa5443f3b48e3fee2526c6c3b32e6c26d Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Mon, 6 Nov 2023 08:43:35 -0500 Subject: [PATCH 3/6] 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 () => { From 7b3d937ae0bfad365410cdd2ca6fb6a4f191db15 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Mon, 6 Nov 2023 16:26:52 -0500 Subject: [PATCH 4/6] Remove errant reference to `self.crypto.subtle` is `isWebCryptoSupport()` function Signed-off-by: Frank Hinek --- packages/crypto/src/utils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/crypto/src/utils.ts b/packages/crypto/src/utils.ts index a72312254..f64644491 100644 --- a/packages/crypto/src/utils.ts +++ b/packages/crypto/src/utils.ts @@ -95,9 +95,6 @@ export function isWebCryptoSupported(): boolean { } else if (typeof global !== 'undefined' && global.crypto && global.crypto.subtle) { // Node.js environment. return true; - } else if (typeof self !== 'undefined' && self.crypto && self.crypto.subtle) { - // React Native environment. - return true; } else if (typeof crypto !== 'undefined' && crypto.subtle) { // Other environment (e.g. Web Worker). return true; From 560736c24f31886c0c7b22e67c47efc22ffcc284 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Tue, 7 Nov 2023 06:57:16 -0500 Subject: [PATCH 5/6] Simplify isWebCryptoSupported() and add docs/tests Signed-off-by: Frank Hinek --- packages/crypto/src/utils.ts | 37 ++++++++++++++++++++++------- packages/crypto/tests/utils.spec.ts | 29 +++++++++++++++++++++- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/packages/crypto/src/utils.ts b/packages/crypto/src/utils.ts index f64644491..0c448f099 100644 --- a/packages/crypto/src/utils.ts +++ b/packages/crypto/src/utils.ts @@ -88,18 +88,37 @@ export function keyToMultibaseId(options: { return multibaseKeyId; } +/** + * Checks if the Web Crypto API is supported in the current runtime environment. + * + * The function uses `globalThis` to provide a universal reference to the global + * scope, regardless of the environment. `globalThis` is a standard feature introduced + * in ECMAScript 2020 that is agnostic to the underlying JavaScript environment, making + * the code portable across browser, Node.js, and Web Workers environments. + * + * In a web browser, `globalThis` is equivalent to the `window` object. In Node.js, it + * is equivalent to the `global` object, and in Web Workers, it corresponds to `self`. + * + * This method checks for the `crypto` object and its `subtle` property on the global scope + * to determine the availability of the Web Crypto API. If both are present, the API is + * supported; otherwise, it is not. + * + * @returns A boolean indicating whether the Web Crypto API is supported in the current environment. + * + * Example usage: + * + * ```ts + * if (isWebCryptoSupported()) { + * console.log('Crypto operations can be performed'); + * } else { + * console.log('Crypto operations are not supported in this environment'); + * } + * ``` + */ export function isWebCryptoSupported(): boolean { - if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) { - // Web browser environment. - return true; - } else if (typeof global !== 'undefined' && global.crypto && global.crypto.subtle) { - // Node.js environment. - return true; - } else if (typeof crypto !== 'undefined' && crypto.subtle) { - // Other environment (e.g. Web Worker). + if (globalThis.crypto && globalThis.crypto.subtle) { return true; } else { - // Web Crypto API is not supported. return false; } } diff --git a/packages/crypto/tests/utils.spec.ts b/packages/crypto/tests/utils.spec.ts index d36bc472d..be88d1246 100644 --- a/packages/crypto/tests/utils.spec.ts +++ b/packages/crypto/tests/utils.spec.ts @@ -1,7 +1,16 @@ import { expect } from 'chai'; +import * as sinon from 'sinon'; import { CryptoKey } from '../src/algorithms-api/crypto-key.js'; -import { checkValidProperty, checkRequiredProperty, isCryptoKeyPair, randomUuid, multibaseIdToKey, keyToMultibaseId } from '../src/utils.js'; +import { + randomUuid, + isCryptoKeyPair, + keyToMultibaseId, + multibaseIdToKey, + checkValidProperty, + isWebCryptoSupported, + checkRequiredProperty, +} from '../src/utils.js'; describe('Crypto Utils', () => { describe('checkValidProperty()', () => { @@ -81,6 +90,24 @@ describe('Crypto Utils', () => { }); }); + describe('isWebCryptoSupported()', () => { + afterEach(() => { + // Restore the original state after each test + sinon.restore(); + }); + + it('returns true if the Web Crypto API is supported', () => { + expect(isWebCryptoSupported()).to.be.true; + }); + + it('returns false if Web Crypto API is not supported', function () { + // Mock an unsupported environment + sinon.stub(globalThis, 'crypto').value({}); + + expect(isWebCryptoSupported()).to.be.false; + }); + }); + describe('keyToMultibaseId()', () => { it('returns a multibase encoded string', () => { const input = { From ded70d44d67b7fef0ce9293e8bda55e92445f899 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Tue, 7 Nov 2023 09:08:06 -0500 Subject: [PATCH 6/6] Add remaining tests to cover PBKDF2 implementation Signed-off-by: Frank Hinek --- .../crypto/src/algorithms-api/pbkdf/pbkdf2.ts | 9 +- .../crypto/src/crypto-algorithms/pbkdf2.ts | 4 + packages/crypto/tests/algorithms-api.spec.ts | 251 +++++++++++++++++- .../crypto/tests/crypto-algorithms.spec.ts | 143 ++++++++++ 4 files changed, 400 insertions(+), 7 deletions(-) diff --git a/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts b/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts index 76c5e0e7d..de5ea703b 100644 --- a/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts +++ b/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts @@ -11,10 +11,7 @@ export abstract class BasePbkdf2Algorithm extends CryptoAlgorithm { public readonly abstract hashAlgorithms: string[]; - public keyUsages: Web5Crypto.KeyPairUsage = { - privateKey : ['deriveBits', 'deriveKey'], - publicKey : ['deriveBits', 'deriveKey'], - }; + public readonly keyUsages: Web5Crypto.KeyUsage[] = ['deriveBits', 'deriveKey']; public checkAlgorithmOptions(options: { algorithm: Web5Crypto.Pbkdf2Options, @@ -30,8 +27,8 @@ export abstract class BasePbkdf2Algorithm extends CryptoAlgorithm { // The algorithm object must contain a iterations property. checkRequiredProperty({ property: 'iterations', inObject: algorithm }); // The iterations value must a number. - if (!(universalTypeOf(algorithm.salt) === 'Number')) { - throw new TypeError(`Algorithm 'salt' is not of type: Number.`); + if (!(universalTypeOf(algorithm.iterations) === 'Number')) { + throw new TypeError(`Algorithm 'iterations' is not of type: Number.`); } // The iterations value must be greater than 0. if (algorithm.iterations < 1) { diff --git a/packages/crypto/src/crypto-algorithms/pbkdf2.ts b/packages/crypto/src/crypto-algorithms/pbkdf2.ts index e579c8d07..68f4aa12f 100644 --- a/packages/crypto/src/crypto-algorithms/pbkdf2.ts +++ b/packages/crypto/src/crypto-algorithms/pbkdf2.ts @@ -16,6 +16,10 @@ export class Pbkdf2Algorithm extends BasePbkdf2Algorithm { this.checkAlgorithmOptions({ algorithm, baseKey }); // The base key must be allowed to be used for deriveBits operations. this.checkKeyUsages({ keyUsages: ['deriveBits'], allowedKeyUsages: baseKey.usages }); + // If the length is 0, throw. + if (typeof length !== 'undefined' && length === 0) { + throw new OperationError(`The value of 'length' cannot be zero.`); + } // If the length is not a multiple of 8, throw. if (length && length % 8 !== 0) { throw new OperationError(`To be compatible with all browsers, 'length' must be a multiple of 8.`); diff --git a/packages/crypto/tests/algorithms-api.spec.ts b/packages/crypto/tests/algorithms-api.spec.ts index 69592f949..ae0d361c5 100644 --- a/packages/crypto/tests/algorithms-api.spec.ts +++ b/packages/crypto/tests/algorithms-api.spec.ts @@ -14,6 +14,7 @@ import { BaseEdDsaAlgorithm, InvalidAccessError, BaseAesCtrAlgorithm, + BasePbkdf2Algorithm, BaseEllipticCurveAlgorithm, } from '../src/algorithms-api/index.js'; @@ -369,7 +370,7 @@ describe('Algorithms API', () => { }); it('throws an error if the key property is missing', () => { - // @ts-expect-error because keyy property was intentionally omitted. + // @ts-expect-error because key property was intentionally omitted. expect(() => alg.checkAlgorithmOptions({ algorithm: { name : 'AES-CTR', counter : new Uint8Array(16), @@ -681,4 +682,252 @@ describe('Algorithms API', () => { }); }); }); + + describe('BasePbkdf2Algorithm', () => { + let alg: BasePbkdf2Algorithm; + + before(() => { + alg = Reflect.construct(BasePbkdf2Algorithm, []) as BasePbkdf2Algorithm; + // @ts-expect-error because `hashAlgorithms` is a read-only property. + alg.hashAlgorithms = ['SHA-256']; + }); + + describe('checkAlgorithmOptions()', () => { + + let baseKey: Web5Crypto.CryptoKey; + + beforeEach(() => { + baseKey = new CryptoKey({ name: 'PBKDF2' }, false, new Uint8Array(32), 'secret', ['deriveBits', 'deriveKey']); + }); + + it('does not throw with matching algorithm name and valid hash, iterations, and salt', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + iterations : 1000, + salt : new Uint8Array(16) + }, + baseKey + })).to.not.throw(); + }); + + it('throws an error when unsupported algorithm specified', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'invalid-name', + hash : 'SHA-256', + iterations : 1000, + salt : new Uint8Array(16) + }, + baseKey + })).to.throw(NotSupportedError, 'Algorithm not supported'); + }); + + it('throws an error if the hash property is missing', () => { + expect(() => alg.checkAlgorithmOptions({ + // @ts-expect-error because `hash` property is intentionally omitted. + algorithm: { + name : 'PBKDF2', + iterations : 1000, + salt : new Uint8Array(16) + }, + baseKey + })).to.throw(TypeError, 'Required parameter missing'); + }); + + it('throws an error if the given hash algorithm is not supported', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'PBKDF2', + hash : 'SHA-1', + iterations : 1000, + salt : new Uint8Array(16) + }, + baseKey + })).to.throw(TypeError, 'Out of range'); + }); + + it('throws an error if the iterations property is missing', () => { + expect(() => alg.checkAlgorithmOptions({ + // @ts-expect-error because `iterations` property is intentionally omitted. + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + salt : new Uint8Array(16) + }, + baseKey + })).to.throw(TypeError, 'Required parameter missing'); + }); + + it('throws error if iterations is not a number', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + // @ts-expect-error because `iterations` is intentionally defined as a string instead of a number. + iterations : '1000', + salt : new Uint8Array(16) + }, + baseKey + })).to.throw(TypeError, 'is not of type'); + }); + + it('throws error if iterations is not 1 or greater', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + iterations : 0, + salt : new Uint8Array(16) + }, + baseKey + })).to.throw(OperationError, 'must be > 0'); + }); + + it('throws an error if the salt property is missing', () => { + expect(() => alg.checkAlgorithmOptions({ + // @ts-expect-error because `salt` property is intentionally omitted. + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + + }, + baseKey + })).to.throw(TypeError, 'Required parameter missing'); + }); + + it('throws error if salt is not a Uint8Array', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + iterations : 1000, + // @ts-expect-error because counter is being intentionally set to the wrong data type to trigger an error. + salt : new Set([...Array(16).keys()].map(n => n.toString(16))) + }, + baseKey + })).to.throw(TypeError, 'is not of type'); + }); + + it('throws an error if the baseKey property is missing', () => { + // @ts-expect-error because baseKey property was intentionally omitted. + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + iterations : 1000, + salt : new Uint8Array(16) + }, + })).to.throw(TypeError, `Required parameter missing: 'baseKey'`); + }); + + it('throws an error if the given key is not valid', () => { + // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. + delete baseKey.extractable; + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + iterations : 1000, + salt : new Uint8Array(16) + }, + baseKey + })).to.throw(TypeError, 'Object is not a CryptoKey'); + }); + + it('throws an error if the algorithm of the key does not match', () => { + const baseKey = new CryptoKey({ name: 'wrong-algorithm' }, false, new Uint8Array(32), 'secret', ['deriveBits', 'deriveKey']); + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'PBKDF2', + hash : 'SHA-256', + iterations : 1000, + salt : new Uint8Array(16) + }, + baseKey + })).to.throw(InvalidAccessError, 'does not match'); + }); + }); + + describe('checkImportKey()', () => { + it('should not throw when all options are valid', () => { + expect(() => alg.checkImportKey({ + algorithm : { name: 'PBKDF2' }, + format : 'raw', + extractable : false, + keyUsages : ['deriveBits'] + })).to.not.throw(); + }); + + it('throws an error when unsupported algorithm specified', () => { + expect(() => alg.checkImportKey({ + algorithm : { name: 'ECDH' }, + format : 'raw', + extractable : false, + keyUsages : ['deriveBits'] + })).to.throw(NotSupportedError, 'Algorithm not supported'); + }); + + it('throws an error if the format is not raw', () => { + expect(() => alg.checkImportKey({ + algorithm : { name: 'PBKDF2' }, + format : 'pkcs8', // Invalid, only 'raw' is supported + extractable : false, + keyUsages : ['deriveBits'] + })).to.throw(SyntaxError, `Only 'raw' is supported`); + }); + + it('throws an error if extractable is not false', () => { + expect(() => alg.checkImportKey({ + algorithm : { name: 'PBKDF2' }, + format : 'raw', + extractable : true, + keyUsages : ['deriveBits'] + })).to.throw(SyntaxError, `Only 'false' is supported`); + }); + + it('throws an error when the requested operation is not valid', () => { + ['sign', 'verify'].forEach((operation) => { + expect(() => alg.checkImportKey({ + algorithm : { name: 'PBKDF2' }, + format : 'raw', + extractable : false, + keyUsages : [operation as KeyUsage] + })).to.throw(InvalidAccessError, 'Requested operation'); + }); + }); + }); + + describe('decrypt()', () => { + it(`throws an error because 'decrypt' operation is valid for PBKDF2 keys`, async () => { + await expect(alg.decrypt()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); + }); + }); + + describe('encrypt()', () => { + it(`throws an error because 'encrypt' operation is valid for PBKDF2 keys`, async () => { + await expect(alg.encrypt()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); + }); + }); + + describe('generateKey()', () => { + it(`throws an error because 'generateKey' operation is valid for PBKDF2 keys`, async () => { + await expect(alg.generateKey()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); + }); + }); + + describe('sign()', () => { + it(`throws an error because 'sign' operation is valid for PBKDF2 keys`, async () => { + await expect(alg.sign()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); + }); + }); + + describe('verify()', () => { + it(`throws an error because 'verify' operation is valid for PBKDF2 keys`, async () => { + await expect(alg.verify()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); + }); + }); + + }); }); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-algorithms.spec.ts b/packages/crypto/tests/crypto-algorithms.spec.ts index 83c817542..d62c8b200 100644 --- a/packages/crypto/tests/crypto-algorithms.spec.ts +++ b/packages/crypto/tests/crypto-algorithms.spec.ts @@ -13,6 +13,7 @@ import { EcdsaAlgorithm, EdDsaAlgorithm, AesCtrAlgorithm, + Pbkdf2Algorithm, } from '../src/crypto-algorithms/index.js'; chai.use(chaiAsPromised); @@ -1331,4 +1332,146 @@ describe('Default Crypto Algorithm Implementations', () => { }); }); }); + + describe('Pbkdf2Algorithm', () => { + let pbkdf2: Pbkdf2Algorithm; + + before(() => { + pbkdf2 = Pbkdf2Algorithm.create(); + }); + + describe('deriveBits()', () => { + let inputKey: Web5Crypto.CryptoKey; + + beforeEach(async () => { + inputKey = await pbkdf2.importKey({ + format : 'raw', + keyData : new Uint8Array([51, 52, 53]), + algorithm : { name: 'PBKDF2' }, + extractable : false, + keyUsages : ['deriveBits'] + }); + }); + + it('returns derived key as a Uint8Array', async () => { + const derivedKey = await pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 256 + }); + + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(256 / 8); + }); + + it('returns derived key with specified length, if possible', async () => { + let derivedKey = await pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 16 + }); + expect(derivedKey.byteLength).to.equal(16 / 8); + + derivedKey = await pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 512 + }); + expect(derivedKey.byteLength).to.equal(512 / 8); + + derivedKey = await pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 1024 + }); + expect(derivedKey.byteLength).to.equal(1024 / 8); + }); + + it('throws error if requested length is 0', async () => { + await expect(pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 0 + })).to.eventually.be.rejectedWith(OperationError, `cannot be zero`); + }); + + it('throws an error if the given length is not a multiple of 8', async () => { + await expect(pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 12 + })).to.eventually.be.rejectedWith(OperationError, `'length' must be a multiple of 8`); + }); + + it(`supports 'SHA-256' hash function`, async () => { + const derivedKey = await pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 256 + }); + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(32); + }); + + it(`supports 'SHA-384' hash function`, async () => { + const derivedKey = await pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-384', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 256 + }); + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(32); + }); + + it(`supports 'SHA-512' hash function`, async () => { + const derivedKey = await pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-512', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 256 + }); + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(32); + }); + + it(`throws an error for 'SHA-1' hash function`, async () => { + await expect(pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-1', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 256 + })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + }); + }); + + describe('importKey()', () => { + it('should import a key when all parameters are valid', async () => { + const key = await pbkdf2.importKey({ + format : 'raw', + keyData : new Uint8Array(16), + algorithm : { name: 'PBKDF2' }, + extractable : false, + keyUsages : ['deriveBits'] + }); + + expect(key).to.exist; + }); + + it('should return a Web5Crypto.CryptoKey object', async () => { + const key = await pbkdf2.importKey({ + format : 'raw', + keyData : new Uint8Array(16), + algorithm : { name: 'PBKDF2' }, + extractable : false, + keyUsages : ['deriveBits'] + }); + + expect(key).to.be.an('object'); + expect(key).to.have.property('algorithm').that.is.an('object'); + expect(key.algorithm).to.have.property('name', 'PBKDF2'); + expect(key).to.have.property('extractable', false); + expect(key).to.have.property('type', 'secret'); + expect(key).to.have.property('usages').that.includes.members(['deriveBits']); + expect(key).to.have.property('material').that.is.instanceOf(Uint8Array); + }); + }); + }); }); \ No newline at end of file