UNPKG

@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
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