【Rust探索之旅】构建安全、高效的本地待办事项工具 TodoLite(系列三)

TodoLite 系列 (三):为数据加锁,深入 Ring 库实现 AES-256 加密

1. 引言:为什么本地数据也需要加密

之前,我们为 TodoLite 构建了坚实的数据模型和健壮的存储层。我们的任务数据现在可以安全地在内存和本地文件之间流转。但是,"安全"目前仅限于程序运行层面。如果我们的电脑丢失或被他人访问,tasks.json 文件就是一个毫无防备的明文文件,任何人都可以打开并查看我们的待办事项,这无疑是一种隐私泄露,这也是为什么本地我们也需要做加密。

本篇,我们将为 TodoLite 加上最关键的一道防线:端到端加密。我们将使用 Rust 生态中备受推崇的加密库 ring,实现工业级的 AES-256-GCM 加密,确保即使文件被他人获取,没有正确的密码也无法窥探其中分毫。

2. 加密策略:不仅仅是“密码”那么简单

一个常见的误解是,可以直接用用户的密码作为加密密钥。这是一个极其危险的做法!

  1. 密码长度不固定:加密算法通常需要固定长度的密钥(如 AES-256 需要 32 字节)。
  2. 密码熵值低:用户设置的密码往往不够随机(如 “abc123、生日、电话号码等”),容易被字典攻击破解、暴力撞库破解。

正确的做法是使用密钥派生函数 (KDF),比如 PBKDF2。它的作用就像一个“密码处理器”,接收用户输入的任意密码,并经过一系列复杂的、可调节强度的计算,最终派生出一个固定长度、高随机性的加密密钥。

我们的加密流程将是:

  1. 用户提供一个主密码。
  2. 我们生成一个随机的盐 (Salt)
  3. 使用 PBKDF2,结合主密码和盐,派生出 32 字节的加密密钥。
  4. 使用派生出的密钥,通过 AES-256-GCM 算法加密我们的任务数据。
  5. Nonce (一个加密时使用的随机数) 和加密后的密文拼接在一起,存入文件。

解密时,流程相反:从文件中分离出盐、Nonce 和密文,要求用户输入主密码,结合盐重新派生出相同的密钥,然后进行解密。

核心概念解释:

  • 盐 (Salt):一串随机数据,它的作用是让 KDF 的计算过程变得独一无二。即使两个用户使用了完全相同的密码,因为他们的盐是不同的,派生出的密钥也会截然不同。这能有效抵御彩虹表攻击。盐是公开的,和密文一起存储即可。
  • AES-256-GCM:一种高级的对称加密标准。它不仅提供机密性(加密),还提供真实性完整性验证(AEAD特性)。这意味着如果加密后的数据被篡改过,解密过程会直接失败,而不是解密出一堆无意义的乱码。
  • Nonce (Number used once):每次加密操作都必须使用一个唯一的 Nonce。在同一密钥下,绝不能重复使用 Nonce,否则会带来毁灭性的安全问题。最简单安全的策略就是每次加密都生成一个新的随机 Nonce。

3. 集成 ring 库,编写加密核心

首先,将 ring 添加到 Cargo.toml

[dependencies]
# ... 其他依赖 ...
ring = "0.17"

ring 的版本可能会更新,建议使用最新的稳定版。

接下来,我们将直接在 src/storage.rs 中实现加密和解密逻辑。我们需要定义一些常量,并扩充我们的 AppError 枚举。

// src/storage.rs

// ... use 语句 ...

// 新增 ring::error::Unspecified 的 From 实现
impl From<ring::error::Unspecified> for AppError {
    fn from(_: ring::error::Unspecified) -> Self {
        AppError::CryptoError
    }
}

#[derive(Debug)]
pub enum AppError {
    Io(std::io::Error),
    Serialization(serde_json::Error),
    CryptoError, // 新增加密错误变体
}

现在,让我们定义加密相关的常量。这些参数决定了加密的强度和数据结构。

// src/storage.rs ...

use ring::{aead, pbkdf2, rand as ring_rand, digest};
use std::num::NonZeroU32;

// --- 加密常量 ---
const PBKDF2_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
const PBKDF2_ITERATIONS: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(100_000) }; // 迭代次数,越高越安全
const SALT_LEN: usize = 16;
const AEAD_ALG: &aead::Algorithm = &aead::AES_256_GCM;
const NONCE_LEN: usize = 12; // AES-GCM 标准 Nonce 长度

4. 实现 encrypt 函数

这个函数将接收明文数据(&[u8])和用户密码,返回加密后的字节向量 Vec<u8>

// 在 storage.rs 中添加 encrypt 函数
fn encrypt(data: &[u8], password: &str) -> AppResult<Vec<u8>> {
    // 1. 生成一个随机的盐 (Salt)
    let mut salt = vec![0u8; SALT_LEN];
    let rng = ring_rand::SystemRandom::new();
    rng.fill(&mut salt)?;

    // 2. 使用 PBKDF2 从密码和盐派生密钥
    let mut key = vec![0u8; AEAD_ALG.key_len()];
    pbkdf2::derive(
        PBKDF2_ALG,
        PBKDF2_ITERATIONS,
        &salt,
        password.as_bytes(),
        &mut key,
    );

    // 3. 准备 AEAD 加密
    //    UnboundKey 是一个准备好加密的密钥,但还未绑定 Nonce
    let unbound_key = aead::UnboundKey::new(AEAD_ALG, &key)?;
    
    // 4. 生成一个随机的 Nonce
    let mut nonce_bytes = vec![0u8; NONCE_LEN];
    rng.fill(&mut nonce_bytes)?;
    let nonce = aead::Nonce::try_assume_unique_for_key(&nonce_bytes)?;

    // 5. 将数据附加到缓冲区并进行加密
    //    我们将明文数据复制到一个新的 Vec 中,ring 会在其后附加加密标签 (Tag)
    let mut buffer = data.to_vec();
    
    // SealingKey 将密钥和 Nonce 绑定,用于加密
    let sealing_key = aead::SealingKey::new(unbound_key, nonce);
    aead::seal_in_place_append_tag(sealing_key, aead::Aad::empty(), &mut buffer)?;
    
    // 6. 组合最终的输出: salt + nonce + ciphertext
    let mut final_data = Vec::with_capacity(SALT_LEN + NONCE_LEN + buffer.len());
    final_data.extend_from_slice(&salt);
    final_data.extend_from_slice(&nonce_bytes);
    final_data.extend_from_slice(&buffer);

    Ok(final_data)
}

5. 实现 decrypt 函数

解密是加密的逆过程。它接收加密后的数据和密码,返回解密后的明文。

// 在 storage.rs 中添加 decrypt 函数
fn decrypt(encrypted_data: &[u8], password: &str) -> AppResult<Vec<u8>> {
    // 1. 从加密数据中分离 salt, nonce, 和密文
    if encrypted_data.len() < SALT_LEN + NONCE_LEN {
        // 如果数据长度连 salt 和 nonce 都不够,肯定是无效的
        return Err(AppError::CryptoError); 
    }
    let salt = &encrypted_data[..SALT_LEN];
    let nonce_bytes = &encrypted_data[SALT_LEN..SALT_LEN + NONCE_LEN];
    let ciphertext = &encrypted_data[SALT_LEN + NONCE_LEN..];

    // 2. 使用相同的参数,从密码和提取出的盐重新派生密钥
    let mut key = vec![0u8; AEAD_ALG.key_len()];
    pbkdf2::derive(
        PBKDF2_ALG,
        PBKDF2_ITERATIONS,
        salt,
        password.as_bytes(),
        &mut key,
    );

    // 3. 准备 AEAD 解密
    let unbound_key = aead::UnboundKey::new(AEAD_ALG, &key)?;
    let nonce = aead::Nonce::try_assume_unique_for_key(nonce_bytes)?;
    
    // 4. 将密文复制到缓冲区并进行解密
    let mut buffer = ciphertext.to_vec();
    
    // OpeningKey 用于解密
    let opening_key = aead::OpeningKey::new(unbound_key, nonce);
    let decrypted_data = aead::open_in_place(opening_key, aead::Aad::empty(), &mut buffer)?;
    
    // open_in_place 成功后,buffer 的前 decrypted_data.len() 字节就是明文
    Ok(decrypted_data.to_vec())
}

6. 集成到 save_tasksload_tasks

现在,我们需要修改 save_tasksload_tasks,让它们接收密码参数,并调用我们刚写好的加密/解密函数。

// 修改 save_tasks 和 load_tasks

pub fn save_tasks(tasks: &[Task], password: &str) -> AppResult<()> {
    // 1. 先将任务序列化为 JSON 字符串 (明文)
    let json_data = serde_json::to_string_pretty(tasks)?;

    // 2. 加密 JSON 数据
    let encrypted_data = encrypt(json_data.as_bytes(), password)?;

    // 3. 将加密后的二进制数据写入文件
    //    注意这里不再需要 BufWriter,因为我们是一次性写入整个 Vec
    std::fs::write(FILE_PATH, encrypted_data)?;
    Ok(())
}

pub fn load_tasks(password: &str) -> AppResult<Vec<Task>> {
    if !std::path::Path::new(FILE_PATH).exists() {
        return Ok(Vec::new());
    }

    // 1. 从文件读取所有加密后的二进制数据
    let encrypted_data = std::fs::read(FILE_PATH)?;

    // 2. 如果文件为空,也视为新用户
    if encrypted_data.is_empty() {
        return Ok(Vec::new());
    }

    // 3. 解密数据
    let decrypted_bytes = decrypt(&encrypted_data, password)?;

    // 4. 将解密后的 JSON 字节反序列化为任务列表
    let tasks = serde_json::from_slice(&decrypted_bytes)?;
    Ok(tasks)
}

最后,更新一下 main.rs 来模拟密码输入和调用新的函数。请注意,在命令行原型中,我们将密码硬编码,这只是为了测试方便,在最终的 GUI 应用中我们会使用安全的输入框。

// src/main.rs (简化版,仅用于测试)

fn main() {
    let password = "a_very_secret_password"; // !! 仅用于测试 !!

    match storage::load_tasks(password) {
        Ok(mut tasks) => {
            // ... (与之前类似) ...
            println!("成功加载 {} 个任务。", tasks.len());
            
            // ... (添加或修改任务的逻辑) ...

            if let Err(e) = storage::save_tasks(&tasks, password) {
                eprintln!("错误:保存任务失败! {:?}", e);
            } else {
                println!("任务已加密并保存。");
            }
        }
        Err(e) => {
            eprintln!("错误:加载任务失败!可能是密码错误或文件已损坏。 {:?}", e);
        }
    }
}

现在运行 cargo run,然后打开 tasks.json 文件。你看到的将不再是可读的 JSON,而是一堆乱码——这正是我们想要的加密效果!

总结

至此我们已经为 TodoLite 构建了一个坚不可摧的保险库。

  • 深入理解了为何需要对本地数据进行加密,以及 KDF、Salt 和 Nonce 的重要性。
  • 引入了强大的 ring 库,并学习了如何使用 PBKDF2 和 AES-256-GCM。
  • 亲手实现了 encryptdecrypt 函数,并将它们无缝集成到了我们的存储层中。

TodoLite 的核心逻辑已经越来越完善。从下一篇开始,我们将为它打造一个用户界面,首先是一个功能完备的命令行工具,让你能真正地开始使用它来管理你的待办事项。



附:完整的 src/storage.rs 代码

下面我们看到的,是 TodoLite 项目中负责数据持久化与安全的核心模块——storage.rs 的完整代码。它看似不长,却融合了 Rust 语言中几个至关重要的编程思想:模块化封装、健壮的错误处理、高性能 I/O 以及工业级加密。让我们逐一拆解,深入理解其设计精髓。

use crate::model::Task;
use ring::{aead, pbkdf2, rand as ring_rand};
use std::fs::File;
use std::io::{Read, Write};
use std::num::NonZeroU32;
use std::path::Path;

// --- 错误处理 ---

#[derive(Debug)]
pub enum AppError {
    Io(std::io::Error),
    Serialization(serde_json::Error),
    CryptoError,
}

impl From<std::io::Error> for AppError {
    fn from(error: std::io::Error) -> Self {
        AppError::Io(error)
    }
}

impl From<serde_json::Error> for AppError {
    fn from(error: serde_json::Error) -> Self {
        AppError::Serialization(error)
    }
}

impl From<ring::error::Unspecified> for AppError {
    fn from(_: ring::error::Unspecified) -> Self {
        AppError::CryptoError
    }
}

pub type AppResult<T> = Result<T, AppError>;

// --- 常量定义 ---

const FILE_PATH: &str = "tasks.json";
const PBKDF2_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
const PBKDF2_ITERATIONS: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(100_000) };
const SALT_LEN: usize = 16;
const AEAD_ALG: &aead::Algorithm = &aead::AES_256_GCM;
const NONCE_LEN: usize = 12;

// --- 加密/解密核心 ---

fn encrypt(data: &[u8], password: &str) -> AppResult<Vec<u8>> {
    let mut salt = vec![0u8; SALT_LEN];
    let rng = ring_rand::SystemRandom::new();
    rng.fill(&mut salt)?;

    let mut key = vec![0u8; AEAD_ALG.key_len()];
    pbkdf2::derive(
        PBKDF2_ALG,
        PBKDF2_ITERATIONS,
        &salt,
        password.as_bytes(),
        &mut key,
    );

    let unbound_key = aead::UnboundKey::new(AEAD_ALG, &key)?;

    let mut nonce_bytes = vec![0u8; NONCE_LEN];
    rng.fill(&mut nonce_bytes)?;
    let nonce = aead::Nonce::try_assume_unique_for_key(&nonce_bytes)?;

    let mut buffer = data.to_vec();
    let sealing_key = aead::SealingKey::new(unbound_key, nonce);
    aead::seal_in_place_append_tag(sealing_key, aead::Aad::empty(), &mut buffer)?;

    let mut final_data = Vec::with_capacity(SALT_LEN + NONCE_LEN + buffer.len());
    final_data.extend_from_slice(&salt);
    final_data.extend_from_slice(&nonce_bytes);
    final_data.extend_from_slice(&buffer);

    Ok(final_data)
}

fn decrypt(encrypted_data: &[u8], password: &str) -> AppResult<Vec<u8>> {
    if encrypted_data.len() < SALT_LEN + NONCE_LEN {
        return Err(AppError::CryptoError);
    }
    let salt = &encrypted_data[..SALT_LEN];
    let nonce_bytes = &encrypted_data[SALT_LEN..SALT_LEN + NONCE_LEN];
    let ciphertext = &encrypted_data[SALT_LEN + NONCE_LEN..];

    let mut key = vec![0u8; AEAD_ALG.key_len()];
    pbkdf2::derive(
        PBKDF2_ALG,
        PBKDF2_ITERATIONS,
        salt,
        password.as_bytes(),
        &mut key,
    );

    let unbound_key = aead::UnboundKey::new(AEAD_ALG, &key)?;
    let nonce = aead::Nonce::try_assume_unique_for_key(nonce_bytes)?;

    let mut buffer = ciphertext.to_vec();
    let opening_key = aead::OpeningKey::new(unbound_key, nonce);
    let decrypted_data = aead::open_in_place(opening_key, aead::Aad::empty(), &mut buffer)?;

    Ok(decrypted_data.to_vec())
}

// --- 公共 API ---

pub fn save_tasks(tasks: &[Task], password: &str) -> AppResult<()> {
    let json_data = serde_json::to_string_pretty(tasks)?;
    let encrypted_data = encrypt(json_data.as_bytes(), password)?;
    std::fs::write(FILE_PATH, encrypted_data)?;
    Ok(())
}

pub fn load_tasks(password: &str) -> AppResult<Vec<Task>> {
    if !Path::new(FILE_PATH).exists() {
        return Ok(Vec::new());
    }

    let encrypted_data = std::fs::read(FILE_PATH)?;
    if encrypted_data.is_empty() {
        return Ok(Vec::new());
    }

    let decrypted_bytes = decrypt(&encrypted_data, password)?;
    let tasks = serde_json::from_slice(&decrypted_bytes)?;
    Ok(tasks)
}

总而言之,storage.rs 不仅仅是一个保存文件的工具,它是一个设计精良、安全可靠的“数据保险箱”。它充分利用了 Rust 的语言特性,为我们的 TodoLite 应用提供了一个可以信赖的、坚实的数据基座。

转载请说明出处内容投诉
CSS教程网 » 【Rust探索之旅】构建安全、高效的本地待办事项工具 TodoLite(系列三)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买