CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-noble--hashes

Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt

Overview
Eval results
Files

eskdf.mddocs/

ESKDF: Experimental KDF for AES

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.

Imports

import {
  eskdf,
  deriveMainSeed,
  scrypt,
  pbkdf2,
  type ESKDF,
  type AccountID,
  type OptsLength,
  type OptsMod,
  type KeyOpts
} from '@noble/hashes/eskdf.js';

Capabilities

Main ESKDF Function

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();

Derive Main Seed

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()

Preset KDF Wrappers

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 implementations

Usage Examples

Basic Key Derivation

import { 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 };
}

Multiple Accounts

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;
}

Password Manager

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
  };
}

SSH Key Derivation

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;
}

File Encryption

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 different

RSA-Like Key with Modulus

WARNING: 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;
}

Key Expiration and Memory Safety

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
  }
}

Fingerprint Verification

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'
);

Technical Details

Algorithm Overview

Main Seed Derivation (deriveMainSeed):

  1. Derive key1 using Scrypt with N=2^19, r=8, p=1
  2. Derive key2 using PBKDF2-HMAC-SHA256 with c=2^17
  3. XOR the two keys: seed = key1 ⊕ key2
  4. Clean temporary keys from memory

Child Key Derivation (via ESKDF.deriveChildKey):

  1. Convert protocol and accountId to HKDF salt and info parameters
  2. Use HKDF-SHA256 with the main seed to derive child key
  3. If modulus option: apply FIPS 186 B.4.1 modulo reduction

Performance

Main seed derivation (Scrypt + PBKDF2):

  • Apple M4: ~3-5 seconds
  • This is intentionally slow for security

Child key derivation (HKDF):

  • Apple M4: <1ms per key
  • Very fast, can derive thousands of keys efficiently

Security Properties

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 Naming

Protocol names must:

  • Be 3-15 characters long
  • Contain only lowercase letters and numbers (a-z, 0-9)
  • Follow pattern: /^[a-z0-9]{3,15}$/

Examples: aes, hmac, ssh, password, password2, file, tor

Account ID Types

Numeric (all protocols):

  • Range: 0 to 2^32-1 (4,294,967,295)
  • Encoded as big-endian 32-bit integer

String (specific protocols only):

  • Allowed for: password, password2, password3, ssh, tor, file
  • Length: 1-255 characters
  • UTF-8 encoded

Key Length Constraints

keyLength option:

  • Minimum: 16 bytes (128 bits)
  • Maximum: 8192 bytes (65,536 bits)
  • Recommended: 32 bytes (256 bits) for symmetric encryption

modulus option:

  • Minimum: > 128 (checked as bigint)
  • Adds 64 bits (8 bytes) for bias reduction per FIPS 186
  • Final key length: ceil(log2(modulus)/8) + 8 bytes, then reduced
  • WARNING: Experimental feature that may not work correctly in all cases. Test thoroughly before production use.

Experimental Status

Why Experimental:

  • Uses HKDF in a non-standard way (PRF-secure, not KDF-secure)
  • Relies on SHA2-256 preimage resistance assumption
  • Not peer-reviewed for this specific application
  • Not standardized by NIST or IETF

When to Use:

  • Suitable for: AES key generation, password managers, SSH keys
  • Not for: Critical infrastructure, compliance-required systems without audit
  • Alternative: Use standard HKDF directly with standard key derivation

Credential Requirements

Username:

  • Length: 8-255 characters
  • Should have sufficient entropy (email addresses recommended)
  • Examples: user@example.com, john.smith, alice.2024

Password:

  • Length: 8-255 characters
  • Should have sufficient entropy
  • Minimum: 12+ characters recommended
  • Use strong, unique passwords

Common Use Cases

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

Memory Security

  • Main seed is stored in closure, not accessible externally
  • expire() zeros seed with fill(1) and sets to undefined
  • Temporary keys cleaned with clean() function
  • JavaScript provides no guarantees about memory cleanup (GC may leave copies)

Error Handling

Invalid Username/Password:

// Throws: "invalid username" or "invalid password"
await eskdf('short', 'password'); // Username too short

Invalid Protocol:

// Throws: "invalid protocol"
kdf.deriveChildKey('ABC', 0); // Uppercase not allowed
kdf.deriveChildKey('ab', 0);  // Too short

Invalid 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 number

Invalid 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 small

After Expiration:

kdf.expire();
// Throws: Assertion error (seed is undefined)
kdf.deriveChildKey('aes', 0); // Error after expire()

Comparison with Standard KDFs

vs HKDF:

  • ESKDF: Derives main seed with Scrypt+PBKDF2, then uses HKDF for children
  • HKDF: Standard expand-extract pattern, KDF-secure
  • Use ESKDF: When deriving many keys from password
  • Use HKDF: For standard key derivation from existing key material

vs PBKDF2:

  • ESKDF: Memory-hard via Scrypt component
  • PBKDF2: CPU-only, easier to attack
  • Use ESKDF: Better security for password-based derivation
  • Use PBKDF2: Standards compliance, wider compatibility

vs Scrypt:

  • ESKDF: Combines Scrypt + PBKDF2 + HKDF for flexibility
  • Scrypt: Single-purpose password hashing
  • Use ESKDF: Deriving multiple child keys
  • Use Scrypt: Simple password hashing

Common Patterns

Singleton KDF Instance

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;
  }
}

Hierarchical Key Derivation

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 };
}

Cached Fingerprint Verification

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;
}

References

  • HKDF (RFC 5869): HMAC-based Key Derivation Function
  • Scrypt (RFC 7914): Memory-Hard KDF
  • PBKDF2 (RFC 2898): Password-Based Key Derivation Function 2
  • FIPS 186 B.4.1: Modulo Bias Reduction

Install with Tessl CLI

npx tessl i tessl/npm-noble--hashes

docs

argon2.md

blake.md

eskdf.md

hkdf.md

hmac.md

index.md

legacy.md

pbkdf2.md

scrypt.md

sha2.md

sha3-addons.md

sha3.md

utils.md

webcrypto.md

tile.json