← 中文资源

简体中文版本

带认证的加密(AEAD)

认证加密产出密文与标签,证明完整性与真实性,使篡改在明文被使用前即被发现,是现代对称加密的正确默认。

阅读英文原文

概念概述

带认证的加密(AEAD)(Authenticated Encryption (AEAD))指的是:认证加密产出密文与标签,证明完整性与真实性,使篡改在明文被使用前即被发现,是现代对称加密的正确默认。

从更严格的定义看,Authenticated encryption with associated data is a mode that encrypts a message and authenticates both the ciphertext and any additional public data, detecting any modification. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。

为什么需要它

It is the correct default for symmetric encryption of data at rest, in transit within a service, and in tokens. 它与 Forward Secrecy、Key Derivation Functions、Public Key Infrastructure 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。

常见误解

  • encryption alone protects integrity
  • the associated data is encrypted along with the message

需要避免的反模式

  • using a raw block cipher mode without a separate integrity check
  • encrypting and then verifying with an unauthenticated hash of the plaintext

实践要点

  1. Modes such as AES-GCM and ChaCha20-Poly1305 bind a nonce, the ciphertext, and optional associated data into a single authentication tag that decryption verifies.
  2. Nonce management is critical: reusing a nonce with the same key under GCM destroys confidentiality and allows forgery, so nonces must be unique per key.
  3. Never use a block cipher in a bare mode for new code; authenticated modes fail closed on tampering rather than silently returning corrupted plaintext.

参考来源

https://datatracker.ietf.org/doc/html/rfc5116

代码示例

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