Crypto Utilities
Standalone cryptographic utilities exported from ideal-auth. These functions are used internally by the library and are also available for direct use in your application.
import { generateToken, signData, verifySignature, encrypt, decrypt, timingSafeEqual,} from 'ideal-auth';generateToken
Section titled “generateToken”function generateToken(bytes?: number): stringGenerates a cryptographically secure random token as a hexadecimal string using crypto.randomBytes (CSPRNG).
| Parameter | Type | Required | Default |
|---|---|---|---|
bytes | number | No | 32 |
- Default: 32 bytes, producing a 64-character hex string.
- Throws if
bytesis less than1.
import { generateToken } from 'ideal-auth';
const token = generateToken();// "a3f1b2c4d5e6f7081920a1b2c3d4e5f6a7b8c9d0e1f2031415161718191a1b2c"// (64 hex characters = 32 bytes)
const shortToken = generateToken(16);// "a3f1b2c4d5e6f708192031415161718" (32 hex characters = 16 bytes)// Throws for invalid inputtry { generateToken(0);} catch (error) { // Error: bytes must be at least 1}signData
Section titled “signData”function signData(data: string, secret: string): stringCreates an HMAC-SHA256 signature of the given data using the provided secret. Returns the signature as a hex digest.
| Parameter | Type | Required |
|---|---|---|
data | string | Yes |
secret | string | Yes |
Throws if secret is an empty string.
import { signData } from 'ideal-auth';
const signature = signData('user_abc123:1709136000000', process.env.AUTH_SECRET!);// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"// Throws for empty secrettry { signData('some data', '');} catch (error) { // Error: secret must not be empty}Use case: signed URLs
Section titled “Use case: signed URLs”import { signData, verifySignature } from 'ideal-auth';
function createSignedUrl(url: string, secret: string): string { const signature = signData(url, secret); return `${url}&sig=${signature}`;}
function isValidSignedUrl(url: string, signature: string, secret: string): boolean { return verifySignature(url, signature, secret);}verifySignature
Section titled “verifySignature”function verifySignature(data: string, signature: string, secret: string): booleanVerifies an HMAC-SHA256 signature using timing-safe comparison. Returns true if the signature is valid for the given data and secret.
| Parameter | Type | Required |
|---|---|---|
data | string | Yes |
signature | string | Yes |
secret | string | Yes |
import { signData, verifySignature } from 'ideal-auth';
const data = 'user_abc123:1709136000000';const secret = process.env.AUTH_SECRET!;
const signature = signData(data, secret);
verifySignature(data, signature, secret); // trueverifySignature(data, 'tampered-signature', secret); // falseverifySignature('tampered-data', signature, secret); // falseencrypt
Section titled “encrypt”function encrypt(plaintext: string, secret: string): Promise<string>Encrypts a string using AES-256-GCM with scrypt key derivation. Returns a base64url-encoded ciphertext string.
| Parameter | Type | Required |
|---|---|---|
plaintext | string | Yes |
secret | string | Yes |
Throws if secret is an empty string.
Encryption details:
| Property | Value |
|---|---|
| Algorithm | AES-256-GCM |
| Key derivation | scrypt (N=32768, r=8, p=1) |
| Salt | Random 16 bytes (generated per call) |
| IV | Random 12 bytes (generated per call) |
| Output | base64url-encoded |
Each call produces a different ciphertext even for the same plaintext and secret, because a fresh random salt and IV are generated every time.
import { encrypt } from 'ideal-auth';
const ciphertext = await encrypt('sensitive-data', process.env.AUTH_SECRET!);// "YWJjZGVmZ2hpamts..." (base64url-encoded)
// Same input, different output each timeconst ciphertext2 = await encrypt('sensitive-data', process.env.AUTH_SECRET!);// Different from ciphertextdecrypt
Section titled “decrypt”function decrypt(ciphertext: string, secret: string): Promise<string>Decrypts a ciphertext string produced by encrypt(). Returns the original plaintext.
| Parameter | Type | Required |
|---|---|---|
ciphertext | string | Yes |
secret | string | Yes |
Throws in the following cases:
secretis an empty string- The ciphertext is too short (fails minimum length check)
- The ciphertext has been tampered with (GCM authentication fails)
- The ciphertext was encrypted with a different secret
import { encrypt, decrypt } from 'ideal-auth';
const secret = process.env.AUTH_SECRET!;
const ciphertext = await encrypt('sensitive-data', secret);const plaintext = await decrypt(ciphertext, secret);// "sensitive-data"// Tampered ciphertext throwstry { await decrypt('invalid-ciphertext', secret);} catch (error) { // Decryption failed}
// Wrong secret throwstry { await decrypt(ciphertext, 'wrong-secret');} catch (error) { // Decryption failed}Use case: encrypting TOTP secrets at rest
Section titled “Use case: encrypting TOTP secrets at rest”import { encrypt, decrypt } from 'ideal-auth';
const secret = process.env.AUTH_SECRET!;
// Encrypt before storingasync function storeTotpSecret(userId: string, totpSecret: string) { const encryptedSecret = await encrypt(totpSecret, secret); await db.user.update({ where: { id: userId }, data: { totpSecret: encryptedSecret }, });}
// Decrypt when readingasync function getTotpSecret(userId: string): Promise<string | null> { const user = await db.user.findUnique({ where: { id: userId } }); if (!user?.totpSecret) return null; return decrypt(user.totpSecret, secret);}timingSafeEqual
Section titled “timingSafeEqual”function timingSafeEqual(a: string, b: string): booleanPerforms a constant-time string comparison to prevent timing side-channel attacks. Uses crypto.timingSafeEqual internally.
| Parameter | Type | Required |
|---|---|---|
a | string | Yes |
b | string | Yes |
The implementation pads both strings to equal length before calling crypto.timingSafeEqual (which requires equal-length buffers), then verifies the actual lengths match. This ensures that neither the comparison itself nor the length check leaks timing information.
import { timingSafeEqual } from 'ideal-auth';
timingSafeEqual('abc123', 'abc123'); // truetimingSafeEqual('abc123', 'xyz789'); // falsetimingSafeEqual('short', 'longer-string'); // falseWhen to use
Section titled “When to use”Use timingSafeEqual whenever you compare secrets, tokens, or signatures in security-sensitive code. A naive === comparison can leak information about how many characters matched through timing differences.
import { timingSafeEqual } from 'ideal-auth';
function verifyWebhook(payload: string, receivedSignature: string, secret: string) { const expectedSignature = computeHmac(payload, secret);
// Timing-safe: does not leak how many characters matched return timingSafeEqual(receivedSignature, expectedSignature);}