← 中文资源

简体中文版本

AES-256-GCM 字符串加密

使用 AES-256-GCM 加密并认证字符串,通过 scrypt 与随机盐从口令派生密钥。

阅读英文原文

功能说明

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");
}