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
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 input
try {
generateToken(0);
} catch (error) {
// Error: bytes must be at least 1
}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 secret
try {
signData('some data', '');
} catch (error) {
// Error: secret must not be empty
}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
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); // true
verifySignature(data, 'tampered-signature', secret); // false
verifySignature('tampered-data', signature, secret); // falseencrypt
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 time
const ciphertext2 = await encrypt('sensitive-data', process.env.AUTH_SECRET!);
// Different from ciphertextdecrypt
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 throws
try {
await decrypt('invalid-ciphertext', secret);
} catch (error) {
// Decryption failed
}
// Wrong secret throws
try {
await decrypt(ciphertext, 'wrong-secret');
} catch (error) {
// Decryption failed
}Use case: encrypting TOTP secrets at rest
import { encrypt, decrypt } from 'ideal-auth';
const secret = process.env.AUTH_SECRET!;
// Encrypt before storing
async function storeTotpSecret(userId: string, totpSecret: string) {
const encryptedSecret = await encrypt(totpSecret, secret);
await db.user.update({
where: { id: userId },
data: { totpSecret: encryptedSecret },
});
}
// Decrypt when reading
async 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
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'); // true
timingSafeEqual('abc123', 'xyz789'); // false
timingSafeEqual('short', 'longer-string'); // falseWhen 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);
}