Node.js Express文件上传流加密后解密文件损坏求助
解决Node.js Express文件加密解密后文件损坏的问题
问题描述
在运行Express的Node.js服务器上实现文件上传加密、下载解密功能时,直接管道传输未加密文件一切正常,但使用加密/解密函数后,文件会出现部分损坏。
损坏示例:
========= input.txt ============ Testing Document! This is just a testing document! ================================= ========= decrypted.txt ======== @h±²k5øcZxÁ·õ7ÌwBox! 5øcis is just a testing document! =================================
原代码
Express路由代码
app.post('/upload', (req, res) => { const busboyInstance = busboy({ headers: req.headers }); // Create a write stream to save the uploaded file let fileName; let writeStream; busboyInstance.on('file', (fieldname, file, filename) => { fileName = 'encrypted_file.txt'; // Modify the filename as needed const filePath = __dirname + '/' + fileName; writeStream = fs.createWriteStream(filePath); // Perform file encryption asynchronously. THIS PARTIALLY CORRUPTS THE FILE /* aesEncryptFileStream(file, writeStream, 'TESTESTESTESTESTESTESTESTESTESTE') .then(() => { file.resume(); // Consume the remaining stream }) .catch((error) => { console.error('Encryption error:', error); res.status(500).send('Encryption error'); });*/ //For debugging purposes we will just copy the file. THIS WORKS FINE file.pipe(writeStream); }); busboyInstance.on('finish', () => { res.end('Upload complete'); console.log('File upload complete'); }); req.pipe(busboyInstance); }); // Set up a route for handling file decryption and download app.get('/download', async (req, res) => { try { // Create a read stream from the encrypted file const fileName = 'encrypted_file.txt'; // Modify the filename as needed const filePath = __dirname + '/' + fileName; const readStream = fs.createReadStream(filePath); // Perform file decryption asynchronously. THIS PARTIALLY CORRUPTS THE FILE //await aesDecryptFile(readStream, res, "TESTESTESTESTESTESTESTESTESTESTE"); //For debugging purposes we will just copy the file. THIS WORKS FINE readStream.pipe(res); console.log('File download complete'); } catch (error) { console.error('Error:', error); res.status(500).send('Server error'); } });
原加密解密函数
async function aesEncryptFileStream(inputStream, outputStream, secret) { const algorithm = 'aes-256-cbc'; const iv = crypto.randomBytes(16); // Initialization vector. const cipher = crypto.createCipheriv(algorithm, secret, iv); return new Promise((resolve, reject) => { inputStream.pipe(cipher).pipe(outputStream); inputStream.on('end', () => { resolve(); }); inputStream.on('error', (err) => { reject(err); }); }); } async function aesDecryptFile(inputStream, outputStream, secret) { const algorithm = 'aes-256-cbc'; const iv = crypto.randomBytes(16); // Initialization vector. const decipher = crypto.createDecipheriv(algorithm, Buffer.from(secret), iv); return new Promise((resolve, reject) => { inputStream.pipe(decipher).pipe(outputStream); inputStream.on('end', () => { resolve(); }); inputStream.on('error', (err) => { reject(err); }); }); }
问题根源
- IV不匹配:加密时生成的随机IV未与加密数据一同保存,解密时重新生成新IV,导致初始块解密完全错误,后续块因CBC模式特性出现部分损坏。
- 流事件监听错误:原函数监听
inputStream的end事件完成Promise,但未确保所有加密后的数据都写入输出流,可能导致数据截断。 - 密钥处理不严谨:虽当前密钥长度符合aes-256要求,但未显式指定编码,存在隐性风险。
修正方案
核心思路
加密时将IV写入文件头部,解密时先读取头部16字节的IV,再用该IV进行解密;同时修正流事件监听逻辑,确保数据完整写入/输出。
修正后的加密解密函数
const crypto = require('crypto'); async function aesEncryptFileStream(inputStream, outputStream, secret) { const algorithm = 'aes-256-cbc'; const iv = crypto.randomBytes(16); // 先将IV写入输出流开头 outputStream.write(iv); const cipher = crypto.createCipheriv(algorithm, Buffer.from(secret, 'utf8'), iv); return new Promise((resolve, reject) => { inputStream.pipe(cipher).pipe(outputStream); // 监听输出流的finish事件,确保所有数据写入完成 outputStream.on('finish', resolve); // 监听所有流的错误事件 inputStream.on('error', reject); cipher.on('error', reject); outputStream.on('error', reject); }); } async function aesDecryptFile(inputStream, outputStream, secret) { const algorithm = 'aes-256-cbc'; return new Promise((resolve, reject) => { let ivBuffer = Buffer.alloc(16); let bytesRead = 0; inputStream.on('data', (chunk) => { if (bytesRead < 16) { // 读取头部的IV const copyLength = Math.min(chunk.length, 16 - bytesRead); chunk.copy(ivBuffer, bytesRead, 0, copyLength); bytesRead += copyLength; if (bytesRead === 16) { // IV读取完成,创建解密器并处理剩余数据 const decipher = crypto.createDecipheriv(algorithm, Buffer.from(secret, 'utf8'), ivBuffer); // 传递当前chunk剩余的部分数据 if (chunk.length > copyLength) { decipher.write(chunk.slice(copyLength)); } // 管道后续流 inputStream.pipe(decipher).pipe(outputStream); // 监听完成和错误事件 outputStream.on('finish', resolve); decipher.on('error', reject); outputStream.on('error', reject); } } }); inputStream.on('error', reject); }); }
修正后的路由代码
app.post('/upload', (req, res) => { const busboyInstance = busboy({ headers: req.headers }); let fileName; let writeStream; busboyInstance.on('file', async (fieldname, file, filename) => { fileName = 'encrypted_file.txt'; const filePath = __dirname + '/' + fileName; writeStream = fs.createWriteStream(filePath); try { await aesEncryptFileStream(file, writeStream, 'TESTESTESTESTESTESTESTESTESTESTE'); } catch (error) { console.error('Encryption error:', error); res.status(500).send('Encryption error'); return; } }); busboyInstance.on('finish', () => { res.end('Upload complete'); console.log('File upload complete'); }); req.pipe(busboyInstance); }); app.get('/download', async (req, res) => { try { const fileName = 'encrypted_file.txt'; const filePath = __dirname + '/' + fileName; const readStream = fs.createReadStream(filePath); // 设置响应头,确保客户端正确处理文件 res.setHeader('Content-Disposition', 'attachment; filename="decrypted.txt"'); res.setHeader('Content-Type', 'text/plain'); await aesDecryptFile(readStream, res, 'TESTESTESTESTESTESTESTESTESTESTE'); console.log('File decrypted and downloaded'); } catch (error) { console.error('Error:', error); res.status(500).send('Server error'); } });
额外注意事项
- 生产环境中密钥应使用安全随机生成方式,通过环境变量存储,避免硬编码。
- 推荐使用
aes-256-gcm模式,该模式自带数据认证功能,可检测文件是否被篡改。 - 处理大文件时,需完善错误捕获逻辑,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Wriar
相关产品推荐
相关产品推荐

