citadel_crypt/argon/
argon_container.rs

1//! # Argon2 Password Hashing Container
2//!
3//! This module provides a secure, asynchronous wrapper around the Argon2 password hashing
4//! algorithm. It implements both client-side and server-side password handling with
5//! configurable parameters and secure memory management.
6//!
7//! ## Features
8//! - Asynchronous password hashing and verification
9//! - Configurable Argon2id parameters (memory, time, parallelism)
10//! - Secure memory handling with SecBuffer
11//! - Automatic salt generation
12//! - Support for associated data and secret keys
13//! - Client and server container types
14//!
15//! ## Usage Example
16//! ```rust
17//! use citadel_crypt::argon::argon_container::{ServerArgonContainer, ArgonSettings, ClientArgonContainer, AsyncArgon};
18//! use citadel_types::crypto::SecBuffer;
19//!
20//! async fn hash_password() {
21//!     // Create settings with default parameters
22//!     let settings = ArgonSettings::new_defaults(vec![1, 2, 3]); // Associated data
23//!     
24//!     // Create client container
25//!     let client = ClientArgonContainer::from(settings);
26//!     
27//!     // Hash a password
28//!     let password = SecBuffer::from("my_secure_password");
29//!     let hashed = client.hash_insecure_input(password).await.unwrap();
30//!     
31//!     // Verify a password (server-side)
32//!     let server_container = ServerArgonContainer::new(
33//!         client.settings.clone(),
34//!         hashed
35//!     );
36//!     
37//!     let verify_result = AsyncArgon::verify(
38//!         SecBuffer::from("my_secure_password"),
39//!         server_container
40//!     ).await.unwrap();
41//! }
42//! ```
43//!
44//! ## Important Notes
45//! - Uses Argon2id variant for optimal security
46//! - Memory-hard algorithm with configurable cost parameters
47//! - Handles password hashing on blocking threads
48//! - Provides secure memory zeroing through SecBuffer
49//! - Supports custom associated data for domain separation
50//!
51//! ## Related Components
52//! - [`SecBuffer`](citadel_types::crypto::SecBuffer): Secure memory management
53//! - [`AsyncArgon`]: Asynchronous hashing interface
54//! - [`ArgonSettings`]: Configuration parameters
55//! - Argon2 password hashing algorithm
56
57use argon2::Config;
58use citadel_io::tokio;
59use citadel_types::crypto::SecBuffer;
60use futures::Future;
61use rand::rngs::ThreadRng;
62use rand::Rng;
63use serde::{Deserialize, Serialize};
64use std::ops::Deref;
65use std::pin::Pin;
66use std::sync::Arc;
67use std::task::{Context, Poll};
68use tokio::task::{JoinError, JoinHandle};
69
70const ARGON_SALT_LENGTH: usize = 16;
71
72// A wrapper that allows asynchronous hashing and verification
73pub struct AsyncArgon {
74    /// for access to the handle as required
75    pub task: JoinHandle<ArgonStatus>,
76}
77
78impl AsyncArgon {
79    pub fn hash(password: SecBuffer, settings: ArgonSettings) -> Self {
80        let task = tokio::task::spawn_blocking(move || {
81            match argon2::hash_raw(
82                password.as_ref(),
83                settings.inner.salt.as_slice(),
84                &settings.as_argon_config(),
85            ) {
86                Ok(hashed) => ArgonStatus::HashSuccess(SecBuffer::from(hashed)),
87                Err(err) => ArgonStatus::HashFailed(err.to_string()),
88            }
89        });
90
91        Self { task }
92    }
93
94    pub fn verify(proposed_password: SecBuffer, settings: ServerArgonContainer) -> Self {
95        let task = tokio::task::spawn_blocking(move || {
96            match argon2::verify_raw(
97                proposed_password.as_ref(),
98                settings.settings.inner.salt.as_slice(),
99                settings.hashed_password.as_ref(),
100                &settings.settings.as_argon_config(),
101            ) {
102                Ok(true) => ArgonStatus::VerificationSuccess,
103
104                Ok(false) => ArgonStatus::VerificationFailed(None),
105
106                Err(err) => ArgonStatus::VerificationFailed(Some(err.to_string())),
107            }
108        });
109
110        Self { task }
111    }
112}
113
114#[derive(Clone, Serialize, Deserialize, Debug)]
115pub struct ArgonSettings {
116    inner: Arc<ArgonSettingsInner>,
117}
118
119impl ArgonSettings {
120    pub fn new(
121        ad: Vec<u8>,
122        salt: Vec<u8>,
123        lanes: u32,
124        hash_length: u32,
125        mem_cost: u32,
126        time_cost: u32,
127        secret: Vec<u8>,
128    ) -> Self {
129        Self {
130            inner: Arc::new(ArgonSettingsInner {
131                ad,
132                salt,
133                lanes,
134                hash_length,
135                mem_cost,
136                time_cost,
137                secret,
138            }),
139        }
140    }
141
142    /// Creates a new instance with default values and no secret
143    pub fn new_defaults(ad: Vec<u8>) -> Self {
144        Self::new_defaults_with_static_secret(ad, vec![])
145    }
146
147    /// Creates a new instance with default values and custom secret
148    pub fn new_defaults_with_static_secret(ad: Vec<u8>, secret: Vec<u8>) -> Self {
149        Self::new_gen_salt(
150            ad,
151            DEFAULT_LANES,
152            DEFAULT_HASH_LENGTH,
153            DEFAULT_MEM_COST,
154            DEFAULT_TIME_COST,
155            secret,
156        )
157    }
158
159    /// Takes the internal config, then
160    pub fn derive_new_with_custom_ad(&self, ad: Vec<u8>) -> Self {
161        Self::new_gen_salt(
162            ad,
163            self.lanes,
164            self.hash_length,
165            self.mem_cost,
166            self.time_cost,
167            self.secret.clone(),
168        )
169    }
170
171    pub fn new_gen_salt(
172        ad: Vec<u8>,
173        lanes: u32,
174        hash_length: u32,
175        mem_cost: u32,
176        time_cost: u32,
177        secret: Vec<u8>,
178    ) -> Self {
179        Self::new(
180            ad,
181            Self::generate_salt().to_vec(),
182            lanes,
183            hash_length,
184            mem_cost,
185            time_cost,
186            secret,
187        )
188    }
189
190    fn generate_salt() -> [u8; ARGON_SALT_LENGTH] {
191        let mut rng = ThreadRng::default();
192        let mut salt: [u8; ARGON_SALT_LENGTH] = Default::default();
193        rng.fill(&mut salt);
194        salt
195    }
196}
197
198impl Deref for ArgonSettings {
199    type Target = ArgonSettingsInner;
200
201    fn deref(&self) -> &Self::Target {
202        self.inner.as_ref()
203    }
204}
205
206#[derive(Serialize, Deserialize, Debug)]
207pub struct ArgonSettingsInner {
208    pub ad: Vec<u8>,
209    pub salt: Vec<u8>,
210    pub lanes: u32,
211    pub hash_length: u32,
212    pub mem_cost: u32,
213    pub time_cost: u32,
214    pub secret: Vec<u8>,
215}
216
217impl ArgonSettings {
218    /// Converts to an acceptable struct for argon2
219    pub fn as_argon_config(&self) -> Config {
220        Config {
221            ad: self.inner.ad.as_slice(),
222            hash_length: self.inner.hash_length,
223            lanes: self.inner.lanes,
224            mem_cost: self.inner.mem_cost,
225            secret: self.inner.secret.as_slice(),
226            time_cost: self.inner.time_cost,
227            variant: argon2::Variant::Argon2id,
228            version: argon2::Version::Version13,
229        }
230    }
231}
232
233impl Default for ArgonSettings {
234    fn default() -> Self {
235        ArgonSettings::new_defaults(vec![])
236    }
237}
238
239#[derive(Clone, Serialize, Deserialize, Default)]
240pub struct ClientArgonContainer {
241    pub settings: ArgonSettings,
242}
243
244impl From<ArgonSettings> for ClientArgonContainer {
245    fn from(settings: ArgonSettings) -> Self {
246        Self { settings }
247    }
248}
249
250impl ClientArgonContainer {
251    pub async fn hash_insecure_input(&self, input: SecBuffer) -> Option<SecBuffer> {
252        match AsyncArgon::hash(input, self.settings.clone()).await.ok()? {
253            ArgonStatus::HashSuccess(ret) => Some(ret),
254            _ => None,
255        }
256    }
257}
258
259#[derive(Clone, Serialize, Deserialize)]
260pub struct ServerArgonContainer {
261    settings: ArgonSettings,
262    hashed_password: SecBuffer,
263}
264
265impl ServerArgonContainer {
266    pub fn new(settings: ArgonSettings, hashed_password: SecBuffer) -> Self {
267        Self {
268            settings,
269            hashed_password,
270        }
271    }
272}
273
274#[derive(Debug)]
275pub enum ArgonStatus {
276    HashSuccess(SecBuffer),
277    HashFailed(String),
278    VerificationSuccess,
279    VerificationFailed(Option<String>),
280}
281
282#[derive(Clone, Serialize, Deserialize)]
283#[allow(variant_size_differences)]
284pub enum ArgonContainerType {
285    Client(ClientArgonContainer),
286    Server(ServerArgonContainer),
287}
288
289impl ArgonContainerType {
290    pub fn client(&self) -> Option<&ClientArgonContainer> {
291        match self {
292            Self::Client(cl) => Some(cl),
293            _ => None,
294        }
295    }
296
297    pub fn server(&self) -> Option<&ServerArgonContainer> {
298        match self {
299            Self::Server(sv) => Some(sv),
300            _ => None,
301        }
302    }
303
304    pub fn settings(&self) -> &ArgonSettings {
305        match self {
306            Self::Client(cl) => &cl.settings,
307            Self::Server(sv) => &sv.settings,
308        }
309    }
310}
311
312impl Future for AsyncArgon {
313    type Output = Result<ArgonStatus, JoinError>;
314
315    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
316        Pin::new(&mut self.task).poll(cx)
317    }
318}
319
320const DEFAULT_LANES: u32 = 8;
321pub const DEFAULT_HASH_LENGTH: u32 = 32;
322#[cfg(not(debug_assertions))]
323const DEFAULT_MEM_COST: u32 = 1024 * 64;
324#[cfg(debug_assertions)]
325const DEFAULT_MEM_COST: u32 = 1024;
326#[cfg(not(debug_assertions))]
327const DEFAULT_TIME_COST: u32 = 10;
328#[cfg(debug_assertions)]
329const DEFAULT_TIME_COST: u32 = 1;
330
331#[derive(Debug, Clone)]
332pub struct ArgonDefaultServerSettings {
333    pub lanes: u32,
334    pub hash_length: u32,
335    pub mem_cost: u32,
336    pub time_cost: u32,
337    pub secret: Vec<u8>,
338}
339
340impl From<ArgonDefaultServerSettings> for ArgonSettings {
341    fn from(settings: ArgonDefaultServerSettings) -> Self {
342        // AD gets created when deriving a new settings container for the specific user during registration
343        Self::new_gen_salt(
344            vec![],
345            settings.lanes,
346            settings.hash_length,
347            settings.mem_cost,
348            settings.time_cost,
349            settings.secret,
350        )
351    }
352}
353
354impl Default for ArgonDefaultServerSettings {
355    fn default() -> Self {
356        Self {
357            lanes: DEFAULT_LANES,
358            hash_length: DEFAULT_HASH_LENGTH,
359            mem_cost: DEFAULT_MEM_COST,
360            time_cost: DEFAULT_TIME_COST,
361            secret: vec![],
362        }
363    }
364}