← 中文资源

简体中文版本

Nonce 与 IV 重用

同一密钥下重用 nonce 或 IV 对流式与 GCM 模式是灾难性的:密钥流或认证子键重用直接导致明文恢复与伪造,而非渐进削弱。

阅读英文原文

概念定义

确定性加密会泄漏相等性并招来字典攻击,因此模式引入 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"))
}