Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt
ESKDF is an experimental key derivation function designed for deriving multiple child keys from a single master seed. It combines Scrypt and PBKDF2 for initial key derivation, then uses HKDF for efficient child key derivation. The implementation is optimized for AES key generation and supports both numeric and string account identifiers with protocol-based domain separation.
Note: This is an experimental module. ESKDF uses HKDF in a non-standard way, making it "PRF-secure" but not "KDF-secure" in the formal sense. It assumes SHA2-256 retains preimage resistance.
import {
eskdf,
deriveMainSeed,
scrypt,
pbkdf2,
type ESKDF,
type AccountID,
type OptsLength,
type OptsMod,
type KeyOpts
} from '@noble/hashes/eskdf.js';Creates an ESKDF instance for deriving child keys from username and password credentials. This is the recommended entry point.
/**
* Creates ESKDF instance for deriving multiple child keys
* @param username - Username, email, or identifier (8-255 characters, should have sufficient entropy)
* @param password - Password (8-255 characters, should have sufficient entropy)
* @returns Promise resolving to ESKDF instance
*/
async function eskdf(username: string, password: string): Promise<ESKDF>;
/**
* ESKDF interface for deriving child keys
*/
interface ESKDF {
/**
* Derives a child key for specific protocol and account
* Each derived key is cryptographically independent
* @param protocol - Protocol name (3-15 lowercase alphanumeric characters, e.g., "aes", "ssh", "password")
* @param accountId - Account identifier (number 0 to 2^32-1, or string for supported protocols)
* @param options - Key generation options
* @returns Derived key as Uint8Array
*/
deriveChildKey(protocol: string, accountId?: AccountID, options?: KeyOpts): Uint8Array;
/**
* Deletes the main seed from memory (zeros and clears)
* After calling expire(), deriveChildKey() will throw errors
*/
expire(): void;
/**
* Account fingerprint for visual identification
* Format: 6 hex bytes separated by colons (e.g., "3A:F2:C1:94:0E:6F")
*/
fingerprint: string;
}
/**
* Account identifier type
* - number: 0 to 2^32-1 for all protocols
* - string: 1-255 characters, only for specific protocols (password*, ssh, tor, file)
*/
type AccountID = number | string;
/**
* Key length option (simple case)
*/
type OptsLength = {
/** Desired key length in bytes (16-8192) */
keyLength: number;
};
/**
* Modulus option (for RSA-like keys)
* Implements FIPS 186 B.4.1 with 64-bit bias reduction
* WARNING: This feature is experimental and may not work as expected in all cases.
* The modulus option may produce errors with certain keyLength or modulus values.
* Thorough testing is recommended before production use.
*/
type OptsMod = {
/** Modulus for key reduction (must be > 128) */
modulus: bigint;
};
/**
* Key generation options
* Use either keyLength OR modulus, not both
*/
type KeyOpts = undefined | OptsLength | OptsMod;Usage:
import { eskdf } from '@noble/hashes/eskdf.js';
// Create ESKDF instance (slow, involves Scrypt + PBKDF2)
const kdf = await eskdf('user@example.com', 'secure-password-123');
// Display fingerprint for verification
console.log('Fingerprint:', kdf.fingerprint); // e.g., "3A:F2:C1:94:0E:6F"
// Derive AES-256 encryption key
const aesKey = kdf.deriveChildKey('aes', 0, { keyLength: 32 });
// Derive different keys for different purposes
const authKey = kdf.deriveChildKey('auth', 0, { keyLength: 32 });
const signKey = kdf.deriveChildKey('signing', 0, { keyLength: 32 });
// Derive key for specific account
const sshKey1 = kdf.deriveChildKey('ssh', 0, { keyLength: 32 });
const sshKey2 = kdf.deriveChildKey('ssh', 1, { keyLength: 32 });
// Use string account ID for supported protocols
const fileKey = kdf.deriveChildKey('file', '/path/to/file.txt', { keyLength: 32 });
const passwordKey = kdf.deriveChildKey('password', 'github.com', { keyLength: 32 });
// Derive key with modulus (e.g., for RSA)
// WARNING: The modulus option is experimental and may not work as expected
const rsaKey = kdf.deriveChildKey('rsa', 0, {
modulus: BigInt('0x' + 'ff'.repeat(256)) // 2048-bit modulus
});
// Clean up when done
kdf.expire();Derives the main seed directly from username and password. This operation is slow (involves both Scrypt and PBKDF2). Prefer using eskdf() which returns an interface for efficient child key derivation.
/**
* Derives 32-byte main seed from username and password
* Combines Scrypt (N=2^19) and PBKDF2 (c=2^17) via XOR
* This operation is intentionally slow (~3-5 seconds)
* @param username - Username (8-255 characters)
* @param password - Password (8-255 characters)
* @returns 32-byte main seed
*/
function deriveMainSeed(username: string, password: string): Uint8Array;Usage:
import { deriveMainSeed } from '@noble/hashes/eskdf.js';
// Direct main seed derivation (slow)
const seed = deriveMainSeed('user@example.com', 'my-password');
// seed is a 32-byte Uint8Array
// Use this if you need the raw seed, otherwise prefer eskdf()Convenience wrappers for Scrypt and PBKDF2 with preset secure parameters.
/**
* Scrypt with preset parameters
* N=2^19 (524288), r=8, p=1, dkLen=32
* @param password - Password as string
* @param salt - Salt as string
* @returns 32-byte derived key
*/
function scrypt(password: string, salt: string): Uint8Array;
/**
* PBKDF2-HMAC-SHA256 with preset parameters
* c=2^17 (131072), dkLen=32
* @param password - Password as string
* @param salt - Salt as string
* @returns 32-byte derived key
*/
function pbkdf2(password: string, salt: string): Uint8Array;Usage:
import { scrypt, pbkdf2 } from '@noble/hashes/eskdf.js';
// Scrypt with preset secure parameters (N=2^19)
const scryptKey = scrypt('password', 'salt');
// PBKDF2 with preset secure parameters (c=2^17)
const pbkdf2Key = pbkdf2('password', 'salt');
// Both return 32-byte keys
// These are wrappers around the full scrypt/pbkdf2 implementationsimport { eskdf } from '@noble/hashes/eskdf.js';
async function setupEncryption(username: string, password: string) {
// Derive main seed (slow, do once per session)
const kdf = await eskdf(username, password);
// Show fingerprint to user for verification
console.log('Your fingerprint:', kdf.fingerprint);
// Derive encryption key
const encryptionKey = kdf.deriveChildKey('aes', 0, { keyLength: 32 });
// Derive MAC key
const macKey = kdf.deriveChildKey('hmac', 0, { keyLength: 32 });
return { encryptionKey, macKey, kdf };
}import { eskdf } from '@noble/hashes/eskdf.js';
async function deriveAccountKeys(username: string, password: string) {
const kdf = await eskdf(username, password);
// Derive keys for different accounts
const accounts = {
personal: kdf.deriveChildKey('aes', 0, { keyLength: 32 }),
work: kdf.deriveChildKey('aes', 1, { keyLength: 32 }),
shared: kdf.deriveChildKey('aes', 2, { keyLength: 32 })
};
return accounts;
}import { eskdf } from '@noble/hashes/eskdf.js';
import { bytesToHex } from '@noble/hashes/utils.js';
async function derivePasswordManagerKeys(masterPassword: string, email: string) {
const kdf = await eskdf(email, masterPassword);
// Derive site-specific passwords
const githubKey = kdf.deriveChildKey('password', 'github.com', { keyLength: 32 });
const googleKey = kdf.deriveChildKey('password', 'google.com', { keyLength: 32 });
const awsKey = kdf.deriveChildKey('password', 'aws.amazon.com', { keyLength: 32 });
// Convert to passwords (example: use first 16 bytes as base64-like)
function keyToPassword(key: Uint8Array): string {
return bytesToHex(key.slice(0, 16));
}
return {
github: keyToPassword(githubKey),
google: keyToPassword(googleKey),
aws: keyToPassword(awsKey),
fingerprint: kdf.fingerprint
};
}import { eskdf } from '@noble/hashes/eskdf.js';
async function deriveSshKeys(username: string, password: string, numKeys: number) {
const kdf = await eskdf(username, password);
const keys = [];
for (let i = 0; i < numKeys; i++) {
keys.push({
id: i,
key: kdf.deriveChildKey('ssh', i, { keyLength: 32 }),
fingerprint: kdf.fingerprint
});
}
return keys;
}import { eskdf } from '@noble/hashes/eskdf.js';
async function deriveFileEncryptionKey(
username: string,
password: string,
filepath: string
): Promise<Uint8Array> {
const kdf = await eskdf(username, password);
// Derive file-specific key using path as account ID
const key = kdf.deriveChildKey('file', filepath, { keyLength: 32 });
return key;
}
// Usage
const key1 = await deriveFileEncryptionKey('user', 'pass', '/docs/secret.txt');
const key2 = await deriveFileEncryptionKey('user', 'pass', '/docs/public.txt');
// key1 and key2 are differentWARNING: The modulus option is experimental and may not work as expected in all cases. This feature may produce errors with certain keyLength or modulus values. Thorough testing is recommended before production use.
import { eskdf } from '@noble/hashes/eskdf.js';
async function deriveRsaKey(username: string, password: string) {
const kdf = await eskdf(username, password);
// Define RSA modulus (example: 2048-bit)
const modulus = BigInt('0x' + 'ff'.repeat(256)); // Simplified example
// Derive key with modulus reduction
// Implements FIPS 186 B.4.1 for bias reduction
// WARNING: This may not work as expected - test thoroughly
const rsaKey = kdf.deriveChildKey('rsa', 0, { modulus });
return rsaKey;
}import { eskdf } from '@noble/hashes/eskdf.js';
async function secureKeyDerivation(username: string, password: string) {
const kdf = await eskdf(username, password);
try {
// Derive keys
const key1 = kdf.deriveChildKey('aes', 0, { keyLength: 32 });
const key2 = kdf.deriveChildKey('hmac', 0, { keyLength: 32 });
// Use keys...
performEncryption(key1, key2);
return { success: true };
} finally {
// Always clean up
kdf.expire();
// After expire(), deriveChildKey() will fail
}
}import { eskdf } from '@noble/hashes/eskdf.js';
async function verifyFingerprint(
username: string,
password: string,
expectedFingerprint: string
): Promise<boolean> {
const kdf = await eskdf(username, password);
try {
// Check if fingerprint matches
return kdf.fingerprint === expectedFingerprint;
} finally {
kdf.expire();
}
}
// Usage
const isValid = await verifyFingerprint(
'user@example.com',
'password123',
'3A:F2:C1:94:0E:6F'
);Main Seed Derivation (deriveMainSeed):
seed = key1 ⊕ key2Child Key Derivation (via ESKDF.deriveChildKey):
Main seed derivation (Scrypt + PBKDF2):
Child key derivation (HKDF):
Memory-Hardness: Scrypt component provides resistance to GPU/ASIC attacks.
Independence: Each child key is cryptographically independent due to HKDF's PRF properties.
Domain Separation: Protocol names provide domain separation between different use cases.
Bias Reduction: Modulus reduction implements FIPS 186 B.4.1, removing 0 and reducing bias.
Forward Secrecy: Calling expire() prevents future key derivation even if attacker gains access.
Protocol names must:
/^[a-z0-9]{3,15}$/Examples: aes, hmac, ssh, password, password2, file, tor
Numeric (all protocols):
String (specific protocols only):
password, password2, password3, ssh, tor, filekeyLength option:
modulus option:
ceil(log2(modulus)/8) + 8 bytes, then reducedWhy Experimental:
When to Use:
Username:
user@example.com, john.smith, alice.2024Password:
Password Manager: Derive site-specific passwords using protocol='password', accountId=domain
File Encryption: Derive file-specific keys using protocol='file', accountId=filepath
SSH Keys: Derive multiple SSH keys using protocol='ssh', accountId=0,1,2,...
AES Encryption: Derive encryption keys using protocol='aes', accountId=account_number
Multi-Protocol: Derive different keys for different protocols from same credentials
expire() zeros seed with fill(1) and sets to undefinedclean() functionInvalid Username/Password:
// Throws: "invalid username" or "invalid password"
await eskdf('short', 'password'); // Username too shortInvalid Protocol:
// Throws: "invalid protocol"
kdf.deriveChildKey('ABC', 0); // Uppercase not allowed
kdf.deriveChildKey('ab', 0); // Too shortInvalid Account ID:
// Throws: "accountId must be a number"
kdf.deriveChildKey('aes', 'string-id'); // String not allowed for 'aes'
// Throws: "invalid accountId"
kdf.deriveChildKey('ssh', -1); // Negative numberInvalid Options:
// Throws: "cannot combine keyLength and modulus options"
kdf.deriveChildKey('aes', 0, { keyLength: 32, modulus: 12345n });
// Throws: "invalid keyLength"
kdf.deriveChildKey('aes', 0, { keyLength: 8 }); // Too smallAfter Expiration:
kdf.expire();
// Throws: Assertion error (seed is undefined)
kdf.deriveChildKey('aes', 0); // Error after expire()vs HKDF:
vs PBKDF2:
vs Scrypt:
let kdfInstance: ESKDF | null = null;
async function getKDF(username: string, password: string): Promise<ESKDF> {
if (!kdfInstance) {
kdfInstance = await eskdf(username, password);
}
return kdfInstance;
}
function clearKDF() {
if (kdfInstance) {
kdfInstance.expire();
kdfInstance = null;
}
}async function deriveHierarchicalKeys(username: string, password: string) {
const kdf = await eskdf(username, password);
// Derive master keys for different purposes
const encryptionMaster = kdf.deriveChildKey('master', 0, { keyLength: 32 });
const signingMaster = kdf.deriveChildKey('master', 1, { keyLength: 32 });
// Use master keys for further derivation with standard HKDF
// (This is just an example pattern)
return { encryptionMaster, signingMaster };
}const fingerprintCache = new Map<string, string>();
async function getCachedFingerprint(username: string): Promise<string | undefined> {
return fingerprintCache.get(username);
}
async function verifyAndCacheFingerprint(
username: string,
password: string
): Promise<string> {
const kdf = await eskdf(username, password);
const fingerprint = kdf.fingerprint;
fingerprintCache.set(username, fingerprint);
return fingerprint;
}