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/login.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 7 deletions(-) (limited to 'src/login.rs') diff --git a/src/login.rs b/src/login.rs index da19a75..a6ce76d 100644 --- a/src/login.rs +++ b/src/login.rs @@ -1,16 +1,85 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; +use nostr::prelude::{FromSkStr, ToBech32}; +use zeroize::Zeroize; use crate::{ + cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, config::{ConfigManagement, ConfigManager}, - key_handling::users::{UserManagement, UserManager}, + key_handling::{ + encryption::{EncryptDecrypt, Encryptor}, + users::{UserManagement, UserManager}, + }, }; -pub fn launch(nsec: &Option) -> Result<()> { +/// handles the encrpytion and storage of key material +pub fn launch(nsec: &Option, password: &Option) -> Result { + // if nsec parameter + if let Some(nsec_unwrapped) = nsec { + // get key or fail without prompts + let key = nostr::Keys::from_sk_str(nsec_unwrapped).context("invalid nsec parameter")?; + println!( + "logged in as {}", + &key.public_key() + .to_bech32() + .context("public key should always produce bech32")? + ); + + // if password, add user to enable password login in future + if password.is_some() { + UserManager::default() + .add(nsec, password) + .context("could not store identity")?; + } + return Ok(key); + } + + // if encrypted nsec stored, attempt password let cfg = ConfigManager .load() .context("failed to load application config")?; - if !cfg.users.is_empty() { - println!("logged in as {}", cfg.users[0].nsec); - } - UserManager::default().add(nsec) + let key = if let Some(user) = cfg.users.last() { + let mut pass = if let Some(p) = password.clone() { + p + } else { + println!( + "login as {}", + &user + .public_key + .to_bech32() + .context("public key should always produce bech32")? + ); + Interactor::default() + .password(PromptPasswordParms::default().with_prompt("password")) + .context("failed to get password input from interactor.password")? + }; + + let key_result = Encryptor + .decrypt_key(&user.encrypted_key, pass.as_str()) + .context("failed to decrypt key with provided password"); + pass.zeroize(); + + key_result.context(format!( + "failed to log in as {}", + &user + .public_key + .to_bech32() + .context("public key should always produce bech32")? + ))? + } else { + // no nsec but password supplied + if password.is_some() { + bail!("no nsec available to decrypt with specified password"); + } + // otherwise add new user with nsec and password prompts + UserManager::default() + .add(nsec, password) + .context("failed to add user")? + }; + println!( + "logged in as {}", + &key.public_key() + .to_bech32() + .context("public key should always produce bech32")? + ); + Ok(key) } -- cgit v1.2.3