JavaScript文件加解密校验和不匹配问题求助
JavaScript文件加解密校验和不匹配问题排查与修复
问题描述
实现了带SHA256校验和的AES-256-CBC文件加解密逻辑,但解密后校验和始终不匹配,同时原UTF-8文本文件加密后编码异常。核心问题表现为:
- 解密得到的校验和与重新计算的明文校验和完全不一致
- 加密后文件编码变为utf16le/ANSI,与原文件编码不符
核心问题分析
加密数据拼接与解密逻辑不匹配
加密时将加密的文件内容和加密的校验和独立加密后追加写入文件,但解密时错误地将这两部分合并后一起解密,导致数据完全错乱。文件读写的编码处理错误
- 加密时用
utf8编码读取文件,会破坏二进制文件或非UTF-8文本的原始字节;解密时用binary编码读取加密文件,会导致Buffer转换异常。 - 错误假设加密后的校验和长度为64字节,但AES-CBC加密输出是16字节块的倍数,实际长度会随输入变化,直接截取最后64字节必然得到错误数据。
- 加密时用
校验和的加密/解密流程错误
加密校验和时将十六进制字符串按utf8传入,但解密后直接将二进制Buffer当作字符串对比,未做编码转换。
修复步骤
1. 统一用二进制模式处理文件读写
所有文件操作都直接读取/写入Buffer,避免编码转换破坏原始字节。
2. 明确加密文件的结构
加密文件采用固定结构:[16字节IV] + [4字节加密内容长度] + [加密文件内容] + [加密校验和],确保解密时能准确拆分各部分数据。
3. 独立解密文件内容与校验和
对加密后的文件内容和校验和分别解密,再将解密后的校验和转成字符串与重新计算的校验和对比。
修正后的完整代码
加密函数
const { randomBytes, createCipheriv, createHash } = require('crypto'); const { readFileSync, writeFileSync } = require('fs'); // 注意:AES-256-CBC要求密钥为32字节(64个十六进制字符) const ENCRYPTION_KEY = 'your-32-byte-hex-key-here'; function encryptAndMoveFile(sourcePath, destinationPath) { try { const iv = randomBytes(16); // 读取原始文件二进制数据,避免编码转换 const sourceBuffer = readFileSync(sourcePath); // 计算原始数据的SHA256校验和(十六进制字符串) const checksum = createHash('sha256').update(sourceBuffer).digest('hex'); // 加密文件内容 const fileCipher = createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv); const encryptedData = Buffer.concat([fileCipher.update(sourceBuffer), fileCipher.final()]); // 加密校验和(将十六进制字符串转成Buffer后加密) const checksumCipher = createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv); const encryptedChecksum = Buffer.concat([checksumCipher.update(checksum, 'utf8'), checksumCipher.final()]); // 构建加密文件:IV + 加密内容长度(4字节大端) + 加密内容 + 加密校验和 const contentLengthBuffer = Buffer.alloc(4); contentLengthBuffer.writeUInt32BE(encryptedData.length, 0); const finalBuffer = Buffer.concat([iv, contentLengthBuffer, encryptedData, encryptedChecksum]); writeFileSync(destinationPath, finalBuffer); console.log('加密完成'); console.log('IV:', iv.toString('hex')); console.log('原始校验和:', checksum); } catch (err) { console.error('加密错误:', err); } }
解密函数
const { createDecipheriv, createHash } = require('crypto'); const { readFileSync } = require('fs'); const ENCRYPTION_KEY = 'your-32-byte-hex-key-here'; // 与加密密钥一致 function decryptFile(filePath) { try { // 读取加密文件的二进制Buffer const encryptedBuffer = readFileSync(filePath); // 拆分各部分数据 const iv = encryptedBuffer.subarray(0, 16); const contentLength = encryptedBuffer.subarray(16, 20).readUInt32BE(0); const encryptedData = encryptedBuffer.subarray(20, 20 + contentLength); const encryptedChecksum = encryptedBuffer.subarray(20 + contentLength); // 解密文件内容 const dataDecipher = createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv); const decryptedDataBuffer = Buffer.concat([dataDecipher.update(encryptedData), dataDecipher.final()]); // 解密校验和并转成UTF-8字符串 const checksumDecipher = createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv); const decryptedChecksum = Buffer.concat([checksumDecipher.update(encryptedChecksum), checksumDecipher.final()]).toString('utf8'); // 重新计算解密后数据的校验和 const calculatedChecksum = createHash('sha256').update(decryptedDataBuffer).digest('hex'); console.log('解密IV:', iv.toString('hex')); console.log('解密得到的校验和:', decryptedChecksum); console.log('重新计算的校验和:', calculatedChecksum); if (calculatedChecksum !== decryptedChecksum) { throw new Error('文件完整性校验失败:校验和不匹配'); } // 若原文件是文本文件,转成UTF-8字符串;二进制文件直接返回decryptedDataBuffer const decryptedContent = decryptedDataBuffer.toString('utf8'); return decryptedContent; } catch (err) { console.error('解密错误:', err); throw err; } }
关键注意事项
- 密钥合法性:AES-256-CBC要求密钥为32字节(64个十六进制字符),若密钥长度不符,Node.js会自动截断或抛出错误。
- 二进制优先:所有文件操作都用二进制模式,避免编码转换导致的字节丢失或错乱。
- 数据拆分可靠性:通过固定长度的IV和显式的长度字段拆分加密数据,避免依赖固定长度的加密结果(AES加密结果长度是块大小的倍数,会随输入变化)。
内容的提问来源于stack exchange,提问作者Daniel Corona
相关产品推荐
相关产品推荐

