Skip to main content

encryption/kdf/
argon2.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! Argon2id(RFC 9106):从人类可读口令派生密钥。
4//!
5//! 固定 v19 版本;默认 [`Params::DEFAULT`] 即 m=19×1024 KiB(约 19 MiB 内存开销)、
6//! t=2、p=1。需要更高强度或与外部约定对齐时用 [`Argon2Kdf::with_params`] 调参
7//! (如 RFC 9106 §4 建议的 m=64 MiB、t=3、p=4)。
8//!
9//! 走 [`Argon2::hash_password_into`](裸字节输出),不经过 PHC 字符串层。
10
11use argon2::{Algorithm, Argon2, Params, Version};
12
13use crate::contract::{Identified, PasswordBasedKdf};
14use crate::error::{Error, Result};
15
16/// 盐值下限:与 argon2 crate 的 `MIN_SALT_LEN` 一致(RFC 9106 建议 16 字节随机盐)。
17const MIN_SALT_LEN: usize = 8;
18
19/// 输出长度上限(RFC 9106 §3.1:tag 最长 2^32−1 字节)。
20const MAX_OUTPUT_LEN: usize = u32::MAX as usize;
21
22/// Argon2id 派生器(v19,可复用)。
23#[derive(Debug, Clone)]
24pub struct Argon2Kdf {
25    inner: Argon2<'static>,
26}
27
28impl Argon2Kdf {
29    pub const IDENTIFIER: &'static str = "argon2";
30
31    /// 使用 [`Params::DEFAULT`]:m=19456 KiB(约 19 MiB)、t=2、p=1。
32    pub fn new() -> Self {
33        Self {
34            inner: Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::DEFAULT),
35        }
36    }
37
38    /// 自定义参数:`m_cost` 单位为 KiB(内存),`t_cost` 为迭代次数,`p_cost`
39    /// 为并行度。参数非法(如 m < 8×p、t = 0、p = 0)返回
40    /// [`Error::InvalidKdfParams`]。
41    pub fn with_params(m_cost: u32, t_cost: u32, p_cost: u32) -> Result<Self> {
42        let params = Params::new(m_cost, t_cost, p_cost, None)
43            .map_err(|_| Error::InvalidKdfParams { label: "Argon2" })?;
44        Ok(Self {
45            inner: Argon2::new(Algorithm::Argon2id, Version::V0x13, params),
46        })
47    }
48}
49
50impl Default for Argon2Kdf {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl Identified for Argon2Kdf {
57    fn identifier(&self) -> &str {
58        Self::IDENTIFIER
59    }
60}
61
62impl PasswordBasedKdf for Argon2Kdf {
63    /// `salt` 至少 8 字节(与 argon2 crate 的 `MIN_SALT_LEN` 一致,推荐 16 字节)。
64    fn derive_from_password(&self, password: &[u8], salt: &[u8], length: usize) -> Result<Vec<u8>> {
65        if salt.len() < MIN_SALT_LEN {
66            // 选 InvalidKeyLength:既有错误集中最贴近的变体 —— 它表达「某输入的
67            // 长度不满足算法要求」,expected/got 能直接指出盐值过短;Display 现为
68            // 「{label} length must be {expected} (got {got})」,未含「至少」语义,
69            // 与 ≥8 的实际约束略有出入,但仅在实际过短时出现,不会把合法长度误报为非法。
70            return Err(Error::InvalidKeyLength {
71                label: "argon2 salt",
72                expected: MIN_SALT_LEN,
73                got: salt.len(),
74            });
75        }
76
77        if length == 0 || length > MAX_OUTPUT_LEN {
78            return Err(Error::InvalidOutputLength {
79                requested: length,
80                max: MAX_OUTPUT_LEN,
81            });
82        }
83
84        let mut output = vec![0u8; length];
85        self.inner
86            .hash_password_into(password, salt, &mut output)
87            // 前置校验后剩余的长度类失败只有 1..=3(Argon2 的输出下限为 4 字节,
88            // RFC 9106 §3.1);错误变体没有 min 字段,一并归入输出长度非法。
89            .map_err(|_| Error::InvalidOutputLength {
90                requested: length,
91                max: MAX_OUTPUT_LEN,
92            })?;
93        Ok(output)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use argon2::{AssociatedData, ParamsBuilder};
101
102    /// RFC 9106 §5.3 官方测试向量(Argon2id v19):
103    /// m=32 KiB、t=3、p=4、tag 32 字节;P=01×32、S=02×16、K=03×8、X=04×12。
104    ///
105    /// 该向量带 secret(K)与 associated data(X)两个输入,而
106    /// `PasswordBasedKdf::derive_from_password` 契约只有 password / salt 两个
107    /// 槽位(contract.rs 不在本任务改动范围内),故官方向量用与
108    /// [`Argon2Kdf::with_params`] 相同的构造链(ParamsBuilder + new_with_secret)
109    /// 在 crate 层复现,钉住底层算法组合能产出 RFC 给出的完整 tag。
110    #[test]
111    fn matches_rfc9106_test_vector() {
112        let params = ParamsBuilder::new()
113            .m_cost(32)
114            .t_cost(3)
115            .p_cost(4)
116            .output_len(32)
117            .data(AssociatedData::new(&[0x04u8; 12]).unwrap())
118            .build()
119            .unwrap();
120        let secret = [0x03u8; 8];
121        let ctx =
122            Argon2::new_with_secret(&secret, Algorithm::Argon2id, Version::V0x13, params).unwrap();
123
124        let mut tag = [0u8; 32];
125        ctx.hash_password_into(&[0x01u8; 32], &[0x02u8; 16], &mut tag)
126            .unwrap();
127        assert_eq!(
128            crate::internal::hex_encode(&tag),
129            concat!(
130                "0d640df58d78766c08c037a34a8b53c9",
131                "d01ef0452d75b65eb52520e96b01e659"
132            )
133        );
134    }
135
136    /// 封装等价性(对照检查,非官方向量):`with_params` 与裸 crate 同参构造
137    /// 输出一致 —— 钉住 m/t/p 被原样透传。
138    #[test]
139    fn with_params_matches_reference_context() {
140        let kdf = Argon2Kdf::with_params(32, 3, 4).unwrap();
141        let reference = Argon2::new(
142            Algorithm::Argon2id,
143            Version::V0x13,
144            Params::new(32, 3, 4, None).unwrap(),
145        );
146
147        let mut expected = [0u8; 32];
148        reference
149            .hash_password_into(b"password", b"0123456789abcdef", &mut expected)
150            .unwrap();
151        assert_eq!(
152            kdf.derive_from_password(b"password", b"0123456789abcdef", 32)
153                .unwrap(),
154            expected
155        );
156    }
157
158    /// 默认构造必须是 Argon2id + v19 + [`Params::DEFAULT`],而不是
159    /// `Argon2::default()`(那会是 Argon2d 变体,安全性不同)。
160    #[test]
161    fn default_matches_reference_context() {
162        let reference = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::DEFAULT);
163        let mut expected = [0u8; 32];
164        reference
165            .hash_password_into(b"pw", b"12345678", &mut expected)
166            .unwrap();
167
168        assert_eq!(
169            Argon2Kdf::default()
170                .derive_from_password(b"pw", b"12345678", 32)
171                .unwrap(),
172            expected
173        );
174    }
175
176    #[test]
177    fn same_inputs_same_output_and_length_validation() {
178        let kdf = Argon2Kdf::with_params(32, 3, 4).unwrap();
179        let first = kdf.derive_from_password(b"pw", b"salt1234", 16).unwrap();
180        let second = kdf.derive_from_password(b"pw", b"salt1234", 16).unwrap();
181        assert_eq!(first, second);
182        assert_eq!(first.len(), 16);
183
184        // Argon2 的 tag 长度参与初始哈希,32 字节输出不以 16 字节输出为前缀。
185        let long = kdf.derive_from_password(b"pw", b"salt1234", 32).unwrap();
186        assert_eq!(long.len(), 32);
187        assert_ne!(first, long[..16]);
188
189        assert_eq!(
190            kdf.derive_from_password(b"pw", b"salt1234", 0).unwrap_err(),
191            Error::InvalidOutputLength {
192                requested: 0,
193                max: MAX_OUTPUT_LEN
194            }
195        );
196    }
197
198    #[test]
199    fn password_and_salt_both_affect_output() {
200        let kdf = Argon2Kdf::with_params(32, 3, 4).unwrap();
201        let base = kdf.derive_from_password(b"pw", b"salt1234", 32).unwrap();
202        assert_ne!(
203            base,
204            kdf.derive_from_password(b"pw2", b"salt1234", 32).unwrap()
205        );
206        assert_ne!(
207            base,
208            kdf.derive_from_password(b"pw", b"salt5678", 32).unwrap()
209        );
210    }
211
212    #[test]
213    fn short_salt_is_rejected() {
214        let kdf = Argon2Kdf::new();
215        assert_eq!(
216            kdf.derive_from_password(b"pw", b"1234567", 32).unwrap_err(),
217            Error::InvalidKeyLength {
218                label: "argon2 salt",
219                expected: 8,
220                got: 7
221            }
222        );
223        // 8 字节即下限,可通过(默认参数也顺带跑通一次)。
224        assert_eq!(
225            kdf.derive_from_password(b"pw", b"12345678", 32)
226                .unwrap()
227                .len(),
228            32
229        );
230    }
231
232    #[test]
233    fn invalid_params_are_rejected() {
234        assert_eq!(
235            Argon2Kdf::with_params(4, 3, 4).unwrap_err(),
236            Error::InvalidKdfParams { label: "Argon2" }
237        );
238        assert_eq!(
239            Argon2Kdf::with_params(32, 0, 4).unwrap_err(),
240            Error::InvalidKdfParams { label: "Argon2" }
241        );
242        assert_eq!(
243            Argon2Kdf::with_params(32, 3, 0).unwrap_err(),
244            Error::InvalidKdfParams { label: "Argon2" }
245        );
246    }
247}