@kanadi/core
Version:
Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles
144 lines (113 loc) • 4.01 kB
text/typescript
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);
}
}