You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
        });
    });
}

问题根源

  1. IV不匹配:加密时生成的随机IV未与加密数据一同保存,解密时重新生成新IV,导致初始块解密完全错误,后续块因CBC模式特性出现部分损坏。
  2. 流事件监听错误:原函数监听inputStream的end事件完成Promise,但未确保所有加密后的数据都写入输出流,可能导致数据截断。
  3. 密钥处理不严谨:虽当前密钥长度符合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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 19:45:12