Skip to main content

compact_argon2/
lib.rs

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;
16/// [`argon2::RECOMMENDED_SALT_LEN`]
17const SALT_LEN: usize = 16;
18/// [`argon2::Params::DEFAULT_OUTPUT_LEN`]
19const OUTPUT_LEN: usize = 32;
20
21/// Serialized size of the [`Hash`] struct.
22pub const OUTPUT_SIZE: usize = size_of::<SerializedHash>();
23/// Size of the [`Hash`] struct when encoded as base64.
24#[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/// Parameters for the argon2 hash function. Subset of [`argon2::Params`].
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub struct Params {
42    /// Equivalent to `t_cost`
43    iterations: NonZeroU32,
44    /// Equivalent to `p_cost`
45    parallelism: NonZeroU32,
46    /// Equivalent to `m_cost`
47    memory: NonZeroU32,
48}
49
50impl Params {
51    /// Iterations (`t_cost`)
52    pub fn iterations(&self) -> u32 { self.iterations.get() }
53
54    /// Parallelism (`p_cost`)
55    pub fn parallelism(&self) -> u32 { self.parallelism.get() }
56
57    /// Memory (`m_cost`)
58    pub fn memory(&self) -> u32 { self.memory.get() }
59
60    /// Minimum amount of memory required to construct a hash with these
61    /// parameters, in KiB. Intended to be used with the
62    /// [`hash_with_memory`] and [`verify_with_memory`] functions.
63    ///
64    /// ## Example usage
65    ///
66    /// ```rs
67    /// let mut blocks = vec![Block::default(); DEFAULT_PARAMS.block_count()];
68    /// let hash = compact_argon2::hash_with_memory(b"hunter2", blocks.as_mut_slice()).unwrap();
69    /// assert!(compact_argon2::verify_with_memory(b"hunter2", &hash, blocks.as_mut_slice()).unwrap());
70    /// ```
71    pub fn block_count(&self) -> usize { argon2::Params::from(*self).block_count() }
72
73    // private impl as not all features of argon2::Params are supported
74    // in this crate's Params such as AD.
75    #[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/// An `argon2id` hash.
102#[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        // unwraps should be optimized out
127        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    /// Get the parameters used to construct this hash.
142    pub fn params(&self) -> &Params { &self.params }
143
144    /// Get the version of argon2 used
145    pub fn version(&self) -> Version { self.version }
146
147    /// Verify a password against this hash. See [`crate::verify`] for more
148    /// information.
149    pub fn verify(&self, password: &[u8]) -> Result<bool, argon2::Error> { verify(password, self) }
150
151    /// Serialize this hash into an existing slice.
152    ///
153    /// ## Panics
154    ///
155    /// Panics if `slice` isn't of length [`OUTPUT_SIZE`].
156    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    /// Serialize this hash into an array.
163    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    ///
170    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
233/// Hash a byte array with default argon2id params.
234pub 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
239/// Hash a byte array with default argon2id params, and a specified allocation.
240/// See [`argon2::Argon2::hash_password_into_with_memory`] for implementation.
241pub 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
260/// Verify the given password against a [`Hash`] with the included argon2id
261/// params.
262pub 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
267/// Verify the given password against a [`Hash`] with the included argon2id
268/// params, and a specified allocation. See
269/// [`argon2::Argon2::hash_password_into_with_memory`] for implementation.
270pub 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}