1#![cfg_attr(test, feature(test))]
2
3#[cfg(feature = "base64")] mod base64;
4#[cfg(feature = "serde")] pub mod serde;
5#[cfg(feature = "postgres")] mod sqlx;
6
7use std::num::{NonZero, NonZeroU32};
8
9use argon2::Algorithm::Argon2id;
10use argon2::password_hash::rand_core::{OsRng, RngCore};
11use argon2::{Argon2, Block, Version};
12use bytemuck::{Pod, Zeroable};
13
14const DEFAULT_PARAMS: argon2::Params = argon2::Params::DEFAULT;
15const DEFAULT_VERSION: Version = Version::V0x13;
16const SALT_LEN: usize = 16;
18const OUTPUT_LEN: usize = 32;
20
21pub const OUTPUT_SIZE: usize = size_of::<SerializedHash>();
23#[cfg(feature = "base64")]
25pub const BASE64_OUTPUT_SIZE: usize = (OUTPUT_SIZE * 4).div_ceil(3);
26
27#[derive(Debug, thiserror::Error)]
28pub enum ParseError {
29 #[error("unrecognized version {0:#02x?}")]
30 InvalidVersion(u8),
31 #[error("memory too low, expected at least {expected}, got {got}")]
32 MemoryTooLow { expected: u32, got: u32 },
33 #[error("parameters out of range")]
34 InvalidParameters,
35 #[error("input is the wrong length, expected exactly {OUTPUT_SIZE} bytes")]
36 SliceLength,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub struct Params {
42 iterations: NonZeroU32,
44 parallelism: NonZeroU32,
46 memory: NonZeroU32,
48}
49
50impl Params {
51 pub fn iterations(&self) -> u32 { self.iterations.get() }
53
54 pub fn parallelism(&self) -> u32 { self.parallelism.get() }
56
57 pub fn memory(&self) -> u32 { self.memory.get() }
59
60 pub fn block_count(&self) -> usize { argon2::Params::from(*self).block_count() }
72
73 #[inline]
76 fn from_argon2(params: &argon2::Params) -> Self {
77 Self {
78 iterations: NonZeroU32::new(params.t_cost()).unwrap(),
79 parallelism: NonZeroU32::new(params.p_cost()).unwrap(),
80 memory: NonZeroU32::new(params.m_cost()).unwrap(),
81 }
82 }
83}
84
85impl Default for Params {
86 fn default() -> Self { Self::from_argon2(&DEFAULT_PARAMS) }
87}
88
89impl From<Params> for argon2::Params {
90 fn from(value: Params) -> Self {
91 Self::new(
92 value.memory(),
93 value.iterations(),
94 value.parallelism(),
95 None,
96 )
97 .unwrap()
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct Hash {
104 params: Params,
105 version: Version,
106 salt: [u8; SALT_LEN],
107 out: [u8; OUTPUT_LEN],
108}
109
110#[derive(Pod, Zeroable, Clone, Copy)]
111#[repr(C, packed)]
112struct SerializedHash {
113 iterations: [u8; 3],
114 parallelism: [u8; 3],
115 memory: [u8; 4],
116 version: u8,
117 salt: [u8; 16],
118 out: [u8; 32],
119}
120
121fn get_u24be(v: [u8; 3]) -> u32 { u32::from_be_bytes([0, v[0], v[1], v[2]]) }
122
123impl Hash {
124 #[inline]
125 fn serialized(&self) -> SerializedHash {
126 SerializedHash {
128 iterations: self.params.iterations().to_be_bytes()[1..]
129 .try_into()
130 .unwrap(),
131 parallelism: self.params.parallelism().to_be_bytes()[1..]
132 .try_into()
133 .unwrap(),
134 memory: self.params.memory().to_be_bytes(),
135 version: self.version as u8,
136 salt: self.salt,
137 out: self.out,
138 }
139 }
140
141 pub fn params(&self) -> &Params { &self.params }
143
144 pub fn version(&self) -> Version { self.version }
146
147 pub fn verify(&self, password: &[u8]) -> Result<bool, argon2::Error> { verify(password, self) }
150
151 pub fn write_to_slice(&self, slice: &mut [u8]) {
157 let serialized = self.serialized();
158
159 slice.copy_from_slice(bytemuck::bytes_of(&serialized));
160 }
161
162 pub fn to_bytes(&self) -> [u8; OUTPUT_SIZE] {
164 let mut slice = [0u8; _];
165 self.write_to_slice(&mut slice);
166 slice
167 }
168
169 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
171 if bytes.len() != OUTPUT_SIZE {
172 return Err(ParseError::SliceLength);
173 }
174
175 let serialized: &SerializedHash = bytemuck::from_bytes(bytes);
176
177 let iterations =
178 NonZero::new(get_u24be(serialized.iterations)).ok_or(ParseError::InvalidParameters)?;
179 let parallelism =
180 NonZero::new(get_u24be(serialized.parallelism)).ok_or(ParseError::InvalidParameters)?;
181 let memory = NonZero::new(u32::from_be_bytes(serialized.memory))
182 .ok_or(ParseError::InvalidParameters)?;
183
184 let required_memory = parallelism.get() * 8;
185 if memory.get() < required_memory {
186 return Err(ParseError::MemoryTooLow {
187 expected: required_memory,
188 got: memory.get(),
189 });
190 }
191
192 let version = Version::try_from(serialized.version as u32)
193 .map_err(|_| ParseError::InvalidVersion(serialized.version))?;
194
195 Ok(Self {
196 params: Params {
197 iterations,
198 parallelism,
199 memory,
200 },
201 version,
202 salt: serialized.salt,
203 out: serialized.out,
204 })
205 }
206}
207
208impl TryFrom<&[u8; 59]> for Hash {
209 type Error = ParseError;
210
211 fn try_from(value: &[u8; 59]) -> Result<Self, Self::Error> { Self::from_bytes(&*value) }
212}
213
214impl TryFrom<&[u8]> for Hash {
215 type Error = ParseError;
216
217 fn try_from(value: &[u8]) -> Result<Self, Self::Error> { Self::from_bytes(value) }
218}
219
220impl From<Hash> for [u8; OUTPUT_SIZE] {
221 fn from(value: Hash) -> Self { value.to_bytes() }
222}
223
224impl std::hash::Hash for Hash {
225 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
226 self.params.hash(state);
227 (self.version as u32).hash(state);
228 self.salt.hash(state);
229 self.out.hash(state);
230 }
231}
232
233pub fn hash(password: &[u8]) -> Result<Hash, argon2::Error> {
235 let mut blocks = vec![Block::default(); DEFAULT_PARAMS.block_count()];
236 hash_with_memory(password, blocks.as_mut_slice())
237}
238
239pub fn hash_with_memory(
242 password: &[u8],
243 memory: impl AsMut<[Block]>,
244) -> Result<Hash, argon2::Error> {
245 let version = DEFAULT_VERSION;
246 let hasher = Argon2::new(Argon2id, version, DEFAULT_PARAMS);
247 let params = Params::from_argon2(hasher.params());
248
249 let mut hash = Hash {
250 params,
251 version,
252 salt: [0; _],
253 out: [0; _],
254 };
255 OsRng.fill_bytes(&mut hash.salt);
256 hasher.hash_password_into_with_memory(password, &hash.salt, &mut hash.out, memory)?;
257 Ok(hash)
258}
259
260pub fn verify(password: &[u8], hash: &Hash) -> Result<bool, argon2::Error> {
263 let mut blocks = vec![Block::default(); DEFAULT_PARAMS.block_count()];
264 verify_with_memory(password, hash, blocks.as_mut_slice())
265}
266
267pub fn verify_with_memory(
271 password: &[u8],
272 hash: &Hash,
273 memory: impl AsMut<[Block]>,
274) -> Result<bool, argon2::Error> {
275 let mut out = [0; _];
276 let hasher = Argon2::new(Argon2id, hash.version, hash.params.into());
277 hasher.hash_password_into_with_memory(password, &hash.salt, &mut out, memory)?;
278 Ok(out.eq(&hash.out))
279}
280
281#[cfg(test)]
282mod tests {
283 extern crate test;
284
285 use std::hint::black_box;
286 use std::sync::LazyLock;
287
288 use argon2::password_hash::{PasswordHashString, SaltString};
289 use argon2::{PasswordHash, PasswordHasher};
290 use test::Bencher;
291
292 use super::*;
293
294 pub(crate) static HASH: LazyLock<Hash> = LazyLock::new(|| hash(b"hunter2").unwrap());
295
296 static PHC_HASH: LazyLock<PasswordHash> = LazyLock::new(|| {
297 static SALT: LazyLock<SaltString> = LazyLock::new(|| SaltString::generate(&mut OsRng));
298 let argon2 = argon2::Argon2::default();
299 argon2.hash_password(b"hunter2", &*SALT).unwrap()
300 });
301
302 #[test]
303 fn it_works() {
304 let password = b"hunter2";
305
306 let hash = hash(password).unwrap();
307
308 assert!(hash.verify(password).unwrap());
309 }
310
311 #[test]
312 fn round_trip() {
313 let bytes = HASH.to_bytes();
314 let reconstructed = Hash::from_bytes(&bytes).unwrap();
315 assert_eq!(*HASH, reconstructed);
316 }
317
318 #[bench]
319 fn to_bytes(bencher: &mut Bencher) {
320 let hash = *HASH;
321
322 bencher.iter(|| black_box(hash).to_bytes());
323 }
324
325 #[bench]
326 fn from_bytes(bencher: &mut Bencher) {
327 let bytes = HASH.to_bytes();
328
329 bencher.iter(|| Hash::from_bytes(black_box(&bytes)));
330 }
331
332 #[bench]
333 fn from_phc_string(bencher: &mut Bencher) {
334 let phc_string = PasswordHashString::from(&*PHC_HASH);
335
336 bencher.iter(|| black_box(&phc_string).password_hash());
337 }
338
339 #[bench]
340 fn to_phc_string(bencher: &mut Bencher) {
341 let phc_hash = &*PHC_HASH;
342
343 bencher.iter(|| PasswordHashString::from(black_box(phc_hash)));
344 }
345}