功能说明
密钥读进内存后,即使变量离开作用域,堆上的字节也可能留在未清零的页里,被转储、交换文件或复用内存的进程读到。Zeroize 提供在析构时覆盖内存的机制,ZeroizeOnDrop 派生宏让结构体在 drop 时自动擦除字段,编译器不会把这次写入优化掉。
示例把 API 密钥包进 ApiKey,禁止直接打印明文,只暴露只读访问方法;临时拼接的字符串在拼接完成后可以手动清零,缩短敏感数据的存在时间。
关键代码
结构体派生 Zeroize 与 ZeroizeOnDrop,字段 String 会先被覆写为零再释放。Debug 实现固定输出 REDACTED,避免日志、panic 信息或断言失败时泄露内容。expose 只返回借用,不复制明文,调用方无法长期持有。main 末尾对临时字符串调用 zeroize,再验证长度为零。
使用注意
擦除只能覆盖本进程分配的这块内存,无法清除已经被复制到别处的数据,例如格式化字符串、日志缓冲或操作系统换页。避免派生 Clone,否则明文会有多份副本。不要实现 Display,也不要让密钥进入错误类型。Zeroize 不保证防御所有编译器优化,极端威胁模型下还需配合内存锁定。密钥长度本身也可能敏感。
验证方式
第一行输出 ApiKey([REDACTED]),看不到明文;第二行输出明文长度二十二。临时字符串清零后 is_empty 返回 true,说明缓冲区已被覆盖。可以在作用域结束前打印指针内容做对比,但不要在生产日志中这样做。
代码示例
use std::fmt;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Zeroize, ZeroizeOnDrop)]
struct ApiKey(String);
impl ApiKey {
fn new(raw: impl Into<String>) -> Self {
Self(raw.into())
}
fn expose(&self) -> &str {
&self.0
}
}
impl fmt::Debug for ApiKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("ApiKey([REDACTED])")
}
}
fn main() {
let key = ApiKey::new("sk-live-example-123456");
println!("{:?}", key);
println!("length: {}", key.expose().len());
let mut temporary = String::from("temporary-secret");
temporary.zeroize();
println!("zeroized is empty: {}", temporary.is_empty());
}