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
27pub const ALIAS_MAX_BYTES: usize = 79;
29pub 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', '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
78pub 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 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 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 pub fn aliases(&self) -> Vec<&ZeroizingString> {
192 self.known_aliases.keys().collect()
193 }
194
195 pub fn alias_is_known(&self, alias: &ZeroizingString) -> bool {
197 self.known_aliases.contains_key(alias)
198 }
199
200 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 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 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 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 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 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 if charset == CharSet::Standard {
313 pos += 10;
314 }
315 password_chars.push(SYMBOLS[pos]);
316 bv_iter.nth(5);
318 } else {
319 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 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#[allow(clippy::derive_partial_eq_without_eq)]
354#[derive(Copy, Clone, Debug, Default, PartialEq)]
355pub enum CharSet {
356 #[default]
359 Standard,
360 Reduced,
362 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#[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()); 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}