generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add PBKDF2 to crypto package with full test coverage * Use crypto package PBKDF2 in AppDataVault
- Loading branch information
1 parent
a2758b9
commit 23429e7
Showing
17 changed files
with
827 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './pbkdf2.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.