From 96660a90e4cd296a2922d7a547de4cd9d0b1928b Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 1 Sep 2023 00:00:00 +0000 Subject: feat(login) password login using encrypted nsec Enables the user to only handle the nsec upon first use of the tool by encrypting it with a password and storing it on disk in an application cache. The approach to encryption draws heavily from that used by the gossip nostr client. - unencrypted nsec is zeroed from memory - a salt is used to defend against rainbow tables - computationally expensive key stretching defends against brute-force attacks of passwords with low entropy. There is UX trade-off between decryption speed and key-stretching computation. This UX challenge is exacerbated in a cli tool as decryption must take place more regularly. Thought was put into the selected n_log and a heavily reduced value is provided for long passwords where security benefits are smaller. A more granular reducing in computation was also considered by rejected to avoided to revealing just how weak a password is as most weak passwords are reused. --- src/config.rs | 111 ++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 65 insertions(+), 46 deletions(-) (limited to 'src/config.rs') diff --git a/src/config.rs b/src/config.rs index b26dea0..f410934 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, Context, Result}; use directories::ProjectDirs; #[cfg(test)] use mockall::*; +use nostr::secp256k1::XOnlyPublicKey; use serde::{self, Deserialize, Serialize}; #[derive(Default)] @@ -59,7 +60,7 @@ impl ConfigManagement for ConfigManager { } } -#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)] #[allow(clippy::module_name_repetitions)] pub struct MyConfig { pub version: u8, @@ -68,44 +69,64 @@ pub struct MyConfig { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct UserRef { - pub nsec: String, + pub public_key: XOnlyPublicKey, + pub encrypted_key: String, } #[cfg(test)] mod tests { use anyhow::Result; use serial_test::serial; - use test_utils::*; use super::*; + fn backup_existing_config() -> Result<()> { + let config_path = get_dirs()?.config_dir().join("config.json"); + let backup_config_path = get_dirs()?.config_dir().join("config-backup.json"); + if config_path.exists() { + std::fs::rename(config_path, backup_config_path)?; + } + Ok(()) + } + + fn restore_config_backup() -> Result<()> { + let config_path = get_dirs()?.config_dir().join("config.json"); + let backup_config_path = get_dirs()?.config_dir().join("config-backup.json"); + if config_path.exists() { + std::fs::remove_file(&config_path)?; + } + if backup_config_path.exists() { + std::fs::rename(backup_config_path, config_path)?; + } + Ok(()) + } + mod load { use super::*; #[test] #[serial] fn when_config_file_doesnt_exist_defaults_are_returned() -> Result<()> { - with_fresh_config(|| { - assert_eq!(ConfigManager.load()?, MyConfig::default()); - - Ok(()) - }) + backup_existing_config()?; + let c = ConfigManager; + assert_eq!(c.load()?, MyConfig::default()); + restore_config_backup()?; + Ok(()) } #[test] #[serial] fn when_config_file_exists_it_is_returned() -> Result<()> { - with_fresh_config(|| { - let c = ConfigManager; - let new_config = MyConfig { - version: 255, - ..MyConfig::default() - }; - c.save(&new_config)?; - assert_eq!(c.load()?, new_config); - - Ok(()) - }) + backup_existing_config()?; + let c = ConfigManager; + let new_config = MyConfig { + version: 255, + ..MyConfig::default() + }; + c.save(&new_config)?; + assert_eq!(c.load()?, new_config); + restore_config_backup()?; + Ok(()) } } @@ -115,38 +136,36 @@ mod tests { #[test] #[serial] fn when_config_file_doesnt_config_is_saved() -> Result<()> { - with_fresh_config(|| { - let c = ConfigManager; - let new_config = MyConfig { - version: 255, - ..MyConfig::default() - }; - c.save(&new_config)?; - assert_eq!(c.load()?, new_config); - - Ok(()) - }) + backup_existing_config()?; + let c = ConfigManager; + let new_config = MyConfig { + version: 255, + ..MyConfig::default() + }; + c.save(&new_config)?; + assert_eq!(c.load().unwrap(), new_config); + restore_config_backup()?; + Ok(()) } #[test] #[serial] fn when_config_file_exists_new_config_is_saved() -> Result<()> { - with_fresh_config(|| { - let c = ConfigManager; - let config = MyConfig { - version: 255, - ..MyConfig::default() - }; - c.save(&config)?; - let new_config = MyConfig { - version: 254, - ..MyConfig::default() - }; - c.save(&new_config)?; - assert_eq!(c.load()?, new_config); - - Ok(()) - }) + backup_existing_config()?; + let c = ConfigManager; + let config = MyConfig { + version: 255, + ..MyConfig::default() + }; + c.save(&config)?; + let new_config = MyConfig { + version: 254, + ..MyConfig::default() + }; + c.save(&new_config)?; + assert_eq!(c.load().unwrap(), new_config); + restore_config_backup()?; + Ok(()) } } } -- cgit v1.2.3