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

Add PBKDF2 to @web5/crypto #262

Merged
merged 6 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
shamilovtim marked this conversation as resolved.
Show resolved Hide resolved

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
Loading