@catbee/utils
Version:
A modular, production-grade utility toolkit for Node.js and TypeScript, designed for robust, scalable applications (including Express-based services). All utilities are tree-shakable and can be imported independently.
262 lines • 10 kB
JavaScript
import { createHmac, createHash, randomUUID, randomBytes, createCipheriv, createDecipheriv, scrypt, timingSafeEqual, } from "crypto";
import { promisify } from "util";
/**
* Generates an HMAC digest using the specified algorithm and secret key.
*
* @param {string} algorithm - The hashing algorithm (e.g., 'sha256', 'sha1').
* @param {string} input - The string to hash.
* @param {string} secret - The secret key for HMAC.
* @param {BinaryToTextEncoding} [encoding='hex'] - Output encoding ('hex', 'base64', etc).
* @returns {string} HMAC digest as a string.
*/
export function hmac(algorithm, input, secret, encoding = "hex") {
return createHmac(algorithm, secret).update(input).digest(encoding);
}
/**
* Generates a hash digest using the specified algorithm.
*
* @param {string} algorithm - The hashing algorithm (e.g., 'sha256', 'md5').
* @param {string} input - The string to hash.
* @param {BinaryToTextEncoding} [encoding='hex'] - Output encoding ('hex', 'base64', etc).
* @returns {string} Hash digest as a string.
*/
export function hash(algorithm, input, encoding = "hex") {
return createHash(algorithm).update(input).digest(encoding);
}
/**
* Generates an HMAC-SHA256 digest.
*
* @param {string} input - The string to hash.
* @param {string} secret - The secret key.
* @returns {string} SHA-256 HMAC digest as a string.
*/
export function sha256Hmac(input, secret) {
return hmac("sha256", input, secret);
}
/**
* Generates a SHA1 hash digest.
*
* @param {string} input - The string to hash.
* @param {BinaryToTextEncoding} [encoding='hex'] - Output encoding.
* @returns {string} SHA-1 hash as a string.
*/
export function sha1(input, encoding = "hex") {
return hash("sha1", input, encoding);
}
/**
* Generates a SHA256 hash digest.
*
* @param {string} input - The string to hash.
* @param {BinaryToTextEncoding} [encoding='hex'] - Output encoding.
* @returns {string} SHA-256 hash as a string.
*/
export function sha256(input, encoding = "hex") {
return hash("sha256", input, encoding);
}
/**
* Generates an MD5 hash digest.
*
* @param {string} input - The string to hash.
* @returns {string} MD5 hash as a string.
*/
export function md5(input) {
return hash("md5", input);
}
/**
* Generates a cryptographically strong random string by hashing a random UUID with SHA-256.
*
* @returns {string} Random string hashed with SHA-256 (hex encoding).
*/
export function randomString() {
return sha256(randomUUID());
}
/**
* Generates a secure random buffer of specified byte length.
*
* @param {number} [byteLength=32] - Number of random bytes to generate.
* @returns {Buffer} Buffer containing random bytes.
*/
export function generateRandomBytes(byteLength = 32) {
return randomBytes(byteLength);
}
/**
* Generates a secure random string of specified byte length with specified encoding.
*
* @param {number} [byteLength=32] - Number of random bytes to generate.
* @param {BinaryToTextEncoding} [encoding='hex'] - Output encoding.
* @returns {string} Random string in specified encoding.
*/
export function generateRandomBytesAsString(byteLength = 32, encoding = "hex") {
return randomBytes(byteLength).toString(encoding);
}
/**
* Generates a secure API key with a specified format.
*
* @param {string} [prefix=''] - Optional prefix for the key.
* @param {number} [byteLength=24] - Number of random bytes to generate.
* @returns {string} Formatted API key.
*/
export function generateApiKey(prefix = "", byteLength = 24) {
const randomString = generateRandomBytesAsString(byteLength, "base64");
const key = randomString
.replace(/[+/=]/g, "") // Remove non-URL-safe characters
.substring(0, 32); // Limit length
return prefix ? `${prefix}_${key}` : key;
}
/**
* Compares two strings, arrays, or buffers in constant time to prevent timing attacks.
*
* @param {string | Buffer | Uint8Array} a - First value to compare
* @param {string | Buffer | Uint8Array} b - Second value to compare
* @returns {boolean} True if values are equal
*/
export function safeCompare(a, b) {
if (typeof a === "string" && typeof b === "string") {
// Convert strings to buffers
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
// Compare lengths first (not constant time, but prevents timing attack on contents)
if (bufA.length !== bufB.length)
return false;
return timingSafeEqual(bufA, bufB);
}
else if ((a instanceof Buffer || a instanceof Uint8Array) &&
(b instanceof Buffer || b instanceof Uint8Array)) {
// Compare lengths first
if (a.length !== b.length)
return false;
return timingSafeEqual(a instanceof Buffer ? a : Buffer.from(a), b instanceof Buffer ? b : Buffer.from(b));
}
throw new Error("Cannot compare: inputs must be strings, Buffers, or Uint8Arrays");
}
// Promisified version of scrypt for key derivation
const scryptAsync = promisify(scrypt);
/**
* Encrypts data using a symmetric key with secure defaults (AES-256-GCM).
*
* @param {string | Buffer} data - Data to encrypt
* @param {string | Buffer} key - Encryption key or passphrase
* @param {EncryptionOptions} [options] - Encryption options
* @returns {Promise<EncryptionResult>} Encrypted data with metadata
*/
export async function encrypt(data, key, options = {}) {
const algorithm = options.algorithm || "aes-256-gcm";
const inputEncoding = options.inputEncoding || "utf8";
const outputEncoding = options.outputEncoding || "hex";
// Generate a random IV
const iv = randomBytes(16);
// Derive key using scrypt if key is a string (passphrase)
const derivedKey = typeof key === "string" ? await scryptAsync(key, iv.slice(0, 8), 32) : key;
// Create cipher
const cipher = createCipheriv(algorithm, derivedKey, iv);
// Encrypt the data
let ciphertext;
if (typeof data === "string") {
ciphertext = cipher.update(data, inputEncoding, outputEncoding);
ciphertext += cipher.final(outputEncoding);
}
else {
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
ciphertext = outputEncoding
? encrypted.toString(outputEncoding)
: encrypted;
}
// Get authentication tag if using GCM mode
const authTag = algorithm.includes("gcm")
? cipher.getAuthTag()
: undefined;
return {
ciphertext,
iv,
authTag,
algorithm,
};
}
/**
* Decrypts data that was encrypted with the encrypt function.
*
* @param {EncryptionResult} encryptedData - The encrypted data and metadata
* @param {string | Buffer} key - Decryption key or passphrase
* @param {DecryptionOptions} [options] - Decryption options
* @returns {Promise<string | Buffer>} Decrypted data
*/
export async function decrypt(encryptedData, key, options = {}) {
const algorithm = options.algorithm || encryptedData.algorithm || "aes-256-gcm";
const inputEncoding = options.inputEncoding || "hex";
const outputEncoding = options.outputEncoding || "utf8";
// Derive key using scrypt if key is a string (passphrase)
const derivedKey = typeof key === "string"
? await scryptAsync(key, encryptedData.iv.slice(0, 8), 32)
: key;
// Create decipher
const decipher = createDecipheriv(algorithm, derivedKey, encryptedData.iv);
// Set auth tag if using GCM mode
if (encryptedData.authTag && algorithm.includes("gcm")) {
decipher.setAuthTag(encryptedData.authTag);
}
// Decrypt the data
let decrypted;
if (typeof encryptedData.ciphertext === "string") {
decrypted = decipher.update(encryptedData.ciphertext, inputEncoding, outputEncoding);
decrypted += decipher.final(outputEncoding);
}
else {
const result = Buffer.concat([
decipher.update(encryptedData.ciphertext),
decipher.final(),
]);
decrypted = outputEncoding ? result.toString(outputEncoding) : result;
}
return decrypted;
}
/**
* Creates a signed token with an expiration time and payload.
*
* @param {object} payload - Data to include in the token
* @param {string} secret - Secret key for signing
* @param {number} [expiresInSeconds=3600] - Token expiration in seconds
* @returns {string} Signed token string
*/
export function createSignedToken(payload, secret, expiresInSeconds = 3600) {
// Create payload with expiration
const tokenPayload = Object.assign(Object.assign({}, payload), { exp: Math.floor(Date.now() / 1000) + expiresInSeconds });
// Convert to string
const payloadStr = JSON.stringify(tokenPayload);
// Base64 encode the payload
const base64Payload = Buffer.from(payloadStr).toString("base64url");
// Create signature
const signature = hmac("sha256", base64Payload, secret, "base64url");
// Combine payload and signature
return `${base64Payload}.${signature}`;
}
/**
* Verifies and decodes a signed token.
*
* @param {string} token - The token to verify
* @param {string} secret - Secret key for verification
* @returns {object | null} Decoded payload if valid, null if invalid
*/
export function verifySignedToken(token, secret) {
try {
// Split token into parts
const [payloadB64, signature] = token.split(".");
if (!payloadB64 || !signature)
return null;
// Verify signature
const expectedSignature = hmac("sha256", payloadB64, secret, "base64url");
if (!safeCompare(signature, expectedSignature))
return null;
// Decode payload
const payloadStr = Buffer.from(payloadB64, "base64url").toString("utf8");
const payload = JSON.parse(payloadStr);
// Check expiration
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
return null;
}
return payload;
}
catch (_a) {
return null;
}
}
//# sourceMappingURL=crypto.utils.js.map