-
Notifications
You must be signed in to change notification settings - Fork 2
Crypt Generators
Cryptographic key pair generation, key derivation (PBKDF2, HKDF), random secrets, and BIP39 mnemonics.
Secure generation of cryptographic keys, derived keys (PBKDF2, HKDF), random secrets, and mnemonic phrases.
| Feature | Bun | Deno | Node.js | Workers | Browser |
|---|---|---|---|---|---|
| RSA key pairs | ✅ | ✅ | ✅ | ✅ | ✅ |
| EC key pairs | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ed25519 pairs | ✅ | ✅ | ✅ | ✅ | ✅ |
| Random secrets | ✅ | ✅ | ✅ | ✅ | ✅ |
| Random numbers | ✅ | ✅ | ✅ | ✅ | ✅ |
| BIP39 mnemonics | ✅ | ✅ | ✅ | ✅ | ✅ |
| PEM/JWK export | ✅ | ✅ | ✅ | ✅ | ✅ |
| PBKDF2 / HKDF | ✅ | ✅ | ✅ | ✅ | ✅ |
Deno:
deno add @tundralibs/cryptBun:
bunx jsr add @tundralibs/cryptNode.js:
npx jsr add @tundralibs/cryptGenerates RSA key pairs for encryption or signing.
async function generateRSAKeyPair(
options: RSAKeyOptions,
): Promise<GeneratedKeyPair>;
interface RSAKeyOptions {
algorithm: 'RSA-OAEP' | 'RSA-PSS';
keySize?: 2048 | 3072 | 4096; // defaults to 2048
hashAlgorithm?: 'SHA-256' | 'SHA-384' | 'SHA-512'; // defaults to 'SHA-256'
format?: 'PEM' | 'DER' | 'JWK'; // 'RAW' is rejected for RSA — see below
extractable?: boolean;
}
format: 'RAW'throws — RSA keys have no raw encoding in Web Crypto.RAWonly works for EC keys (seegenerateECKeyPair()below), where it exports the public key alone.
Example:
import { generateRSAKeyPair } from '@tundralibs/crypt/generators';
const { publicKey, privateKey, publicKeyExported, privateKeyExported } =
await generateRSAKeyPair({
algorithm: 'RSA-OAEP',
keySize: 2048,
hashAlgorithm: 'SHA-256',
format: 'PEM',
});Generates elliptic curve key pairs.
async function generateECKeyPair(
options: ECKeyOptions,
): Promise<GeneratedKeyPair>;
interface ECKeyOptions {
algorithm: 'ECDSA' | 'ECDH';
curve: 'P-256' | 'P-384' | 'P-521';
format?: 'PEM' | 'DER' | 'JWK' | 'RAW';
extractable?: boolean;
}
format: 'RAW'is public-key-only: it exportspublicKeyExportedas the uncompressed curve point and leavesprivateKeyExportedundefined — there is no raw encoding for an EC private key. Use it when you only need to hand the public key to a peer (e.g. for ECDH agreement); use'PEM'or'JWK'when you need both halves.
Example:
import { generateECKeyPair } from '@tundralibs/crypt/generators';
const { publicKeyExported, privateKeyExported } = await generateECKeyPair({
algorithm: 'ECDSA',
curve: 'P-256',
format: 'JWK',
});Generates an Ed25519 key pair for signEd25519/verifyEd25519 and the JWT
EdDSA algorithm. Nothing is configurable — curve and digest are fixed by
Ed25519 itself (RFC 8032); the keys are always extractable.
generateEd25519Keys(format?: KeyFormat): Promise<GeneratedKeyPair>'RAW' exports the public key alone (its 32 bytes), leaving
privateKeyExported undefined. Also reachable as
generateKeyPair('Ed25519', format).
import { generateEd25519Keys } from '@tundralibs/crypt/generators';
const { publicKeyExported } = await generateEd25519Keys('PEM');
console.log(publicKeyExported); // -----BEGIN PUBLIC KEY----- …generateRSAKeyPair() and generateECKeyPair() take every option explicitly;
five presets skip the boilerplate for the pairings the rest of this package
actually consumes. All of them always generate extractable keys — call
the underlying function directly if you need a non-extractable CryptoKey.
function generateKeyPair(
algorithm: 'RSA-OAEP' | 'RSA-PSS' | 'ECDSA' | 'ECDH',
format?: 'PEM' | 'DER' | 'JWK' | 'RAW',
): Promise<GeneratedKeyPair>; // RSA-OAEP/RSA-PSS: 2048-bit, SHA-256; ECDSA/ECDH: P-256
function generateRSAEncryptionKeys(
keySize?: 2048 | 3072 | 4096, // default 2048
format?: 'PEM' | 'DER' | 'JWK',
): Promise<GeneratedKeyPair>; // RSA-OAEP + SHA-256, for encryptRSA/decryptRSA
function generateRSASigningKeys(
keySize?: 2048 | 3072 | 4096, // default 2048
format?: 'PEM' | 'DER' | 'JWK',
): Promise<GeneratedKeyPair>; // RSA-PSS + SHA-256, for the PS* side of signRSA/verifyRSA
function generateECDSAKeys(
curve?: 'P-256' | 'P-384' | 'P-521', // default 'P-256'
format?: 'PEM' | 'DER' | 'JWK' | 'RAW',
): Promise<GeneratedKeyPair>; // for signEC/verifyEC
function generateECDHKeys(
curve?: 'P-256' | 'P-384' | 'P-521', // default 'P-256'
format?: 'PEM' | 'DER' | 'JWK' | 'RAW',
): Promise<GeneratedKeyPair>; // deriveKey usage only — see below
generateRSASigningKeys()andgenerateRSAEncryptionKeys()produce keys for two different RSA primitives. AgenerateRSASigningKeys()key is RSA-PSS — it can signPS*but is refused forRS*(PKCS#1 v1.5); it is also the wrong shape forencryptRSA/decryptRSA, which need anRSA-OAEPkey. Reach forgenerateRSAEncryptionKeys()for encryption andgenerateRSASigningKeys()for signing — never the same key pair for both.
generateECDHKeys()grants the private keyderiveKeyusage only, socrypto.subtle.deriveBitsrejects it directly — derive aCryptoKeywithderiveKeyand export that if you need raw shared-secret bytes. Both sides of an exchange must agree on the curve; a P-256 key cannot derive against a P-384 one.
Example:
import {
generateECDSAKeys,
generateKeyPair,
generateRSAEncryptionKeys,
} from '@tundralibs/crypt/generators';
// Same as generateRSAKeyPair({ algorithm: 'RSA-OAEP', keySize: 2048, hashAlgorithm: 'SHA-256', format: 'PEM' })
const encKeys = await generateRSAEncryptionKeys(2048, 'PEM');
// ES256-ready EC keys, JWK-exported
const signingKeys = await generateECDSAKeys('P-256', 'JWK');
// Fully generic — algorithm decides the sensible default (P-256, or 2048-bit RSA)
const keys = await generateKeyPair('ECDSA', 'PEM');Generates a cryptographically secure random secret.
const secretGenerator: (
byteLengthOrOptions: number | SecretGeneratorOptions,
encoding?: 'HEX' | 'BASE64' | 'BASE32' | 'ALPHANUMERIC',
) => string;Example:
import { secretGenerator } from '@tundralibs/crypt/generators';
const secret = secretGenerator(32, 'HEX'); // 64 hex characters
const b64Secret = secretGenerator(32, 'BASE64');
const b32Secret = secretGenerator(20, 'BASE32');
const alphaSecret = secretGenerator(16, 'ALPHANUMERIC');const generateHexSecret: (byteLength: number) => string;
const generateBase64Secret: (byteLength: number) => string;
const generateBase32Secret: (byteLength: number) => string;
const generateAlphanumericSecret: (byteLength: number) => string;
const generateToken: () => string; // 32-byte HEX token
function generatePassword(length?: number, options?: PasswordOptions): string;Example:
import {
generateAlphanumericSecret,
generateHexSecret,
generatePassword,
generateToken,
} from '@tundralibs/crypt/generators';
const apiKey = generateHexSecret(32); // 64 hex chars
const token = generateToken(); // 64 hex chars (32 bytes)
const code = generateAlphanumericSecret(16);
const password = generatePassword(16, {
uppercase: true,
numbers: true,
symbols: true,
});Cryptographically secure alternatives to Math.random() for numeric ranges —
dice rolls, lottery draws, shuffling, sampling — anywhere the output must not
be predictable. Both integer functions use rejection sampling (drawing extra
random bytes and discarding out-of-range draws) so every value in the range is
equally likely; a naive byte % range would bias low values whenever range
does not evenly divide 256.
function randomInt(min: number, max: number): number; // inclusive both ends
function randomFloat(min: number, max: number, precision?: number): number; // max exclusive
function randomNumber(options?: RandomNumberOptions): number;
interface RandomNumberOptions {
min?: number; // default 0
max?: number; // default 100
float?: boolean; // default false
precision?: number; // default 16 (float mode only)
}Reach for
Math.random()instead when the output does not need to resist prediction (a UI animation delay, a non-security sample).randomInt's rejection sampling costs extracrypto.getRandomValues()calls compared toMath.random(), which is the right trade for anything security- or fairness-sensitive but unnecessary overhead otherwise.
Example:
import {
randomFloat,
randomInt,
randomNumber,
} from '@tundralibs/crypt/generators';
const dice = randomInt(1, 6); // 1-6 inclusive
const probability = randomFloat(0, 1); // 0.0 to 0.999...
const inRange = randomNumber({ min: 10, max: 20 }); // integer, 10-20 inclusive
const preciseFloat = randomNumber({
min: 0,
max: 1,
float: true,
precision: 4,
});Generates a BIP39 mnemonic phrase asynchronously.
const generateBIP39Mnemonic: (
options?: BIP39Options,
) => Promise<BIP39Result>; // { words, phrase, entropy, seed }
interface BIP39Options {
wordCount?: 12 | 15 | 18 | 21 | 24; // default 12
wordlist?: readonly string[]; // default: the built-in English list; must have exactly 2048 entries
passphrase?: string; // default ''; folded into `seed` only — see below
}
passphrasenever appears inphraseorwords— it only changes the derivedseed. It cannot be recovered from the phrase, and a different passphrase over the same phrase silently derives a different, equally valid-looking seed. Losing the passphrase is as unrecoverable as losing the phrase itself.
Aliases: generate12WordSeed(passphrase?), generate24WordSeed(passphrase?),
generateSeedPhrase(wordCount?, passphrase?) — positional shorthands fixed to
the built-in English wordlist; call generateBIP39Mnemonic directly to supply
another language or a custom wordlist.
Example:
import { generateBIP39Mnemonic } from '@tundralibs/crypt/generators';
const { phrase } = await generateBIP39Mnemonic({ wordCount: 12 });
console.log(phrase); // 'abandon ability able about...'Converts mnemonic to seed for key derivation.
async function mnemonicToSeed(
mnemonic: string,
passphrase?: string,
): Promise<Uint8Array>;Example:
import { mnemonicToSeed } from '@tundralibs/crypt/generators';
declare const mnemonic: string;
const seed = await mnemonicToSeed(mnemonic, 'optional passphrase');Validates a BIP39 mnemonic phrase (async). The mnemonic and the wordlist are
compared in NFKD form — matching mnemonicToSeed — so an IME-composed
(NFC/NFD) mnemonic still validates against an official NFKD wordlist.
const validateBIP39Mnemonic: (
mnemonic: string,
wordlist?: string[],
) => Promise<boolean>;
// Alias:
const validateSeedPhrase = validateBIP39Mnemonic;Example:
import { validateBIP39Mnemonic } from '@tundralibs/crypt/generators';
const isValid = await validateBIP39Mnemonic('abandon ability able...');Derives a non-extractable AES CryptoKey from a secret + salt using
PBKDF2-SHA-256 at PBKDF2_ITERATIONS (210,000) — the same derivation
encryptAES / decryptAES run per message. The key is bound to a single AES
algorithm + length, ready for crypto.subtle.encrypt / decrypt.
derivePBKDF2Key(
secret: string,
salt: Uint8Array,
algorithm: 'AES-GCM' | 'AES-CBC' | 'AES-CTR',
keyLengthBits: 128 | 192 | 256,
): Promise<CryptoKey>Example:
import { derivePBKDF2Key } from '@tundralibs/crypt/generators';
// A fixed salt makes derivation deterministic — store it with the secret.
const salt = new Uint8Array(16).fill(7);
const key = await derivePBKDF2Key('mySecret', salt, 'AES-GCM', 256);
// Reuse `key` across many crypto.subtle.encrypt/decrypt calls.Derive independent sub-keys from a single high-entropy secret using HKDF
(RFC 5869). Unlike PBKDF2 — a deliberately slow password stretcher — HKDF
is fast and is the correct primitive for domain separation: vary the
info label to get keys for distinct purposes from the same secret, with
the guarantee that no derived key reveals the secret or any sibling key.
Prefer it over ad-hoc secret + label concatenation.
hkdf(
ikm: string | Uint8Array,
options?: {
salt?: string | Uint8Array;
info?: string | Uint8Array;
length?: number; // bytes, default 32
hash?: 'SHA-256' | 'SHA-384' | 'SHA-512'; // default 'SHA-256'
},
): Promise<Uint8Array>Parameters:
-
ikm- Input keying material (the shared high-entropy secret). Not for low-entropy passwords — usepbkdf2Hashfrom Digest for those. -
options.salt- Optional salt (HKDF-Extract). Defaults to empty, which is fine whenikmis already high-entropy. -
options.info- Context/application label; the domain-separation tag. Two calls that differ only ininfoyield independent keys. -
options.length- Output length in bytes (default 32; max 255 × hash-length). -
options.hash- Underlying digest (default'SHA-256').
Returns: The derived key material as a Uint8Array.
Throws:
-
RangeError- Whenlengthis not an integer in1..255×hashLen.
Example:
import { hkdf } from '@tundralibs/crypt/generators';
declare const secret: Uint8Array;
// Two independent keys from one secret — signing vs MAC.
const signKey = await hkdf(secret, { info: 'jwt' });
const macKey = await hkdf(secret, { info: 'hmac' });
// signKey and macKey are unrelated; neither leaks `secret`.import { generateRSAKeyPair } from '@tundralibs/crypt/generators';
import { decryptRSA, encryptRSA } from '@tundralibs/crypt/encrypt';
// Generate key pair — `format: 'PEM'` fills in the exported PEM strings
const { publicKeyExported, privateKeyExported } = await generateRSAKeyPair({
algorithm: 'RSA-OAEP',
keySize: 2048,
hashAlgorithm: 'SHA-256',
format: 'PEM',
});
// Use for encryption — encryptRSA/decryptRSA take the PEM strings
const encrypted = await encryptRSA('secret', publicKeyExported as string);
const decrypted = await decryptRSA(encrypted, privateKeyExported as string);import { generateHexSecret, generateToken } from '@tundralibs/crypt/generators';
// Generate API key
const apiKey = generateHexSecret(32); // 64 hex chars
// Generate single-use token
const token = generateToken(); // 64 hex charsimport {
generateBIP39Mnemonic,
mnemonicToSeed,
validateBIP39Mnemonic,
} from '@tundralibs/crypt/generators';
// 1. Generate mnemonic (async)
const { phrase } = await generateBIP39Mnemonic({ wordCount: 24 });
console.log('Save this safely:', phrase);
// 2. Validate mnemonic (async)
const isValid = await validateBIP39Mnemonic(phrase);
// 3. Derive seed for key generation
if (isValid) {
const seed = await mnemonicToSeed(phrase, 'optional passphrase');
// Use seed for HD wallet key derivation
}import { generatePassword } from '@tundralibs/crypt/generators';
// Default: 16 chars with mixed case, numbers, symbols
const password = generatePassword();
// Custom length and character sets
const customPassword = generatePassword(20, {
uppercase: true,
lowercase: true,
numbers: true,
symbols: false, // No symbols
});- All random generation uses
crypto.getRandomValues() - RSA: Use minimum 2048-bit keys
- EC: P-256 curve is recommended minimum
- BIP39: Store mnemonics securely offline
- Secrets: Use sufficient entropy (32+ bytes)
- Never commit secrets to version control
- Use hardware security modules (HSM) for production keys