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

如何分块加密大文件?Rust实现遇解密失败问题求助

分块加密大文件(原地替换)的问题修复方案

核心问题分析

你的代码存在三个致命问题,导致加密后无法解密:

  1. 重复使用同一个Nonce
    sodiumoxide的secretbox(基于XSalsa20-Poly1305)要求每个加密操作使用唯一的Nonce。重复使用相同Nonce和密钥会直接泄露密钥,导致解密失败,同时严重破坏加密安全性。

  2. 加密后数据膨胀覆盖原始数据
    secretbox::seal会为每个块附加16字节的Poly1305认证标签,加密后块大小 = 原块大小 + 16字节。你从前往后原地写入加密块时,会覆盖下一个块的前16字节原始数据,导致后续读取的原始数据损坏,解密自然失败。

  3. 解密时块大小不匹配
    解密阶段你仍按原CHUNK_SIZE读取,但加密后的块实际是原大小+16字节,读取的内容不完整,无法通过认证和解密。

解决方案

针对你的原地加密需求(50GB文件、有限内存/存储),调整方案如下:

1. 唯一Nonce生成策略

基于初始Nonce,为每个块生成唯一Nonce:将初始Nonce转换为字节数组,按块索引递增(XSalsa20的Nonce支持安全递增,不会重复)。

2. 从后往前处理块

为避免加密后的数据覆盖未处理的原始数据,从文件末尾开始向前处理每个块:

  • 先计算文件总长度和总加密后长度(原长度 + 块数×16),扩展文件到该长度。
  • 从最后一个块开始,读取原始数据→加密→写入到文件末尾方向的对应位置,确保原始数据不被提前覆盖。

3. 解密时匹配加密块大小

解密阶段同样从后往前处理,读取完整的加密块(原大小+16字节)→解密→写入回原位置,最后截断文件到原长度。

修改后的代码

use clap::Parser;
use sodiumoxide::crypto::secretbox;
use sodiumoxide::crypto::secretbox::{Key, Nonce};
use sodiumoxide::hex;
use std::fs::OpenOptions;
use std::io::{self, Read, Seek, SeekFrom, Write};

const CHUNK_SIZE: usize = 1024 * 1024; // 调整为1MB块,更适合大文件处理
const TAG_SIZE: usize = 16; // secretbox认证标签长度

#[derive(Parser)]
struct Options {
    #[clap(subcommand)]
    command: Command,
    #[clap(value_parser)]
    file_path: String,
}

#[derive(Parser)]
enum Command {
    Encrypt,
    Decrypt,
}

fn main() -> io::Result<()> {
    let args = Options::parse();
    sodiumoxide::init().expect("Failed to initialize sodiumoxide");

    match args.command {
        Command::Encrypt => encrypt_file(&args.file_path),
        Command::Decrypt => decrypt_file(&args.file_path),
    }
}

// 递增Nonce:为每个块生成唯一Nonce
fn increment_nonce(nonce: &Nonce, step: u64) -> Nonce {
    let mut nonce_bytes = nonce.as_ref().to_vec();
    // 将Nonce的最后8字节作为u64递增(兼容大索引)
    let mut counter = u64::from_le_bytes(nonce_bytes[16..24].try_into().unwrap());
    counter += step;
    nonce_bytes[16..24].copy_from_slice(&counter.to_le_bytes());
    Nonce::from_slice(&nonce_bytes).expect("Invalid nonce after increment")
}

fn encrypt_file(file_path: &str) -> io::Result<()> {
    let mut file = OpenOptions::new()
        .read(true)
        .write(true)
        .open(file_path)?;

    // 读取密钥和初始Nonce(实际使用中应随机生成Nonce并保存)
    let raw_key = hex::decode("ba72744932db553b55cb944aa8e5739cf23a7f668c32d164c51ce09d4d631160")
        .unwrap();
    let raw_nonce = hex::decode("48ccdb552de220538ac1667e7e99054fd39ad417c5d83c6e").unwrap();
    let key = Key::from_slice(&raw_key).expect("Invalid key");
    let base_nonce = Nonce::from_slice(&raw_nonce).expect("Invalid nonce");

    // 获取原文件总长度
    let file_len = file.seek(SeekFrom::End(0))?;
    let chunk_count = if file_len == 0 {
        0
    } else {
        (file_len as usize + CHUNK_SIZE - 1) / CHUNK_SIZE
    };
    let encrypted_len = file_len + chunk_count as u64 * TAG_SIZE as u64;

    // 扩展文件到加密后的总长度
    file.set_len(encrypted_len)?;

    // 从最后一个块开始向前处理
    for i in (0..chunk_count).rev() {
        let chunk_start = i as u64 * CHUNK_SIZE as u64;
        let chunk_size = if i == chunk_count - 1 {
            file_len as usize - i * CHUNK_SIZE
        } else {
            CHUNK_SIZE
        };

        // 读取原始块数据
        file.seek(SeekFrom::Start(chunk_start))?;
        let mut raw_chunk = vec![0u8; chunk_size];
        file.read_exact(&mut raw_chunk)?;

        // 生成唯一Nonce
        let nonce = increment_nonce(&base_nonce, i as u64);
        // 加密块
        let encrypted_chunk = secretbox::seal(&raw_chunk, &nonce, &key);

        // 计算加密块的写入位置:从文件末尾向前偏移
        let encrypted_start = encrypted_len - (i + 1) as u64 * (chunk_size + TAG_SIZE) as u64;
        file.seek(SeekFrom::Start(encrypted_start))?;
        file.write_all(&encrypted_chunk)?;
    }

    println!("Encryption completed. Encrypted file size: {} bytes", encrypted_len);
    Ok(())
}

fn decrypt_file(file_path: &str) -> io::Result<()> {
    let mut file = OpenOptions::new()
        .read(true)
        .write(true)
        .open(file_path)?;

    // 读取密钥和初始Nonce
    let raw_key = hex::decode("ba72744932db553b55cb944aa8e5739cf23a7f668c32d164c51ce09d4d631160")
        .unwrap();
    let raw_nonce = hex::decode("48ccdb552de220538ac1667e7e99054fd39ad417c5d83c6e").unwrap();
    let key = Key::from_slice(&raw_key).expect("Invalid key");
    let base_nonce = Nonce::from_slice(&raw_nonce).expect("Invalid nonce");

    // 获取加密文件总长度
    let encrypted_len = file.seek(SeekFrom::End(0))?;
    if encrypted_len == 0 {
        return Ok(());
    }

    // 计算原文件长度和块数
    // 先估算块数:假设所有块都是CHUNK_SIZE+16,最后一个块可能更小
    let estimated_chunk_count = (encrypted_len as usize + CHUNK_SIZE + TAG_SIZE - 1) / (CHUNK_SIZE + TAG_SIZE);
    let mut chunk_count = 0;
    let mut original_len = 0u64;

    // 计算实际块数和原文件长度
    let mut remaining = encrypted_len;
    for i in (0..estimated_chunk_count).rev() {
        let encrypted_chunk_size = if i == estimated_chunk_count - 1 {
            remaining as usize
        } else {
            CHUNK_SIZE + TAG_SIZE
        };
        original_len += (encrypted_chunk_size - TAG_SIZE) as u64;
        remaining -= encrypted_chunk_size as u64;
        if remaining == 0 {
            chunk_count = i + 1;
            break;
        }
    }

    // 从最后一个块开始向前处理
    for i in (0..chunk_count).rev() {
        // 计算加密块的位置和大小
        let encrypted_chunk_size = if i == chunk_count - 1 {
            (encrypted_len - (chunk_count - 1) as u64 * (CHUNK_SIZE + TAG_SIZE) as u64) as usize
        } else {
            CHUNK_SIZE + TAG_SIZE
        };
        let encrypted_start = encrypted_len - (i + 1) as u64 * encrypted_chunk_size as u64;

        // 读取加密块
        file.seek(SeekFrom::Start(encrypted_start))?;
        let mut encrypted_chunk = vec![0u8; encrypted_chunk_size];
        file.read_exact(&mut encrypted_chunk)?;

        // 生成对应Nonce
        let nonce = increment_nonce(&base_nonce, i as u64);
        // 解密块
        let raw_chunk = secretbox::open(&encrypted_chunk, &nonce, &key)
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Decryption failed"))?;

        // 写入回原位置
        let raw_start = i as u64 * CHUNK_SIZE as u64;
        file.seek(SeekFrom::Start(raw_start))?;
        file.write_all(&raw_chunk)?;
    }

    // 截断文件到原长度
    file.set_len(original_len)?;
    println!("Decryption completed. Original file size: {} bytes", original_len);
    Ok(())
}

关键改进点

  • 唯一Nonce:通过递增初始Nonce确保每个块使用唯一值,符合XSalsa20-Poly1305的安全要求。
  • 从后往前处理:避免加密后的数据覆盖未处理的原始数据,确保所有原始块都能正确读取。
  • 适配块大小:加密和解密阶段都正确处理最后一个不完整块的大小,匹配加密后的数据长度。
  • 内存优化:使用1MB块大小(可调整),避免占用过多内存,适合2GB内存的场景。

内容的提问来源于stack exchange,提问作者Sukhach17659

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 15:43:10