-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
Signed-off-by: Frank Hinek <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './pbkdf2.js'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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.`); | ||
} | ||
} |
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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 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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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 node:crypto. | ||
const { pbkdf2 } = await import('node:crypto'); | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
frankhinek
Author
Contributor
|
||
|
||
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<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( | ||
This comment has been minimized.
Sorry, something went wrong.
shamilovtim
Contributor
|
||
{ name: 'PBKDF2', hash, salt, iterations }, | ||
webCryptoKey, | ||
length | ||
); | ||
|
||
// Convert from ArrayBuffer to Uint8Array. | ||
const derivedKey = new Uint8Array(derivedKeyBuffer); | ||
|
||
return derivedKey; | ||
} | ||
} |
could this just be
crypto
@frankhinek?I could be off but I'm pretty sure this strange import syntax
node:crypto
bites us where we polyfillglobalThis.crypto
orcrypto
which is not necessarilynode:crypto
.