功能说明
encryptString 使用 AES-256-GCM 加密 UTF-8 字符串,返回盐、随机初始化向量、认证标签与密文的 Base64 字段;decryptString 校验标签后还原明文。密钥由口令和随机盐经 scrypt 派生,因此相同明文每次加密结果不同。适合加密小型配置、令牌和本地敏感数据,不建议直接用于大文件。
关键代码
每次加密生成十六字节盐与十二字节 IV,scrypt 派生三十二字节密钥,正好对应 AES-256。加密使用 createCipheriv,输出密文后单独获取十六字节认证标签。解密前重新派生密钥并设置标签,final 会在标签不匹配或密文被篡改时抛错。所有字段以 Base64 存储,便于放入 JSON 或数据库文本列。
使用注意
GCM 的 IV 在同一密钥下重复使用会严重破坏安全性,本实现每次随机生成,禁止调用方自行复用。口令必须高熵并通过安全通道传递,scrypt 默认参数适合交互场景,高负载服务应调整成本并做基准测试。认证失败应视为篡改或口令错误,不要返回部分明文。密文仍需配合访问控制与密钥轮换策略。
验证方式
测试加密后再解密得到原文,包含中文、emoji 与空字符串。确认同一明文两次加密的盐、IV 与密文不同。篡改密文、标签或 IV 后断言解密抛错,错误口令也应失败。测试短口令、非字符串明文与缺失字段会抛错,并验证 Base64 字段可 JSON 往返而不丢失字节。
代码示例
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
const ALGORITHM = "aes-256-gcm";
export interface EncryptedPayload {
salt: string;
iv: string;
tag: string;
data: string;
}
export function deriveAesKey(passphrase: string, salt: Buffer): Buffer {
if (typeof passphrase !== "string" || passphrase.length < 12) {
throw new RangeError("passphrase must be at least 12 characters");
}
return scryptSync(passphrase.normalize("NFKC"), salt, 32);
}
export function encryptString(plaintext: string, passphrase: string): EncryptedPayload {
const salt = randomBytes(16);
const iv = randomBytes(12);
const cipher = createCipheriv(ALGORITHM, deriveAesKey(passphrase, salt), iv);
const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
return {
salt: salt.toString("base64"),
iv: iv.toString("base64"),
tag: cipher.getAuthTag().toString("base64"),
data: data.toString("base64"),
};
}
export function decryptString(payload: EncryptedPayload, passphrase: string): string {
const salt = Buffer.from(payload.salt, "base64");
const iv = Buffer.from(payload.iv, "base64");
const tag = Buffer.from(payload.tag, "base64");
const decipher = createDecipheriv(ALGORITHM, deriveAesKey(passphrase, salt), iv);
decipher.setAuthTag(tag);
return Buffer.concat([
decipher.update(Buffer.from(payload.data, "base64")),
decipher.final(),
]).toString("utf8");
}