Skip to content

Commit

Permalink
Add PBKDF2 to @web5/crypto (#262)
Browse files Browse the repository at this point in the history
* Add PBKDF2 to crypto package with full test coverage
* Use crypto package PBKDF2 in AppDataVault
  • Loading branch information
frankhinek authored Nov 7, 2023
1 parent a2758b9 commit 23429e7
Show file tree
Hide file tree
Showing 17 changed files with 827 additions and 21 deletions.
20 changes: 8 additions & 12 deletions packages/agent/src/app-data-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/agent/src/types/managed-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/crypto/src/algorithms-api/crypto-key.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/crypto/src/algorithms-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions packages/crypto/src/algorithms-api/pbkdf/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './pbkdf2.js';
91 changes: 91 additions & 0 deletions packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 readonly keyUsages: Web5Crypto.KeyUsage[] = ['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.iterations) === 'Number')) {
throw new TypeError(`Algorithm 'iterations' 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<Uint8Array> {
throw new InvalidAccessError(`Requested operation 'decrypt' is not valid for ${this.name} keys.`);
}

public override async encrypt(): Promise<Uint8Array> {
throw new InvalidAccessError(`Requested operation 'encrypt' is not valid for ${this.name} keys.`);
}

public override async generateKey(): Promise<Web5Crypto.CryptoKey> {
throw new InvalidAccessError(`Requested operation 'generateKey' is not valid for ${this.name} keys.`);
}

public override async sign(): Promise<Uint8Array> {
throw new InvalidAccessError(`Requested operation 'sign' is not valid for ${this.name} keys.`);
}

public override async verify(): Promise<boolean> {
throw new InvalidAccessError(`Requested operation 'verify' is not valid for ${this.name} keys.`);
}
}
1 change: 1 addition & 0 deletions packages/crypto/src/crypto-algorithms/index.ts
Original file line number Diff line number Diff line change
@@ -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';
54 changes: 54 additions & 0 deletions packages/crypto/src/crypto-algorithms/pbkdf2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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<Uint8Array> {
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 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.`);
}

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<Web5Crypto.CryptoKey> {
const { format, keyData, algorithm, extractable, keyUsages } = options;

this.checkImportKey({ algorithm, format, extractable, keyUsages });

const cryptoKey = new CryptoKey(algorithm, extractable, keyData, 'secret', keyUsages);

return cryptoKey;
}
}
1 change: 1 addition & 0 deletions packages/crypto/src/crypto-primitives/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './pbkdf2.js';
export * from './x25519.js';
export * from './aes-ctr.js';
export * from './aes-gcm.js';
Expand Down
78 changes: 78 additions & 0 deletions packages/crypto/src/crypto-primitives/pbkdf2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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<Uint8Array> {
if (isWebCryptoSupported()) {
return Pbkdf2.deriveKeyWithWebCrypto(options);
} else {
return Pbkdf2.deriveKeyWithNodeCrypto(options);
}
}

private static async deriveKeyWithNodeCrypto(options: DeriveKeyOptions): Promise<Uint8Array> {
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 the `crypto` package.
const { pbkdf2 } = await import('node:crypto');

return new Promise((resolve) => {
pbkdf2(
password,
salt,
iterations,
length,
hash,
(err, derivedKey) => {
if (!err) {
resolve(new Uint8Array(derivedKey));
}
}
);
});
}

private static async deriveKeyWithWebCrypto(options: DeriveKeyOptions): Promise<Uint8Array> {
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;
}
}
4 changes: 2 additions & 2 deletions packages/crypto/src/jose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ export class Jose {
}

public static webCryptoToJose(options:
Web5Crypto.GenerateKeyOptions
Web5Crypto.Algorithm | Web5Crypto.GenerateKeyOptions
): Partial<JsonWebKey> {
const params: string[] = [];

Expand All @@ -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());

/**
Expand Down
10 changes: 9 additions & 1 deletion packages/crypto/src/types/web5-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,6 +64,8 @@ export namespace Web5Crypto {
name: string;
}

export type KeyFormat = 'jwk' | 'pkcs8' | 'raw' | 'spki';

export interface KeyPairUsage {
privateKey: KeyUsage[];
publicKey: KeyUsage[];
Expand Down Expand Up @@ -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';
}
Loading

0 comments on commit 23429e7

Please sign in to comment.