概念定义
确定性加密会泄漏相等性并招来字典攻击,因此模式引入 nonce 让每条消息输出不同。安全契约是“每密钥一次”。打破它不是让密码慢慢变弱——而是直接把明文之间的关系式或认证密钥交给攻击者。
工作机制
计数器类模式中 nonce 播种密钥流:重用意味着同一密钥流异或两段明文,相消后得到“明文异或明文”,结构化格式让它可读。GCM 里 nonce 重用还会暴露认证子键 H,此后可为选定密文伪造标签。计数器与足够宽的随机 nonce 是公认策略;XChaCha 与 SIV 模式则放宽唯一性要求。
使用场景
审计一切直接调用密码 API 的代码、自建加密 Cookie 或令牌格式、文件与列加密。优先使用只收明文与密钥、内部管理 nonce 的库;按密钥监控消息计数是否逼近安全边界。
常见误区
“96 位随机数撞不上”过于乐观:单密钥下消息量够大,生日碰撞比直觉来得快——随机 nonce 需要更宽或改用计数器。密钥轮换后才重用没问题,要求是“每密钥唯一”。进程重启会重置内存计数器——需持久化或无状态派生。AES-GCM-SIV 等防误用模式用少量性能换对意外重用的容忍。
代码示例
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");
}
package main
import (
"crypto/sha256"
"crypto/subtle"
"fmt"
)
func ConstantTimeEqual(a, b string) bool {
left := sha256.Sum256([]byte(a))
right := sha256.Sum256([]byte(b))
return subtle.ConstantTimeCompare(left[:], right[:]) == 1
}
func main() {
secret := "s3cr3t-token"
fmt.Println(ConstantTimeEqual(secret, "s3cr3t-token"))
fmt.Println(ConstantTimeEqual(secret, "s3cr3t-tokeX"))
fmt.Println(ConstantTimeEqual(secret, "short"))
}