1. 建立正确的密码学模型

1.1 crypto 是什么

node:crypto 是 Node.js 官方核心模块对 OpenSSL 等底层密码学能力的封装。JavaScript 代码负责选择算法、准备密钥与数据;真正的大量字节运算通常由 Node.js 背后的原生代码完成。

TypeScript 业务代码
  ↓ 调用 node:crypto
Node.js 原生绑定
  ↓
OpenSSL / 操作系统能力
  ↓
CPU 完成密码学计算

它主要解决四个安全目标:

crypto 只是基础工具,不会自动替业务做出正确选择。用 SHA-256 保存密码、重复使用 AES-GCM 的 IV,虽然代码可以正常运行,安全设计却是错误的。

1.2 数据最终都是字节

密码学算法处理的是字节,不理解“中文”“JSON”或“用户对象”。Node.js 通常用 Buffer 表示字节:

const bytes = Buffer.from("你好", "utf8");

console.log(bytes); // UTF-8 字节
console.log(bytes.length); // 6

算法的输入输出关系可以理解为:

字符串/对象
  ↓ UTF-8 编码或 JSON.stringify
Buffer 字节
  ↓ crypto 算法
Buffer 字节
  ↓ hex / base64 / base64url 编码
便于存储或传输的字符串

hex、Base64 和 Base64URL 都只是字节的文本表示方式,不是加密。知道编码规则的人不需要密钥就能还原:

const encoded = Buffer.from("secret", "utf8").toString("base64url");
const decoded = Buffer.from(encoded, "base64url").toString("utf8");

console.log(encoded);
console.log(decoded); // secret

常用表示方式:

编码 特点 常见场景
hex 一个字节变成两个字符,直观但较长 摘要、日志、调试
base64 更紧凑,可能包含 +/= 配置、普通文本传输
base64url 适合 URL,不使用 +/ Token、JWT

1.3 哈希、密码哈希、加密、HMAC 与签名

这些技术都可能输出“看不懂的字符串”,但用途完全不同。

普通哈希

任意数据 --SHA-256--> 固定长度摘要

密码哈希

密码 + 随机盐 + 成本参数 --scrypt--> 密码哈希

对称加密

明文 + Key + IV -> 密文
密文 + 同一个 Key + IV -> 明文

HMAC

消息 + 共享 Secret -> MAC

非对称签名

消息 + 私钥 -> 签名
消息 + 签名 + 公钥 -> 真或假

1.4 Key、Salt、IV 和 Auth Tag 不要混用

名称 是否保密 主要作用 能否重复使用
Key / Secret 必须保密 控制加解密、HMAC 或签名能力 可按策略使用和轮换
Salt 不需要 让相同密码产生不同哈希 每个密码都应随机生成
IV / Nonce 通常不需要 让同一 Key 下的加密具有唯一性 AES-GCM 同一 Key 下绝不能重复
Auth Tag 不需要 验证密文和附加数据未被篡改 与本次密文一起保存

一个常见误解是“不是秘密就不重要”。Salt、IV、Auth Tag 通常可以与结果一起保存,但丢失或错误使用仍会让验证失败,甚至破坏安全性。

1.5 随机数不是随便一个随机函数

Math.random() 主要用于游戏、抽样和界面效果,不承诺密码学不可预测性。攻击者可能根据内部状态推测结果,因此不能用它生成:

安全相关随机值应来自 node:crypto

import { randomBytes, randomInt, randomUUID } from "node:crypto";

1.6 crypto 不能解决所有安全问题

即使算法选择正确,下面的问题仍需业务系统处理:

密码学更像门锁:门锁很重要,但它不会替你决定谁可以拿钥匙,也不会阻止别人从没关好的窗户进入。