Skip to content

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

function generateToken(bytes?: number): string

Generates a cryptographically secure random token as a hexadecimal string using crypto.randomBytes (CSPRNG).

ParameterTypeRequiredDefault
bytesnumberNo32
  • Default: 32 bytes, producing a 64-character hex string.
  • Throws if bytes is less than 1.
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
}

function signData(data: string, secret: string): string

Creates an HMAC-SHA256 signature of the given data using the provided secret. Returns the signature as a hex digest.

ParameterTypeRequired
datastringYes
secretstringYes

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

function verifySignature(data: string, signature: string, secret: string): boolean

Verifies an HMAC-SHA256 signature using timing-safe comparison. Returns true if the signature is valid for the given data and secret.

ParameterTypeRequired
datastringYes
signaturestringYes
secretstringYes
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); // false

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.

ParameterTypeRequired
plaintextstringYes
secretstringYes

Throws if secret is an empty string.

Encryption details:

PropertyValue
AlgorithmAES-256-GCM
Key derivationscrypt (N=32768, r=8, p=1)
SaltRandom 16 bytes (generated per call)
IVRandom 12 bytes (generated per call)
Outputbase64url-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 ciphertext

function decrypt(ciphertext: string, secret: string): Promise<string>

Decrypts a ciphertext string produced by encrypt(). Returns the original plaintext.

ParameterTypeRequired
ciphertextstringYes
secretstringYes

Throws in the following cases:

  • secret is 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
}
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);
}

function timingSafeEqual(a: string, b: string): boolean

Performs a constant-time string comparison to prevent timing side-channel attacks. Uses crypto.timingSafeEqual internally.

ParameterTypeRequired
astringYes
bstringYes

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

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