better-cipher
Version:
A secure encryption library with browser and Node.js support using AES-GCM
59 lines (49 loc) • 1.94 kB
text/typescript
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
import type { Encrypted, IEncryption, CreateRotator } from "./types";
import { isValidHexKey } from "./util";
const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 12;
export class Cipher implements IEncryption {
private secretKey: Buffer;
static createRotator: CreateRotator = (oldSecretKey, newSecretKey) => {
const oldCipher = new Cipher(oldSecretKey);
const newCipher = new Cipher(newSecretKey);
const rotator = async (prevEncrypted: Encrypted): Promise<Encrypted> => {
const deciphered = await oldCipher.decrypt(prevEncrypted);
return await newCipher.encrypt(deciphered);
};
return rotator;
};
constructor(secretKeyHex: string) {
if (!isValidHexKey(secretKeyHex)) {
throw new Error("Secret key must be a valid hex string");
}
this.secretKey = Buffer.from(secretKeyHex, "hex");
}
async encrypt(text: string): Promise<Encrypted> {
// For empty string, we still need to encrypt something
const dataToEncrypt = text || "\0";
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, this.secretKey, iv);
let encrypted = cipher.update(dataToEncrypt, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag().toString("hex");
return Promise.resolve({
iv: iv.toString("hex"),
content: encrypted,
authTag: authTag,
});
}
async decrypt(encrypted: Encrypted): Promise<string> {
const decipher = createDecipheriv(
ALGORITHM,
this.secretKey,
Buffer.from(encrypted.iv, "hex")
);
decipher.setAuthTag(Buffer.from(encrypted.authTag, "hex"));
let decrypted = decipher.update(encrypted.content, "hex", "utf8");
decrypted += decipher.final("utf8");
// Convert null byte back to empty string
return Promise.resolve(decrypted === "\0" ? "" : decrypted);
}
}