node.js使用AES加密/解密的详细教程

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 的多种工作模式中,常用的有CBCGCM两种:

特性 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:认证标签验证失败如何处理?

认证标签不匹配意味着以下可能之一:

  1. 密钥错误
  2. 密文被篡改
  3. AAD 与加密时不符
  4. 解密方法用错了加密算法

应在代码中用 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 模式注意事项

  1. 不考虑完整性验证时,CBC 加密结果不可信:如果中间人篡改密文,CBC 模式无法检测。生产环境建议配合 HMAC(Encrypt-then-MAC)使用,或直接迁移到 GCM 模式。

  2. 填充模式 :Node.js 的 createCipheriv 默认使用 PKCS7 填充。如果服务端要求其他填充模式(如 ZeroPadding),需要手动实现,这通常是跨语言对接时的常见坑点。

  3. 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 参数使用的完整案例

结合此前对话中 keybase64aad 的配置需求,以下是一个与 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 加密,并满足生产环境下的安全与合规要求。如果在实际项目中遇到特定的技术细节或兼容性问题,可以在此基础上进一步探讨。

相关推荐
Rabbit_QL2 小时前
【前端工具链小白篇】前端工具链全景:Node、npm、Vite 各管什么
前端·npm·node.js
身如柳絮随风扬2 小时前
前端基础进阶:Node.js + ES6 + Axios + Vue 全面入门指南
前端·node.js·es6
Weisley18 小时前
从 Java 到 Node:我如何理解 async/await 和 fetch
node.js
六bring个六1 天前
opencv简单操作(一)
前端·webpack·node.js
接着奏乐接着舞。1 天前
【Node】Cluster和死锁问题
node.js
不会敲代码11 天前
深入理解 LangChain 文本分割器:为什么 RecursiveCharacterTextSplitter 是 RAG 的标配
langchain·node.js
天外飞雨道沧桑1 天前
Node.js在前端开发中扮演的角色
前端·node.js
神奇小梵1 天前
CTFSHOW的node.js漏洞
node.js
zhensherlock2 天前
Protocol Launcher 系列:Tally 快速计数器的深度集成
前端·javascript·typescript·node.js·自动化·github·js