1use argon2::{Algorithm, Argon2, Params, Version};
12
13use crate::contract::{Identified, PasswordBasedKdf};
14use crate::error::{Error, Result};
15
16const MIN_SALT_LEN: usize = 8;
18
19const MAX_OUTPUT_LEN: usize = u32::MAX as usize;
21
22#[derive(Debug, Clone)]
24pub struct Argon2Kdf {
25 inner: Argon2<'static>,
26}
27
28impl Argon2Kdf {
29 pub const IDENTIFIER: &'static str = "argon2";
30
31 pub fn new() -> Self {
33 Self {
34 inner: Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::DEFAULT),
35 }
36 }
37
38 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 fn derive_from_password(&self, password: &[u8], salt: &[u8], length: usize) -> Result<Vec<u8>> {
65 if salt.len() < MIN_SALT_LEN {
66 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 .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 #[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 #[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 #[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 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 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}