
本文详解如何使用 node.js 的 crypto 模块对文件进行 aes-256-cbc 加密/解密,并正确嵌入和校验 sha-256 校验和;重点修复原始代码中因未分离加密数据与加密校验和、忽略填充导致的校验失败问题,并指出更安全的替代方案(如 encrypt-then-mac 或 aead 模式)。
本文详解如何使用 node.js 的 crypto 模块对文件进行 aes-256-cbc 加密/解密,并正确嵌入和校验 sha-256 校验和;重点修复原始代码中因未分离加密数据与加密校验和、忽略填充导致的校验失败问题,并指出更安全的替代方案(如 encrypt-then-mac 或 aead 模式)。
在原始实现中,校验和验证失败的根本原因在于:加密后的校验和与主加密数据被错误地拼接并统一解密,而未作为独立密文单独解密。AES-CBC 是分组密码,其输出长度受 PKCS#7 填充影响,因此加密后的校验和(32 字节原始 hex)经 CBC 加密后长度不固定(通常为 48 或 64 字节),直接从末尾截取 64 字节会截断或混入有效密文,导致解密失败或内容错乱。
此外,原始代码将 sourceData 以 'utf8' 读取后计算 SHA-256,但二进制文件(如图片、PDF)无法用 UTF-8 安全表示——这会导致哈希值错误。校验和必须基于原始字节计算,而非文本编码后的字符串。
以下是修正后的完整实现(基于 Node.js crypto 模块):
const crypto = require('crypto');
const fs = require('fs');
const ENCRYPTION_KEY = 'your-64-hex-char-key-here'; // 32-byte key as hex string
function encryptAndMoveFile(sourcePath, destinationPath) {
try {
const iv = crypto.randomBytes(16);
const sourceData = fs.readFileSync(sourcePath); // ← 二进制读取,保留原始字节
// ✅ 正确:对原始字节计算 SHA-256,输出 32 字节 buffer
const checksum = crypto.createHash('sha256').update(sourceData).digest();
// ✅ 分别加密:主数据 + 校验和(均使用相同 IV)
const cipherData = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
const encryptedData = Buffer.concat([
cipherData.update(sourceData),
cipherData.final()
]);
const cipherChecksum = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
const encryptedChecksum = Buffer.concat([
cipherChecksum.update(checksum), // ← 直接传入 32-byte buffer,非 hex 字符串
cipherChecksum.final()
]);
// ✅ 写入顺序:IV (16) + 密文 + 加密校验和
const outputBuffer = Buffer.concat([iv, encryptedData, encryptedChecksum]);
fs.writeFileSync(destinationPath, outputBuffer);
console.log('✅ File encrypted successfully.');
} catch (err) {
throw new Error(`Encryption failed: ${err.message}`);
}
}
async function decryptFile(filePath) {
try {
const encryptedBuffer = fs.readFileSync(filePath);
// ✅ 精确分离各部分(注意:加密校验和长度不可预设为 64!需动态计算)
const iv = encryptedBuffer.slice(0, 16);
const ciphertext = encryptedBuffer.slice(16, -encryptedBuffer.length % 16 ? -16 : encryptedBuffer.length); // 简化示意,实际需知加密校验和长度
// 更健壮做法:加密时记录加密校验和长度(如写入头部),或约定固定长度(见下方优化版)
// ✅ 推荐优化:加密时将加密校验和长度(如 48 字节)写入文件头,此处略去;临时采用保守分离:
const encryptedChecksumLength = 48; // AES-CBC 加密 32-byte input 通常产出 48-byte output(2 block)
const ciphertextEnd = encryptedBuffer.length - encryptedChecksumLength;
const ciphertextSafe = encryptedBuffer.slice(16, ciphertextEnd);
const encryptedChecksum = encryptedBuffer.slice(ciphertextEnd);
// ✅ 分别解密
const decipherData = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
const decryptedData = Buffer.concat([
decipherData.update(ciphertextSafe),
decipherData.final()
]);
const decipherChecksum = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
const decryptedChecksumBuf = Buffer.concat([
decipherChecksum.update(encryptedChecksum),
decipherChecksum.final()
]);
// ✅ 校验和为原始 32-byte buffer,转 hex 用于比对
const decryptedChecksumHex = decryptedChecksumBuf.toString('hex');
const expectedChecksumHex = crypto.createHash('sha256').update(decryptedData).digest('hex');
if (decryptedChecksumHex !== expectedChecksumHex) {
throw new Error('❌ Integrity check failed: corrupted or tampered file.');
}
return decryptedData.toString('utf8'); // 仅当确定是文本文件时使用;否则返回 Buffer
} catch (err) {
throw new Error(`Decryption failed: ${err.message}`);
}
}⚠️ 关键注意事项与安全建议
-
永远避免对二进制文件调用 readFileSync(..., 'utf8'):这会破坏非文本字节,导致哈希失效。始终用 readFileSync(path) 获取 Buffer。
立即学习“Java免费学习笔记(深入)”;
校验和必须加密独立,且解密后与明文哈希比对:不能将校验和拼入主密文再整体解密。
-
不要自行设计认证机制:当前方案(Encrypt-then-Encrypt-Checksum)仍属“自制 MAC”,存在理论风险。生产环境应优先选择:
- ✅ Encrypt-then-MAC:用 HMAC-SHA256 对密文生成认证标签(无需加密校验和),验证通过后再解密;
- ✅ AEAD 模式(强烈推荐):如 aes-256-gcm,内建认证,一行代码完成加解密+验证:
// GCM 示例(自动保证机密性与完整性) const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); const authTag = cipher.getAuthTag(); // 16-byte tag // 存储:IV + encrypted + authTag
密钥管理:ENCRYPTION_KEY 必须安全存储(如环境变量、密钥管理服务),切勿硬编码。
IV 重用禁忌:每个加密操作必须使用全新随机 IV,且 IV 可公开传输(通常前置存储)。
综上,修复核心在于二进制处理、分离加密、正确解密校验和;但长远来看,请迁移到 GCM 或 ChaCha20-Poly1305 等现代 AEAD 方案——它们既简洁又经过严格密码学验证,是保障机密性与完整性的黄金标准。


















