1. 环境准备:Node.js crypto 模块概览
Node.js 的 crypto 模块是内置模块,底层基于 OpenSSL 实现,提供了完整的加密、解密、哈希、HMAC 和签名验证功能。使用原生 crypto 模块无需安装任何第三方依赖,可直接通过 require('crypto') 引入。
javascript
const crypto = require('crypto');
// 查看当前 OpenSSL 支持的所有加密算法
console.log(crypto.getCiphers());
// 输出示例:['aes-128-cbc', 'aes-256-gcm', ...]
crypto.getCiphers() 会列出当前 Node.js 版本支持的所有对称加密算法名称。其中以 aes- 开头的即为 AES 算法族,不同后缀代表不同的密钥长度(128、192、256)和工作模式(cbc、gcm、ctr 等)。
2. AES 模式选择:CBC vs GCM
在正式开始代码之前,理解不同工作模式的适用场景至关重要。
2.1 为什么推荐 GCM
AES 的多种工作模式中,常用的有CBC 和GCM两种:
| 特性 | AES-CBC | AES-GCM |
|---|---|---|
| 身份认证 | 不支持(需额外配合 HMAC) | 内置认证(AEAD) |
| 防篡改 | 无 | 有(通过认证标签) |
| 性能 | 较慢(串行处理) | 较快(可并行计算) |
| 当前推荐度 | 仅限兼容旧系统 | 新系统首选 |
GCM 模式属于AEAD(认证加密),一次操作同时提供机密性和完整性保护,省去了 CBC 模式下需要额外使用 HMAC 验证完整性的麻烦。CBC 模式容易受到 Padding Oracle 攻击,虽然可以通过 Encrypt-then-MAC 缓解,但实现复杂度会增加。
2.2 关键概念:IV/Nonce、AAD 与认证标签
IV / Nonce(初始化向量)
每次加密都必需一个唯一的随机值,目的是让同一密钥下的相同明文加密后产生不同的密文。
- CBC 模式使用 16 字节 IV
- GCM 模式使用 12 字节 Nonce(IV),12 字节是安全与性能的最佳平衡点
重要:GCM 模式下,同一密钥绝不能重用 IV/Nonce,否则会导致认证密钥泄漏,严重威胁数据机密性。
AAD(额外认证数据)
AAD 是 GCM 模式提供的额外保护层。这部分数据仅被认证、不被加密,以明文形式参与认证标签的计算。解密时必须提供完全相同的 AAD,否则认证标签验证失败。
认证标签(Auth Tag)
GCM 模式加密后产出的认证标签,长度通常为 16 字节(128 位)。解密时必须通过 setAuthTag() 传入该标签,否则即使密钥正确也会解密失败。这一机制保证了数据的完整性:任何对密文的篡改都会导致标签不匹配。
3. AES-256-GCM 加密与解密(推荐方案)
3.1 完整代码示例
GCM 模式是推荐的新系统首选方案。以下示例展示了加密/解密的完整流程,包含密钥生成、AAD 设置和认证标签管理。
javascript
const crypto = require('crypto');
// ==================== 加密函数 ====================
function encryptGCM(plaintext, key) {
const algorithm = 'aes-256-gcm';
// GCM 模式推荐 12 字节随机 nonce
const iv = crypto.randomBytes(12);
// 创建加密器
const cipher = crypto.createCipheriv(algorithm, key, iv);
// 设置额外认证数据(可选但推荐)
const aad = Buffer.from('additional-data-to-bind-context', 'utf8');
cipher.setAAD(aad);
// 执行加密:输入 utf8 字符串,输出 hex 编码
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
// 获取认证标签(16 字节)
const authTag = cipher.getAuthTag();
// 返回结果(iv 和 aad 的原始字节数需要保存以便解密)
return {
encrypted: encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
aad: aad.toString('utf8')
};
}
// ==================== 解密函数 ====================
function decryptGCM(encryptedData, key) {
const algorithm = 'aes-256-gcm';
// 恢复 Buffer
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');
// 创建解密器
const decipher = crypto.createDecipheriv(algorithm, key, iv);
// 必须与加密时使用完全一致的 AAD
const aad = Buffer.from(encryptedData.aad, 'utf8');
decipher.setAAD(aad);
// 设置认证标签(需在解密前设置)
decipher.setAuthTag(authTag);
// 执行解密
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// ==================== 使用示例 ====================
// 生成 32 字节(256 位)随机密钥
const key = crypto.randomBytes(32);
const result = encryptGCM('这是需要加密的敏感数据', key);
console.log('加密结果:', result.encrypted);
console.log('IV (hex):', result.iv);
console.log('认证标签 (hex):', result.authTag);
console.log('AAD:', result.aad);
const decrypted = decryptGCM(result, key);
console.log('解密结果:', decrypted);
// 输出:"这是需要加密的敏感数据"
3.2 代码要点说明
crypto.createCipheriv()和createDecipheriv():必须使用带iv后缀的这两个方法,crypto.createCipher()已弃用。cipher.setAAD():额外认证数据的设置时机必须在update()和final()之前。AAD 本身不加密但参与认证,保证密文与上下文绑定。cipher.getAuthTag():获取认证标签通常放在final()之后调用。decipher.setAuthTag():设置认证标签需在解密update()之前完成。如果认证标签不匹配,解密会抛出异常。- IV 的管理:GCM 模式使用 12 字节 Nonce。每次加密都需生成全新的随机 Nonce,不可重复使用。
3.3 常见问题
Q:GCM 模式下可以使用 Base64 编码的密钥吗?
完全可以。如果你存储的密钥是 Base64 格式,使用时先解码为 Buffer 即可:
javascript
const keyBase64 = 'AsaiFfQ1hMYYvT2N4Y1BhenJ0RjY0M2cxWTMZXdXMVY=';
const key = Buffer.from(keyBase64, 'base64');
Q:认证标签验证失败如何处理?
认证标签不匹配意味着以下可能之一:
- 密钥错误
- 密文被篡改
- AAD 与加密时不符
- 解密方法用错了加密算法
应在代码中用 try-catch 处理:
javascript
try {
const decrypted = decryptGCM(encryptedData, key);
console.log('解密成功:', decrypted);
} catch (error) {
throw new Error('解密失败:密钥错误或数据已被篡改');
}
4. AES-256-CBC 加密与解密(兼容旧系统)
CBC 模式是传统系统常用的模式。相比 GCM,CBC 只提供机密性,不提供完整性认证,使用时需格外注意。
4.1 完整代码示例
javascript
const crypto = require('crypto');
// ==================== 加密函数 ====================
function encryptCBC(plaintext, key) {
const algorithm = 'aes-256-cbc';
// CBC 模式使用 16 字节 IV
const iv = crypto.randomBytes(16);
// 创建加密器(默认自动使用 PKCS7 填充)
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
encrypted: encrypted,
iv: iv.toString('hex')
};
}
// ==================== 解密函数 ====================
function decryptCBC(encryptedData, key) {
const algorithm = 'aes-256-cbc';
const iv = Buffer.from(encryptedData.iv, 'hex');
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// ==================== 使用示例 ====================
const key = crypto.randomBytes(32); // 32 字节 = 256 位
const result = encryptCBC('这是敏感数据', key);
console.log('CBC 加密结果:', result.encrypted);
const decrypted = decryptCBC(result, key);
console.log('CBC 解密结果:', decrypted);
4.2 CBC 模式注意事项
-
不考虑完整性验证时,CBC 加密结果不可信:如果中间人篡改密文,CBC 模式无法检测。生产环境建议配合 HMAC(Encrypt-then-MAC)使用,或直接迁移到 GCM 模式。
-
填充模式 :Node.js 的
createCipheriv默认使用 PKCS7 填充。如果服务端要求其他填充模式(如 ZeroPadding),需要手动实现,这通常是跨语言对接时的常见坑点。 -
IV 必须随机不可预测:CBC 模式下 IV 必须是安全随机数,不可使用固定值或可预测的序列号。
5. 密钥生成与管理
5.1 安全随机密钥生成
javascript
const crypto = require('crypto');
// 生成 32 字节(256 位)密钥
const key = crypto.randomBytes(32);
console.log('密钥 (Buffer):', key);
console.log('密钥 (hex):', key.toString('hex'));
console.log('密钥 (base64):', key.toString('base64'));
crypto.randomBytes() 生成的是密码学安全的随机数据,在 Node.js 中底层由 OpenSSL 的安全随机数生成器实现。生成 32 字节的密钥对应 AES-256,这是欧盟 ENISA 推荐的密钥长度。生成后的密钥可用 Base64 编码存入 keybase64 配置字段,长度刚好为 44 个字符。
5.2 从密码派生密钥(PBKDF2)
当用户只需要记忆密码而不是随机密钥时,需要使用 PBKDF2 从密码派生 AES 密钥:
javascript
const crypto = require('crypto');
// 生成随机 salt(存储时可与密文一起保存)
const salt = crypto.randomBytes(16);
// 从密码派生 32 字节密钥(PBKDF2 需要 10 万次迭代)
const password = 'user-master-password';
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
console.log('派生密钥 (hex):', key.toString('hex'));
PBKDF2 通过大量迭代计算消耗攻击者的暴力破解成本。10 万次迭代是 OWASP 当前推荐的最低值,生产中可选用更高迭代次数或更现代的 Argon2id。
6. 流式加密(处理大文件)
当需要加密大文件或数据流时,一次性将所有内容读入内存可能造成内存压力。crypto 模块的 Cipher/Decipher 对象本身就是可写/可读流,天然支持流式处理。
javascript
const crypto = require('crypto');
const fs = require('fs');
const algorithm = 'aes-256-gcm';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(12);
// 创建加密流
const cipher = crypto.createCipheriv(algorithm, key, iv);
cipher.setAAD(Buffer.from('file-metadata', 'utf8'));
// 创建读写流
const input = fs.createReadStream('large-file.txt');
const output = fs.createWriteStream('large-file.enc');
// 管道连接(流式加密)
input.pipe(cipher).pipe(output);
// 加密完成后获取认证标签
output.on('finish', () => {
const authTag = cipher.getAuthTag();
console.log('认证标签:', authTag.toString('hex'));
// 实际应用中需保存 iv 和 authTag 以备解密
});
对于 GCM 模式的流式解密,需要在创建 Decipher 后先通过 setAuthTag() 设置认证标签,再开始解密密文流。
7. Base64 编码配置与 AAD 参数使用的完整案例
结合此前对话中 keybase64 和 aad 的配置需求,以下是一个与 JSON 配置文件结合的完整示例:
javascript
const crypto = require('crypto');
// 模拟配置文件中的参数(实际应从安全存储中读取)
const config = {
"algorithm": "AES-256-GCM",
"keybase64": "AsaiFfQ1hMYYvT2N4Y1BhenJ0RjY0M2cxWTMZXdXMVY=",
"aad": "application:eu-userdata;version:1;intent:storage"
};
// 1. 从 Base64 解码密钥
const key = Buffer.from(config.keybase64, 'base64');
console.log('密钥长度:', key.length, '字节'); // 应输出 32
// 2. 生成随机 Nonce(每次加密需要新的)
const nonce = crypto.randomBytes(12);
// 3. 加密
function encryptWithConfig(plaintext, config) {
const key = Buffer.from(config.keybase64, 'base64');
const nonce = crypto.randomBytes(12);
const aad = Buffer.from(config.aad, 'utf8');
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
cipher.setAAD(aad);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted: encrypted,
nonce: nonce.toString('base64'),
authTag: authTag.toString('base64')
};
}
const encryptedResult = encryptWithConfig('GDPR保护的敏感数据', config);
console.log('加密结果:', encryptedResult);
通过这种方式,keybase64 中的密钥始终以安全的随机密钥存储并正确解码,AAD 绑定了业务上下文,确保在欧盟监管环境下的合规性。
8. 安全最佳实践总结
| 实践要求 | 具体措施 |
|---|---|
| 推荐算法 | 新系统统一使用 AES-256-GCM |
| 密钥长度 | 256 位(32 字节),符合 GDPR 和 ENISA 推荐 |
| 密钥安全 | 通过 crypto.randomBytes(32) 生成;存储在 KMS 或 HSM 中;禁止硬编码在源码中 |
| 密钥编码 | 使用 Base64 配置字段 keybase64,解码后长度严格 32 字节(AES-256) |
| IV/Nonce | GCM 用 12 字节,CBC 用 16 字节;每次加密全新随机生成;GCM 下严禁重用 |
| 认证标签 | GCM 模式下必须生成、安全保存并在解密前验证;任何不匹配都应中断流程并记录 |
| AAD | 绑定业务上下文(应用名、版本号、操作意图等),虽不加密但加解密两端必须一致 |
| 废弃 API | 不再使用已弃用的 createCipher() 接口 |
| 错误处理 | 用 try-catch 捕获认证标签验证失败等异常,记录安全事件 |
| 密码场景 | 使用 PBKDF2(10 万次迭代以上)或 Argon2 从密码派生密钥 |
8.1 注意事项
- 密钥生命周期管理:遵循欧盟 GDPR 第 32 条及相关技术指南,密钥不应长期复用。推荐实施密钥轮换、访问控制和审计日志。
- 避免常见陷阱:GCM 模式最致命的错误是 IV 重用------一旦重用,攻击者可以通过代数关系恢复认证密钥,导致机密性和完整性完全丧失。在生产环境中,可以使用计数器结合随机数的方式确保唯一性。
- 系统兼容性:当必须对接使用 CBC 的旧系统时,严格遵守 Encrypt-then-MAC 模式,即在加密后对密文计算 HMAC,解密前先验证 HMAC 再解密。
通过以上从基础加密/解密、流式处理到安全实践的全面讲解,你应该能够在 Node.js 中正确、安全地使用原生 crypto 模块实现 AES 加密,并满足生产环境下的安全与合规要求。如果在实际项目中遇到特定的技术细节或兼容性问题,可以在此基础上进一步探讨。