UNPKG

@kanadi/core

Version:

Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles

144 lines (113 loc) 4.01 kB
import { createCipheriv, createDecipheriv, createHmac, randomBytes, } from "crypto"; export class CryptoUtil { private static readonly ALGORITHM = "aes-256-gcm"; private static readonly KEY_LENGTH = 32; private static readonly IV_LENGTH = 16; private static readonly AUTH_TAG_LENGTH = 16; private static SECRET_KEY = process.env.KANADI_SECRET_KEY || "kanadi-secret-key-change-this-in-production!!"; private static getKey(): Buffer { return createHmac("sha256", CryptoUtil.SECRET_KEY) .update("kanadi-encryption-key") .digest() .slice(0, CryptoUtil.KEY_LENGTH); } static encrypt(data: any): string { try { const iv = randomBytes(CryptoUtil.IV_LENGTH); const key = CryptoUtil.getKey(); const cipher = createCipheriv(CryptoUtil.ALGORITHM, key, iv); const jsonData = JSON.stringify(data); let encrypted = cipher.update(jsonData, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`; } catch (error) { throw new Error("Encryption failed"); } } static decrypt(encryptedData: string): any { try { const parts = encryptedData.split(":"); if (parts.length !== 3) { throw new Error("Invalid encrypted data format"); } const ivHex = parts[0]!; const authTagHex = parts[1]!; const encrypted = parts[2]!; const iv = Buffer.from(ivHex, "hex"); const authTag = Buffer.from(authTagHex, "hex"); const key = CryptoUtil.getKey(); const decipher = createDecipheriv(CryptoUtil.ALGORITHM, key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted, "hex", "utf8"); decrypted += decipher.final("utf8"); return JSON.parse(decrypted); } catch (error) { throw new Error("Decryption failed"); } } static sign(data: any): string { const jsonData = JSON.stringify(data); return createHmac("sha256", CryptoUtil.SECRET_KEY) .update(jsonData) .digest("hex"); } static verify(data: any, signature: string): boolean { const expectedSignature = CryptoUtil.sign(data); return signature === expectedSignature; } static generateSessionKey(): Buffer { return randomBytes(CryptoUtil.KEY_LENGTH); } static exportKey(key: Buffer): string { return key.toString("base64"); } static importKey(keyBase64: string): Buffer { return Buffer.from(keyBase64, "base64"); } static decryptFromArray(encryptedArray: number[], key: Buffer): any { if (!Array.isArray(encryptedArray) || encryptedArray.length < 2) { throw new Error("Invalid encrypted data format"); } const totalLength = encryptedArray[0]; const dataNumbers = encryptedArray.slice(1); const bytes: number[] = []; for (let i = 0; i < dataNumbers.length; i++) { const num = dataNumbers[i]; const isLastChunk = i === dataNumbers.length - 1; const remainingBytes = totalLength - bytes.length; const chunkSize = isLastChunk ? Math.min(4, remainingBytes) : 4; for (let j = chunkSize - 1; j >= 0; j--) { bytes.push((num >> (j * 8)) & 0xff); } } const allBytes = Buffer.from(bytes.slice(0, totalLength)); const hmacSize = 32; const nonceSize = 12; const hmac = allBytes.slice(-hmacSize); const nonce = allBytes.slice(-hmacSize - nonceSize, -hmacSize); const encryptedWithTag = allBytes.slice(0, -hmacSize - nonceSize); const combinedBytes = Buffer.concat([encryptedWithTag, nonce]); const hmacHex = hmac.toString("hex"); const expectedHmac = createHmac("sha256", key) .update(combinedBytes) .digest("hex"); if (hmacHex !== expectedHmac) { throw new Error("HMAC verification failed"); } const ciphertext = encryptedWithTag.slice(0, -16); const authTag = encryptedWithTag.slice(-16); const decipher = createDecipheriv(CryptoUtil.ALGORITHM, key, nonce); decipher.setAuthTag(authTag); let decrypted = decipher.update(ciphertext, undefined, "utf8"); decrypted += decipher.final("utf8"); return JSON.parse(decrypted); } }