psh/
lib.rs

1#![doc = include_str!("../README_crate.md")]
2
3#![no_std]
4
5extern crate alloc;
6use alloc::boxed::Box;
7use alloc::collections::BTreeMap;
8use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10use core::fmt;
11use core::ops::{Deref, DerefMut};
12
13use anyhow::{bail, Result};
14use argon2::{Algorithm, Argon2, Params, ParamsBuilder, Version};
15use bitvec::prelude::*;
16use zeroize::{ZeroizeOnDrop, Zeroizing};
17
18mod alias_data;
19use alias_data::AliasData;
20
21mod error;
22use error::Error;
23
24pub mod store;
25pub use store::PshStore;
26
27/// Maximum length for alias in bytes
28pub const ALIAS_MAX_BYTES: usize = 79;
29/// Minimum length for master password in characters
30pub const MASTER_PASSWORD_MIN_LEN: usize = 8;
31
32const PASSWORD_LEN: usize = 16;
33const COLLECTED_BYTES_LEN: usize = 64;
34const MASTER_PASSWORD_MEM_COST: u32 = 64 * 1024;
35const MASTER_PASSWORD_TIME_COST: u32 = 10;
36
37const SYMBOLS: [char; 104] = [
38    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // Skip this line for Standard set
39    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
40    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
41    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U' ,'V', 'W', 'X', 'Y', 'Z',
42    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
43    'n', 'o', 'p', 'q', 'r', 's', 't', 'u' ,'v', 'w', 'x', 'y', 'z',
44    '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':',
45    ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~',
46];
47
48fn hash_master_password(master_password: &ZeroizingString) -> Result<ZeroizingVec> {
49    if master_password.chars().count() < MASTER_PASSWORD_MIN_LEN {
50        bail!(Error::MasterPasswordTooShort);
51    }
52    let mut argon2_params = ParamsBuilder::new();
53    if cfg!(debug_assertions) {
54        argon2_params.m_cost(MASTER_PASSWORD_MEM_COST / 64)
55            .expect("Error setting Argon2 memory cost");
56        argon2_params.t_cost(MASTER_PASSWORD_TIME_COST / 10)
57            .expect("Error setting Argon2 time cost");
58    } else {
59        argon2_params.m_cost(MASTER_PASSWORD_MEM_COST)
60            .expect("Error setting Argon2 memory cost");
61        argon2_params.t_cost(MASTER_PASSWORD_TIME_COST)
62            .expect("Error setting Argon2 time cost");
63    }
64    let argon2_params = argon2_params.params()
65        .expect("Error getting Argon2 params");
66
67    let salt = [0u8; 16];
68    let mut buf = Zeroizing::new([0u8; Params::DEFAULT_OUTPUT_LEN]);
69    let argon2 = Argon2::new(Algorithm::default(), Version::default(), argon2_params);
70    argon2.hash_password_into(master_password.as_bytes(), &salt, &mut *buf)
71        .expect("Error hashing master password");
72
73    let hashed_mp = buf.to_vec();
74
75    Ok(ZeroizingVec::new(hashed_mp))
76}
77
78/// `psh` interface
79pub struct Psh {
80    master_password: ZeroizingString,
81    hashed_mp: ZeroizingVec,
82    known_aliases: BTreeMap<ZeroizingString, AliasData>,
83    db: Box<dyn PshStore + 'static>,
84}
85
86impl Psh {
87    /// Initializes password generator/manager fetching all known (previously used) aliases from
88    /// `psh` database.
89    pub fn new(master_password: ZeroizingString, db: impl PshStore + 'static) -> Result<Self> {
90        let hashed_mp = hash_master_password(&master_password)?;
91
92        let mut psh = Self {
93            master_password,
94            hashed_mp,
95            known_aliases: BTreeMap::new(),
96            db: Box::new(db),
97        };
98
99        psh.get_aliases()?;
100
101        Ok(psh)
102    }
103
104    /// Derives password.
105    ///
106    /// If `charset` is `None` - uses `Standard` charset.
107    ///
108    /// # Panics
109    ///
110    /// Panics if `alias` is an empty string.\
111    /// Panics if `alias` is longer than ALIAS_MAX_BYTES.\
112    /// Panics if `alias` is known and expects `secret` but `None` or empty string is given.\
113    /// Panics if `alias` is known and wrong `charset` is given.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use psh::{Psh, ZeroizingString, store::PshMemDb};
119    ///
120    /// let psh = Psh::new(
121    ///         ZeroizingString::new("password".to_string()),
122    ///         PshMemDb::new(),
123    ///     ).expect("Error initializing Psh");
124    /// let alias = ZeroizingString::new("alias".to_string());
125    /// let secret = ZeroizingString::new("secret".to_string());
126    /// let password = psh.derive_password(&alias, Some(secret), None);
127    ///
128    /// assert_eq!(password.as_str(), "MBF>VgO/UsR-OeQU");
129    /// ```
130    pub fn derive_password(
131        &self,
132        alias: &ZeroizingString,
133        secret: Option<ZeroizingString>,
134        charset: Option<CharSet>,
135    ) -> ZeroizingString {
136        if alias.is_empty() {
137            panic!("Alias cannot be empty");
138        }
139        if alias.len() > ALIAS_MAX_BYTES {
140            panic!("Alias is too long (more than {} bytes)", ALIAS_MAX_BYTES);
141        }
142
143        let charset = charset.unwrap_or_default();
144        let use_secret: bool;
145        if self.alias_is_known(alias) {
146            let alias_data = self.known_aliases.get(alias).unwrap();
147            use_secret = alias_data.use_secret();
148            if charset != self.get_charset(alias) {
149                panic!("This alias uses different charset: {:?}", self.get_charset(alias));
150            }
151        } else {
152            use_secret = secret.is_some();
153        }
154        if use_secret && (secret.is_none() || secret.as_ref().unwrap().is_empty()) {
155            panic!("Secret must not be empty for this alias");
156        }
157
158        let secret = secret.unwrap_or_else(|| ZeroizingString::new("".to_string()));
159        let mut local_nonce: u64 = 0;
160        loop {
161            let bytes = self.generate_bytes(alias, &secret, local_nonce);
162            if let Ok(password_string) = Self::produce_password(charset, bytes) {
163                break password_string;
164            }
165            local_nonce += 1;
166        }
167    }
168
169    fn master_password(&self) -> &ZeroizingString {
170        &self.master_password
171    }
172
173    fn hashed_mp(&self) -> &ZeroizingVec {
174        &self.hashed_mp
175    }
176
177    fn get_aliases(&mut self) -> Result<()> {
178        if self.db.exists() {
179            for record in self.db.records() {
180                let alias_data = AliasData::new_known(&record, self.hashed_mp())?;
181
182                self.known_aliases
183                    .insert(alias_data.alias().clone(), alias_data);
184            }
185        }
186
187        Ok(())
188    }
189
190    /// Returns a sorted list of previously used aliases (those recorded in `psh` database).
191    pub fn aliases(&self) -> Vec<&ZeroizingString> {
192        self.known_aliases.keys().collect()
193    }
194
195    /// Checks if alias has been previously used (exists in `psh` database).
196    pub fn alias_is_known(&self, alias: &ZeroizingString) -> bool {
197        self.known_aliases.contains_key(alias)
198    }
199
200    /// Checks if alias that has been previously used requires a secret.
201    ///
202    /// # Panics
203    ///
204    /// Panics if `alias` is not present in `psh` database.
205    pub fn alias_uses_secret(&self, alias: &ZeroizingString) -> bool {
206        if let Some(alias_data) = self.known_aliases.get(alias) {
207            alias_data.use_secret()
208        } else {
209            panic!("Unknown alias");
210        }
211    }
212
213    /// Returns a charset for an alias that has been previously used.
214    ///
215    /// # Panics
216    ///
217    /// Panics if `alias` is not present in `psh` database.
218    pub fn get_charset(&self, alias: &ZeroizingString) -> CharSet {
219        if let Some(alias_data) = self.known_aliases.get(alias) {
220            alias_data.charset()
221        } else {
222            panic!("Unknown alias");
223        }
224    }
225
226    /// Saves alias to `psh` database.
227    ///
228    /// If `use_secret` is `None` - does not use secret.\
229    /// If `charset` is `None` - uses `Standard` charset.
230    pub fn append_alias_to_db(
231        &mut self,
232        alias: &ZeroizingString,
233        use_secret: Option<bool>,
234        charset: Option<CharSet>,
235    ) -> Result<()> {
236        if self.alias_is_known(alias) {
237            bail!(Error::DbAliasAppendError(alias.clone()));
238        }
239        let mut alias_data = AliasData::new(
240            alias,
241            use_secret.unwrap_or(false),
242            charset.unwrap_or_default(),
243        );
244        alias_data.encrypt_alias(self.hashed_mp());
245
246        let encrypted_alias = alias_data.encrypted_alias().expect("Alias was not encrypted");
247        self.db.append(&encrypted_alias)?;
248
249        self.known_aliases.insert(alias_data.alias().clone(), alias_data);
250
251        Ok(())
252    }
253
254    /// Removes alias from `psh` database.
255    pub fn remove_alias_from_db(&mut self, alias: &ZeroizingString) -> Result<()> {
256        if self.alias_is_known(alias) {
257            let alias_data = self.known_aliases.get(alias).unwrap();
258            let encrypted_alias = alias_data.encrypted_alias().unwrap().clone();
259
260            self.db.delete(&encrypted_alias)?;
261
262            self.known_aliases.remove(&alias_data.alias().clone());
263        } else {
264            bail!(Error::DbAliasRemoveError(alias.clone()));
265        }
266        Ok(())
267    }
268
269    // Generates COLLECTED_BYTES_LEN bytes using argon2 hashing algorithm
270    // with hashed_mp + alias and secret as inputs.
271    fn generate_bytes(
272        &self,
273        alias: &ZeroizingString,
274        secret: &ZeroizingString,
275        nonce: u64,
276    ) -> ZeroizingVec {
277        let mut argon2_params = ParamsBuilder::new();
278        argon2_params.output_len(COLLECTED_BYTES_LEN)
279            .expect("Error setting Argon2 output length");
280        let argon2_params = argon2_params.params()
281            .expect("Error getting Argon2 params");
282
283        let salt = [0u8; 16];
284        let mut buf = Zeroizing::new([0u8; COLLECTED_BYTES_LEN]);
285        let argon2 = Argon2::new(Algorithm::default(), Version::default(), argon2_params);
286        let input = Zeroizing::new(
287            [
288                alias.as_bytes(),
289                secret.as_bytes(),
290                nonce.to_le_bytes().as_slice(),
291                self.master_password().as_bytes(),
292                self.hashed_mp(),
293            ]
294            .concat()
295        );
296        argon2.hash_password_into(&input, &salt, &mut *buf)
297            .expect("Error hashing with Argon2");
298
299        ZeroizingVec::new(buf.to_vec())
300    }
301
302    // Iterate over 7-bit windows of input bytes gathering symbols
303    // for password using SYMBOLS table.
304    fn produce_password(charset: CharSet, bytes: ZeroizingVec) -> Result<ZeroizingString> {
305        let mut password_chars: Zeroizing<Vec<char>> = Zeroizing::new(Vec::new());
306        let bv = BitSlice::<_, Msb0>::from_slice(&bytes);
307        let mut bv_iter = bv.windows(7);
308        while let Some(bits) = bv_iter.next() {
309            let mut pos: usize = bits.load_be();
310            if pos < charset.len() {
311                // Skip first 10 (duplicate) symbols for Standard set
312                if charset == CharSet::Standard {
313                    pos += 10;
314                }
315                password_chars.push(SYMBOLS[pos]);
316                // Skip 6 bits + 1 with iteration = 7 bits of current `bits`
317                bv_iter.nth(5);
318            } else {
319                // If `pos` >= `charset.len()`
320                // -for Reduced set we know that MSB 0 = '1' and at least one of MSB 1,2,3 = '1'
321                // -for Standard set we know that MSB 0 = '1' and MSB 1 very likely = '1'
322                // -for RequireAll set we know that MSB 0,1 = '1' and at least one of MSB 2,3 = '1'
323                // So skip 3 bits + 1 with iteration, because they are largely predetermined
324                bv_iter.nth(2);
325                continue;
326            }
327            if password_chars.len() == PASSWORD_LEN {
328                match charset {
329                    CharSet::Reduced | CharSet::Standard => break,
330                    CharSet::RequireAll => {
331                        if password_chars.iter().any(|b| b.is_ascii_digit())
332                            && password_chars.iter().any(|b| b.is_ascii_lowercase())
333                            && password_chars.iter().any(|b| b.is_ascii_uppercase())
334                            && password_chars.iter().any(|b| b.is_ascii_punctuation())
335                        {
336                            break;
337                        } else {
338                            // Start over
339                            password_chars.clear();
340                        }
341                    }
342                }
343            }
344        }
345        if password_chars.len() < PASSWORD_LEN {
346            bail!("Not enough input data")
347        }
348        Ok(ZeroizingString::new(password_chars.iter().collect()))
349    }
350}
351
352/// Character set for a derived password
353#[allow(clippy::derive_partial_eq_without_eq)]
354#[derive(Copy, Clone, Debug, Default, PartialEq)]
355pub enum CharSet {
356    /// Standard charset consists of all printable ASCII characters (space excluded). It is a
357    /// default charset.
358    #[default]
359    Standard,
360    /// Reduced charset allows only ASCII alphanumeric (i.e., [a-zA-Z0-9]).
361    Reduced,
362    /// RequireAll is like Standard, but guarantees that derived password has at least one
363    /// symbol from all character types: numbers, lowercase letters, uppercase letters and
364    /// punctuation symbols.
365    RequireAll,
366}
367
368impl CharSet {
369    fn len(&self) -> usize {
370        match self {
371            Self::Standard => 94,
372            Self::Reduced => 72,
373            Self::RequireAll => 104,
374        }
375    }
376}
377
378/// Safe `String` wrapper which employs `zeroize` crate to wipe memory of its content
379#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ZeroizeOnDrop)]
380pub struct ZeroizingString {
381    string: String,
382}
383
384impl ZeroizingString {
385    pub fn new(string: String) -> Self {
386        Self { string }
387    }
388}
389
390impl Deref for ZeroizingString {
391    type Target = String;
392
393    fn deref(&self) -> &Self::Target {
394        &self.string
395    }
396}
397
398impl DerefMut for ZeroizingString {
399    fn deref_mut(&mut self) -> &mut Self::Target {
400        &mut self.string
401    }
402}
403
404impl fmt::Display for ZeroizingString {
405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        write!(f, "{}", self.string)
407    }
408}
409
410#[derive(ZeroizeOnDrop)]
411pub(crate) struct ZeroizingVec {
412    vec: Vec<u8>,
413}
414
415impl ZeroizingVec {
416    fn new(vec: Vec<u8>) -> Self {
417        Self { vec }
418    }
419}
420
421impl Deref for ZeroizingVec {
422    type Target = Vec<u8>;
423
424    fn deref(&self) -> &Self::Target {
425        &self.vec
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432    use test_case::test_case;
433
434    #[test]
435    fn produce_password_fails_with_not_enough_bytes() {
436        let bytes = ZeroizingVec::new([0u8; 13].to_vec()); // not enough bytes to produce password
437        let charset = CharSet::Standard;
438        let result = Psh::produce_password(charset, bytes);
439        assert!(result.is_err());
440    }
441
442    #[test_case([0u8; 14], CharSet::Standard => "0000000000000000"; "zeros produce zeros")]
443    #[test_case([2,4,8,16,32,64,129,2,4,8,16,32,64,129], CharSet::Standard
444        => "1111111111111111"; "boolean 1 every 7 bits gives all 1s")]
445    fn produce_password_with_14_bytes(bytes: [u8; 14], charset: CharSet) -> String {
446        let bytes = ZeroizingVec::new(bytes.to_vec());
447        Psh::produce_password(charset, bytes)
448            .unwrap()
449            .to_string()
450    }
451
452    #[test_case([224,0,65,4,16,65,4,0,0,65,4,16,65,4,0,0], CharSet::Standard
453        => "01248GW#01248GW#"; "1st byte out of symbol table range (Standard set)")]
454    #[test_case([224,0,65,4,16,65,4,0,0,65,4,16,65,4,0,0], CharSet::Reduced
455        => "012486Ms012486Ms"; "1st byte out of symbol table range (Reduced set)")]
456    #[test_case([236,0,65,4,16,65,4,0,0,65,4,16,65,4,0,0], CharSet::RequireAll
457        => "]12486Ms012486Ms"; "1st byte out of symbol table range (RequireAll set)")]
458    #[test_case([204,204,192,0,0,0,0,0,0,0,0,0,0,0,0,0], CharSet::Standard
459        => panics "Not enough"; "not enough input data (Standard set)")]
460    #[test_case([204,204,192,0,0,0,0,0,0,0,0,0,0,0,0,0], CharSet::Reduced
461        => panics "Not enough"; "not enough input data (Reduced set)")]
462    #[test_case([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], CharSet::RequireAll
463        => panics "Not enough"; "not enough input data (RequireAll set)")]
464    fn produce_password(bytes: [u8; 16], charset: CharSet) -> String {
465        let bytes = ZeroizingVec::new(bytes.to_vec());
466        Psh::produce_password(charset, bytes)
467            .unwrap()
468            .to_string()
469    }
470}