TodoLite 系列 (三):为数据加锁,深入 Ring 库实现 AES-256 加密
1. 引言:为什么本地数据也需要加密
之前,我们为 TodoLite 构建了坚实的数据模型和健壮的存储层。我们的任务数据现在可以安全地在内存和本地文件之间流转。但是,"安全"目前仅限于程序运行层面。如果我们的电脑丢失或被他人访问,tasks.json 文件就是一个毫无防备的明文文件,任何人都可以打开并查看我们的待办事项,这无疑是一种隐私泄露,这也是为什么本地我们也需要做加密。
本篇,我们将为 TodoLite 加上最关键的一道防线:端到端加密。我们将使用 Rust 生态中备受推崇的加密库 ring,实现工业级的 AES-256-GCM 加密,确保即使文件被他人获取,没有正确的密码也无法窥探其中分毫。
2. 加密策略:不仅仅是“密码”那么简单
一个常见的误解是,可以直接用用户的密码作为加密密钥。这是一个极其危险的做法!
- 密码长度不固定:加密算法通常需要固定长度的密钥(如 AES-256 需要 32 字节)。
- 密码熵值低:用户设置的密码往往不够随机(如 “abc123、生日、电话号码等”),容易被字典攻击破解、暴力撞库破解。
正确的做法是使用密钥派生函数 (KDF),比如 PBKDF2。它的作用就像一个“密码处理器”,接收用户输入的任意密码,并经过一系列复杂的、可调节强度的计算,最终派生出一个固定长度、高随机性的加密密钥。
我们的加密流程将是:
- 用户提供一个主密码。
- 我们生成一个随机的盐 (Salt)。
- 使用 PBKDF2,结合主密码和盐,派生出 32 字节的加密密钥。
- 使用派生出的密钥,通过 AES-256-GCM 算法加密我们的任务数据。
- 将 盐、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_tasks 和 load_tasks
现在,我们需要修改 save_tasks 和 load_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。 - 亲手实现了
encrypt和decrypt函数,并将它们无缝集成到了我们的存储层中。
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 应用提供了一个可以信赖的、坚实的数据基座。