use crate::vault::{MASTER_KEY_LEN, MasterKey, NONCE_LEN as AEAD_NONCE_LEN, master_key_to_bytes};
use crate::{Error, ExposeSecret, Result, SecretBytes, SecretString};
use argon2::{Algorithm, Argon2, Params, Version};
use chacha20poly1305::aead::{AeadInOut, KeyInit};
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
use secrecy::zeroize::Zeroize;
const MAGIC: &[u8; 8] = b"Q2FAREC\0";
pub const FORMAT_VERSION: u8 = 1;
const ALGORITHM_ARGON2ID_V13: u8 = 1;
pub const SALT_LEN: usize = 16;
pub const DEFAULT_MEMORY_KIB: u32 = 64 * 1024;
pub const DEFAULT_ITERATIONS: u32 = 3;
pub const PARALLELISM: u32 = 1;
const DERIVED_KEY_LEN: usize = 32;
pub const MAX_MEMORY_KIB: u32 = 1024 * 1024;
pub const MAX_ITERATIONS: u32 = 1000;
mod offset {
pub const MAGIC: usize = 0;
pub const VERSION: usize = 8;
pub const ALGORITHM: usize = 9;
pub const MEMORY: usize = 10;
pub const ITERATIONS: usize = 14;
pub const PARALLELISM: usize = 18;
pub const SALT: usize = 22;
pub const NONCE: usize = SALT + super::SALT_LEN;
pub const CIPHERTEXT: usize = NONCE + super::AEAD_NONCE_LEN;
pub const AAD: usize = super::offset::VERSION;
pub const AAD_END: usize = super::offset::CIPHERTEXT;
}
pub const FILE_LEN: usize = offset::CIPHERTEXT + MASTER_KEY_LEN + crate::vault::TAG_LEN;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KdfParams {
memory_kib: u32,
iterations: u32,
parallelism: u32,
}
impl KdfParams {
#[must_use]
pub const fn recommended() -> Self {
Self {
memory_kib: DEFAULT_MEMORY_KIB,
iterations: DEFAULT_ITERATIONS,
parallelism: PARALLELISM,
}
}
pub const fn new(memory_kib: u32, iterations: u32, parallelism: u32) -> Result<Self> {
if memory_kib < Params::MIN_M_COST {
return Err(Error::RecoveryParametersInvalid {
reason: "内存开销低于 Argon2 允许的下限",
});
}
if memory_kib > MAX_MEMORY_KIB {
return Err(Error::RecoveryParametersInvalid {
reason: "内存开销超出本实现允许的上限",
});
}
if iterations < Params::MIN_T_COST {
return Err(Error::RecoveryParametersInvalid {
reason: "迭代次数不得为 0",
});
}
if iterations > MAX_ITERATIONS {
return Err(Error::RecoveryParametersInvalid {
reason: "迭代次数超出本实现允许的上限",
});
}
if parallelism != PARALLELISM {
return Err(Error::RecoveryParametersInvalid {
reason: "本实现只支持并行度为 1",
});
}
Ok(Self {
memory_kib,
iterations,
parallelism,
})
}
#[must_use]
pub const fn memory_kib(&self) -> u32 {
self.memory_kib
}
#[must_use]
pub const fn iterations(&self) -> u32 {
self.iterations
}
#[must_use]
pub const fn parallelism(&self) -> u32 {
self.parallelism
}
}
impl Default for KdfParams {
fn default() -> Self {
Self::recommended()
}
}
pub fn export(
master: &MasterKey,
passphrase: &SecretString,
params: &KdfParams,
) -> Result<Vec<u8>> {
if passphrase.expose_secret().is_empty() {
return Err(Error::RecoveryPassphraseEmpty);
}
let mut salt = [0_u8; SALT_LEN];
crate::getrandom::fill(&mut salt).map_err(|_| Error::RandomnessUnavailable)?;
let mut nonce = [0_u8; AEAD_NONCE_LEN];
crate::getrandom::fill(&mut nonce).map_err(|_| Error::RandomnessUnavailable)?;
let mut key = derive_key(passphrase, &salt, params)?;
let mut file = Vec::with_capacity(FILE_LEN);
file.extend_from_slice(MAGIC);
file.push(FORMAT_VERSION);
file.push(ALGORITHM_ARGON2ID_V13);
file.extend_from_slice(¶ms.memory_kib.to_be_bytes());
file.extend_from_slice(¶ms.iterations.to_be_bytes());
file.extend_from_slice(¶ms.parallelism.to_be_bytes());
file.extend_from_slice(&salt);
file.extend_from_slice(&nonce);
let mut ciphertext = master.expose().to_vec();
let aad = file[offset::AAD..offset::AAD_END].to_vec();
let result = XChaCha20Poly1305::new(&Key::from(key)).encrypt_in_place(
&XNonce::from(nonce),
&aad,
&mut ciphertext,
);
key.zeroize();
result.map_err(|_| Error::RecoveryEncryptionFailed)?;
file.extend_from_slice(&ciphertext);
debug_assert_eq!(file.len(), FILE_LEN);
Ok(file)
}
pub fn import(file: &[u8], passphrase: &SecretString) -> Result<MasterKey> {
if file.len() != FILE_LEN {
return Err(Error::RecoveryFileMalformed {
reason: "长度不是恢复文件的固定长度",
});
}
if &file[offset::MAGIC..offset::VERSION] != MAGIC {
return Err(Error::RecoveryFileMalformed {
reason: "magic 不匹配,这不是本应用的恢复文件",
});
}
if file[offset::VERSION] != FORMAT_VERSION {
return Err(Error::UnsupportedFormatVersion {
version: file[offset::VERSION],
});
}
if file[offset::ALGORITHM] != ALGORITHM_ARGON2ID_V13 {
return Err(Error::UnsupportedFormatVersion {
version: file[offset::ALGORITHM],
});
}
let memory_kib = u32::from_be_bytes(
file[offset::MEMORY..offset::MEMORY + 4]
.try_into()
.map_err(|_| Error::RecoveryFileMalformed {
reason: "内存开销字段读取失败",
})?,
);
let iterations = u32::from_be_bytes(
file[offset::ITERATIONS..offset::ITERATIONS + 4]
.try_into()
.map_err(|_| Error::RecoveryFileMalformed {
reason: "迭代次数字段读取失败",
})?,
);
let parallelism = u32::from_be_bytes(
file[offset::PARALLELISM..offset::PARALLELISM + 4]
.try_into()
.map_err(|_| Error::RecoveryFileMalformed {
reason: "并行度字段读取失败",
})?,
);
let params = KdfParams::new(memory_kib, iterations, parallelism)?;
let mut salt = [0_u8; SALT_LEN];
salt.copy_from_slice(&file[offset::SALT..offset::SALT + SALT_LEN]);
let mut nonce = [0_u8; AEAD_NONCE_LEN];
nonce.copy_from_slice(&file[offset::NONCE..offset::NONCE + AEAD_NONCE_LEN]);
let mut key = derive_key(passphrase, &salt, ¶ms)?;
let mut plaintext = file[offset::CIPHERTEXT..].to_vec();
let aad = file[offset::AAD..offset::AAD_END].to_vec();
let result = XChaCha20Poly1305::new(&Key::from(key)).decrypt_in_place(
&XNonce::from(nonce),
&aad,
&mut plaintext,
);
key.zeroize();
result.map_err(|_| Error::RecoveryAuthenticationFailed)?;
let secret: SecretBytes = core::mem::take(&mut plaintext).into();
MasterKey::from_bytes(secret)
}
fn derive_key(
passphrase: &SecretString,
salt: &[u8; SALT_LEN],
params: &KdfParams,
) -> Result<[u8; DERIVED_KEY_LEN]> {
let params = Params::new(
params.memory_kib,
params.iterations,
params.parallelism,
Some(DERIVED_KEY_LEN),
)
.map_err(|_| Error::RecoveryParametersInvalid {
reason: "Argon2 拒绝了这组参数",
})?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut out = [0_u8; DERIVED_KEY_LEN];
argon2
.hash_password_into(passphrase.expose_secret().as_bytes(), salt, &mut out)
.map_err(|_| Error::RecoveryKdfFailed)?;
Ok(out)
}
#[must_use]
pub fn master_key_for_storage(master: &MasterKey) -> SecretBytes {
master_key_to_bytes(master)
}
#[cfg(test)]
mod tests {
use super::*;
use argon2::{AssociatedData, ParamsBuilder};
fn fast_params() -> KdfParams {
KdfParams::new(Params::MIN_M_COST, 1, 1).expect("下限参数应当合法")
}
fn passphrase(text: &str) -> SecretString {
SecretString::from(text)
}
#[test]
fn export_import_roundtrip() {
let master = MasterKey::generate().expect("随机源可用");
let file = export(
&master,
&passphrase("correct horse battery staple"),
&fast_params(),
)
.expect("导出应当成功");
assert_eq!(file.len(), FILE_LEN, "文件长度必须恒定");
let restored =
import(&file, &passphrase("correct horse battery staple")).expect("导入应当成功");
assert_eq!(restored.expose(), master.expose());
}
#[test]
fn wrong_passphrase_fails() {
let master = MasterKey::generate().expect("随机源可用");
let file = export(&master, &passphrase("right"), &fast_params()).expect("导出");
let result = import(&file, &passphrase("wrong"));
assert!(
matches!(result, Err(Error::RecoveryAuthenticationFailed)),
"错误口令应被拒绝"
);
}
#[test]
fn empty_passphrase_is_rejected_on_export() {
let master = MasterKey::generate().expect("随机源可用");
let result = export(&master, &passphrase(""), &fast_params());
assert!(matches!(result, Err(Error::RecoveryPassphraseEmpty)));
}
#[test]
fn any_single_byte_flip_is_detected() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("tamper test");
let file = export(&master, &phrase, &fast_params()).expect("导出");
let param_range = offset::MEMORY..offset::PARALLELISM + 4;
for index in 0..FILE_LEN {
if param_range.contains(&index) {
continue;
}
let mut tampered = file.clone();
tampered[index] ^= 0x01;
match import(&tampered, &phrase) {
Err(_) => {}
Ok(restored) => panic!(
"第 {index} 字节被翻转却成功导入(还原出的密钥与原密钥相同 = {})",
restored.expose() == master.expose()
),
}
}
}
#[test]
fn flipping_parameter_bytes_is_rejected_by_the_limits() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("param tamper test");
let file = export(&master, &phrase, &fast_params()).expect("导出");
for index in offset::MEMORY..offset::PARALLELISM + 4 {
let mut tampered = file.clone();
tampered[index] ^= 0x01;
match import(&tampered, &phrase) {
Err(Error::RecoveryParametersInvalid { .. }) => {}
Err(Error::RecoveryAuthenticationFailed) => {}
Err(other) => panic!("第 {index} 字节:意外的错误类型 {other}"),
Ok(restored) => panic!(
"第 {index} 字节被翻转却成功导入(密钥相同 = {})",
restored.expose() == master.expose()
),
}
}
}
#[test]
fn wrong_length_is_rejected() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("length test");
let file = export(&master, &phrase, &fast_params()).expect("导出");
for cut in [0_usize, 1, FILE_LEN - 1] {
assert!(
matches!(
import(&file[..cut], &phrase),
Err(Error::RecoveryFileMalformed { .. })
),
"长度 {cut} 应被拒绝"
);
}
let mut longer = file.clone();
longer.push(0);
assert!(matches!(
import(&longer, &phrase),
Err(Error::RecoveryFileMalformed { .. })
));
}
#[test]
fn wrong_magic_is_reported_as_malformed_not_wrong_passphrase() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("magic test");
let mut file = export(&master, &phrase, &fast_params()).expect("导出");
file[0] = b'X';
assert!(matches!(
import(&file, &phrase),
Err(Error::RecoveryFileMalformed { .. })
));
}
#[test]
fn unknown_version_is_rejected() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("version test");
let mut file = export(&master, &phrase, &fast_params()).expect("导出");
file[offset::VERSION] = 0x7F;
match import(&file, &phrase) {
Err(Error::UnsupportedFormatVersion { version }) => assert_eq!(version, 0x7F),
other => panic!("应报版本不支持,实际 is_ok={}", other.is_ok()),
}
}
#[test]
fn unknown_algorithm_is_rejected() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("algorithm test");
let mut file = export(&master, &phrase, &fast_params()).expect("导出");
file[offset::ALGORITHM] = 0x7F;
assert!(import(&file, &phrase).is_err());
}
#[test]
fn absurd_memory_cost_is_rejected_without_allocating() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("memory bomb");
let file = export(&master, &phrase, &fast_params()).expect("导出");
for memory in [0_u32, 1, Params::MIN_M_COST - 1] {
assert!(
matches!(
import_with_memory(&file, &phrase, memory),
Err(Error::RecoveryParametersInvalid { .. })
),
"内存参数 {memory} 应报「参数非法」"
);
}
for memory in [MAX_MEMORY_KIB + 1, u32::MAX, u32::MAX / 2] {
assert!(
matches!(
import_with_memory(&file, &phrase, memory),
Err(Error::RecoveryParametersInvalid { .. })
),
"内存参数 {memory} 应报「参数非法」,而不是真的去分配"
);
}
assert!(
KdfParams::new(MAX_MEMORY_KIB, 1, 1).is_ok(),
"上限值本身应当合法"
);
assert!(
KdfParams::new(MAX_MEMORY_KIB + 1, 1, 1).is_err(),
"超过上限应被拒绝"
);
}
fn import_with_memory(file: &[u8], phrase: &SecretString, memory: u32) -> Result<MasterKey> {
let mut tampered = file.to_vec();
tampered[offset::MEMORY..offset::MEMORY + 4].copy_from_slice(&memory.to_be_bytes());
import(&tampered, phrase)
}
#[test]
fn absurd_iteration_count_is_rejected_without_running_forever() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("iteration bomb");
let file = export(&master, &phrase, &fast_params()).expect("导出");
for iterations in [MAX_ITERATIONS + 1, u32::MAX, u32::MAX / 2] {
let mut tampered = file.clone();
tampered[offset::ITERATIONS..offset::ITERATIONS + 4]
.copy_from_slice(&iterations.to_be_bytes());
assert!(
matches!(
import(&tampered, &phrase),
Err(Error::RecoveryParametersInvalid { .. })
),
"迭代次数 {iterations} 应报「参数非法」"
);
}
assert!(
KdfParams::new(1024, MAX_ITERATIONS, 1).is_ok(),
"上限值本身应当合法"
);
assert!(KdfParams::new(1024, MAX_ITERATIONS + 1, 1).is_err());
}
#[test]
fn zero_iterations_is_rejected() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("iterations test");
let mut file = export(&master, &phrase, &fast_params()).expect("导出");
file[offset::ITERATIONS..offset::ITERATIONS + 4].copy_from_slice(&0_u32.to_be_bytes());
assert!(matches!(
import(&file, &phrase),
Err(Error::RecoveryParametersInvalid { .. })
));
}
#[test]
fn parallelism_other_than_one_is_rejected() {
assert!(KdfParams::new(1024, 1, 2).is_err(), "并行度 2 应被拒绝");
assert!(KdfParams::new(1024, 1, 0).is_err(), "并行度 0 应被拒绝");
assert!(KdfParams::new(1024, 1, 1).is_ok());
}
#[test]
fn rfc9106_argon2id_test_vector() {
let params = ParamsBuilder::new()
.m_cost(32)
.t_cost(3)
.p_cost(4)
.output_len(32)
.data(AssociatedData::new(&[0x04_u8; 12]).expect("12 ≤ MAX_DATA_LEN"))
.build()
.expect("RFC 向量参数合法");
let secret = [0x03_u8; 8];
let context = Argon2::new_with_secret(&secret, Algorithm::Argon2id, Version::V0x13, params)
.expect("构造成功");
let mut out = [0_u8; 32];
context
.hash_password_into(&[0x01_u8; 32], &[0x02_u8; 16], &mut out)
.expect("计算成功");
let expected: [u8; 32] = [
0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, 0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b,
0x53, 0xc9, 0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e, 0xb5, 0x25, 0x20, 0xe9,
0x6b, 0x01, 0xe6, 0x59,
];
assert_eq!(out, expected, "Argon2id 输出与 RFC 9106 §5.3 的向量不符");
}
#[test]
fn derived_key_is_deterministic_and_pinned() {
let phrase = passphrase("deterministic");
let salt = [0x11_u8; SALT_LEN];
let params = fast_params();
let a = derive_key(&phrase, &salt, ¶ms).expect("派生成功");
let b = derive_key(&phrase, &salt, ¶ms).expect("派生成功");
assert_eq!(a, b, "同一个口令与 salt 必须派生出同一个密钥");
let mut other_salt = salt;
other_salt[0] ^= 0x01;
assert_ne!(
derive_key(&phrase, &other_salt, ¶ms).expect("派生成功"),
a,
"salt 改变却派生出同一个密钥"
);
let other_params = KdfParams::new(Params::MIN_M_COST * 2, 1, 1).expect("合法");
assert_ne!(
derive_key(&phrase, &salt, &other_params).expect("派生成功"),
a,
"内存参数改变却派生出同一个密钥"
);
assert_eq!(a.len(), DERIVED_KEY_LEN);
}
#[test]
fn output_length_is_explicitly_requested() {
let phrase = passphrase("output length");
let salt = [0x22_u8; SALT_LEN];
let mut key = derive_key(&phrase, &salt, &fast_params()).expect("派生成功");
let params = Params::new(Params::MIN_M_COST, 1, 1, Some(DERIVED_KEY_LEN)).expect("合法");
let mut expected = [0_u8; DERIVED_KEY_LEN];
Argon2::new(Algorithm::Argon2id, Version::V0x13, params)
.hash_password_into(phrase.expose_secret().as_bytes(), &salt, &mut expected)
.expect("计算成功");
assert_eq!(key, expected);
key.zeroize();
}
#[test]
fn same_passphrase_yields_different_files() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("same passphrase");
let a = export(&master, &phrase, &fast_params()).expect("导出");
let b = export(&master, &phrase, &fast_params()).expect("导出");
assert_ne!(a, b, "salt 或 nonce 被复用");
assert_eq!(
import(&a, &phrase).expect("a 可导入").expose(),
import(&b, &phrase).expect("b 可导入").expose()
);
}
#[test]
fn file_never_contains_the_master_key_in_clear() {
let master = MasterKey::generate().expect("随机源可用");
let file = export(&master, &passphrase("cleartext test"), &fast_params()).expect("导出");
assert!(
!file.windows(MASTER_KEY_LEN).any(|w| w == master.expose()),
"恢复文件里出现了主密钥明文"
);
}
#[test]
fn default_params_are_expensive() {
let params = KdfParams::recommended();
assert_eq!(params.memory_kib(), 64 * 1024, "默认内存不是 64 MiB");
assert!(params.iterations() >= 3, "默认迭代次数过低");
assert_eq!(params.parallelism(), 1);
assert!(
params.memory_kib() >= Params::DEFAULT_M_COST,
"默认内存低于 Argon2 自己的默认值"
);
}
#[test]
fn parameters_travel_with_the_file() {
let master = MasterKey::generate().expect("随机源可用");
let phrase = passphrase("params travel");
let params = KdfParams::new(Params::MIN_M_COST * 4, 2, 1).expect("合法参数");
let file = export(&master, &phrase, ¶ms).expect("导出");
assert_eq!(
u32::from_be_bytes(file[offset::MEMORY..offset::MEMORY + 4].try_into().unwrap()),
params.memory_kib()
);
assert_eq!(
u32::from_be_bytes(
file[offset::ITERATIONS..offset::ITERATIONS + 4]
.try_into()
.unwrap()
),
params.iterations()
);
assert_eq!(
import(&file, &phrase).expect("导入").expose(),
master.expose()
);
}
#[test]
fn master_key_storage_roundtrip() {
let master = MasterKey::generate().expect("随机源可用");
let bytes = master_key_for_storage(&master);
let rebuilt = crate::pairing::master_key_from_storage(bytes).expect("长度正确");
assert_eq!(master.expose(), rebuilt.expose());
}
#[test]
fn file_layout_is_stable() {
assert_eq!(FILE_LEN, 110, "恢复文件长度变了,已导出的文件会失效");
assert_eq!(offset::MAGIC, 0);
assert_eq!(offset::VERSION, 8);
assert_eq!(offset::ALGORITHM, 9);
assert_eq!(offset::MEMORY, 10);
assert_eq!(offset::ITERATIONS, 14);
assert_eq!(offset::PARALLELISM, 18);
assert_eq!(offset::SALT, 22);
assert_eq!(offset::NONCE, 38);
assert_eq!(offset::CIPHERTEXT, 62);
assert_eq!(offset::AAD, 8);
assert_eq!(offset::AAD_END, 62);
}
}