diff options
Diffstat (limited to 'src/key_handling')
| -rw-r--r-- | src/key_handling/encryption.rs | 247 | ||||
| -rw-r--r-- | src/key_handling/mod.rs | 1 | ||||
| -rw-r--r-- | src/key_handling/users.rs | 262 |
3 files changed, 483 insertions, 27 deletions
diff --git a/src/key_handling/encryption.rs b/src/key_handling/encryption.rs new file mode 100644 index 0000000..0ef7f69 --- /dev/null +++ b/src/key_handling/encryption.rs | |||
| @@ -0,0 +1,247 @@ | |||
| 1 | use anyhow::{anyhow, bail, ensure, Context, Result}; | ||
| 2 | use chacha20poly1305::{ | ||
| 3 | aead::{rand_core::RngCore, Aead, AeadCore, KeyInit, OsRng, Payload}, | ||
| 4 | XChaCha20Poly1305, | ||
| 5 | }; | ||
| 6 | #[cfg(test)] | ||
| 7 | use mockall::*; | ||
| 8 | use nostr::{prelude::*, Keys}; | ||
| 9 | use rand::{distributions::Alphanumeric, thread_rng, Rng}; | ||
| 10 | use zeroize::Zeroize; | ||
| 11 | |||
| 12 | #[derive(Default)] | ||
| 13 | pub struct Encryptor; | ||
| 14 | |||
| 15 | #[cfg_attr(test, automock)] | ||
| 16 | pub trait EncryptDecrypt { | ||
| 17 | /// requires less CPU time if the password is long | ||
| 18 | fn encrypt_key(&self, keys: &Keys, password: &str) -> Result<String>; | ||
| 19 | fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result<Keys>; | ||
| 20 | /// generates a long random string | ||
| 21 | fn random_token(&self) -> String; | ||
| 22 | } | ||
| 23 | |||
| 24 | /// approach and code adapted from nostr gossip client | ||
| 25 | impl EncryptDecrypt for Encryptor { | ||
| 26 | fn encrypt_key(&self, keys: &Keys, password: &str) -> Result<String> { | ||
| 27 | // Generate a random 16-byte salt | ||
| 28 | let salt = { | ||
| 29 | let mut salt: [u8; 16] = [0; 16]; | ||
| 30 | OsRng.fill_bytes(&mut salt); | ||
| 31 | salt | ||
| 32 | }; | ||
| 33 | |||
| 34 | let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); | ||
| 35 | |||
| 36 | let log2_rounds: u8 = if password.len() > 20 { | ||
| 37 | // we have enough of entropy - no need to spend CPU time adding much more | ||
| 38 | 1 | ||
| 39 | } else { | ||
| 40 | // default (scrypt::Params::RECOMMENDED_LOG_N) is 17 but 30s is too long to wait | ||
| 41 | 15 | ||
| 42 | }; | ||
| 43 | |||
| 44 | let associated_data: Vec<u8> = vec![1]; | ||
| 45 | |||
| 46 | let ciphertext = { | ||
| 47 | let cipher = { | ||
| 48 | let symmetric_key = password_to_key(password, &salt, log2_rounds) | ||
| 49 | .context("failed create encryption key from password")?; | ||
| 50 | XChaCha20Poly1305::new((&symmetric_key).into()) | ||
| 51 | }; | ||
| 52 | cipher | ||
| 53 | .encrypt( | ||
| 54 | &nonce, | ||
| 55 | Payload { | ||
| 56 | msg: keys | ||
| 57 | .secret_key() | ||
| 58 | .context( | ||
| 59 | "supplied key should reveal secret key. Is this a public key only?", | ||
| 60 | )? | ||
| 61 | .display_secret() | ||
| 62 | .to_string() | ||
| 63 | .as_bytes(), | ||
| 64 | aad: &associated_data, | ||
| 65 | }, | ||
| 66 | ) | ||
| 67 | .map_err(|_| anyhow!("ChaChaPoly1305 failed to encrypt nsec with password"))? | ||
| 68 | }; | ||
| 69 | // Combine salt, IV and ciphertext | ||
| 70 | let mut concatenation: Vec<u8> = Vec::new(); | ||
| 71 | concatenation.push(0x1); // 1 byte version number | ||
| 72 | concatenation.push(log2_rounds); // 1 byte for scrypt N (rounds) | ||
| 73 | concatenation.extend(salt); // 16 bytes of salt | ||
| 74 | concatenation.extend(nonce); // 24 bytes of nonce | ||
| 75 | concatenation.extend(associated_data); // 1 byte of key security | ||
| 76 | concatenation.extend(ciphertext); // 48 bytes of ciphertext expected | ||
| 77 | // Total length is 91 = 1 + 1 + 16 + 24 + 1 + 48 | ||
| 78 | |||
| 79 | bech32::encode( | ||
| 80 | "ncryptsec", | ||
| 81 | concatenation.to_base32(), | ||
| 82 | bech32::Variant::Bech32, | ||
| 83 | ) | ||
| 84 | .context("encrypted nsec failed to encode") | ||
| 85 | } | ||
| 86 | |||
| 87 | fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result<nostr::Keys> { | ||
| 88 | let data = | ||
| 89 | bech32::decode(encrypted_key).context("failed to decode encrypted key as bech32")?; | ||
| 90 | if data.0 != "ncryptsec" { | ||
| 91 | bail!("encrypted key is in the wrong format - it doesnt start with ncryptsec"); | ||
| 92 | } | ||
| 93 | let concatenation = Vec::<u8>::from_base32(&data.1) | ||
| 94 | .context("failed to convert bech32::decode output to Vec<u8>")?; | ||
| 95 | |||
| 96 | // Break into parts | ||
| 97 | let version: u8 = concatenation[0]; | ||
| 98 | ensure!(version == 0x1, "encryption version is incorrect"); | ||
| 99 | let log2_rounds: u8 = concatenation[1]; | ||
| 100 | let salt: [u8; 16] = concatenation[2..2 + 16].try_into()?; | ||
| 101 | let nonce = &concatenation[2 + 16..2 + 16 + 24]; | ||
| 102 | let associated_data = &concatenation[(2 + 16 + 24)..=(2 + 16 + 24)]; | ||
| 103 | let ciphertext = &concatenation[2 + 16 + 24 + 1..]; | ||
| 104 | |||
| 105 | let cipher = { | ||
| 106 | let symmetric_key = password_to_key(password, &salt, log2_rounds)?; | ||
| 107 | XChaCha20Poly1305::new((&symmetric_key).into()) | ||
| 108 | }; | ||
| 109 | |||
| 110 | let payload = Payload { | ||
| 111 | msg: ciphertext, | ||
| 112 | aad: associated_data, | ||
| 113 | }; | ||
| 114 | |||
| 115 | let mut inner_secret = cipher | ||
| 116 | .decrypt(nonce.into(), payload) | ||
| 117 | .map_err(|_| anyhow!("failed to decrypt"))?; | ||
| 118 | |||
| 119 | if associated_data.is_empty() { | ||
| 120 | bail!("invalid encrypted key"); | ||
| 121 | } | ||
| 122 | |||
| 123 | let key = Keys::from_sk_str( | ||
| 124 | std::str::from_utf8(&inner_secret).context("inner secret is not [u8]")?, | ||
| 125 | ) | ||
| 126 | .context("incorrect password. Key decrypted with password did not produce a valid nsec.")?; | ||
| 127 | |||
| 128 | inner_secret.zeroize(); | ||
| 129 | |||
| 130 | Ok(key) | ||
| 131 | } | ||
| 132 | |||
| 133 | fn random_token(&self) -> String { | ||
| 134 | thread_rng() | ||
| 135 | .sample_iter(&Alphanumeric) | ||
| 136 | .take(32) | ||
| 137 | .map(char::from) | ||
| 138 | .collect() | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | /// uses scrypt to stretch password into key | ||
| 143 | fn password_to_key(password: &str, salt: &[u8; 16], log_n: u8) -> Result<[u8; 32]> { | ||
| 144 | let params = scrypt::Params::new(log_n, 8, 1, 32) | ||
| 145 | .context("scrypt failed to generate params to stretch password")?; | ||
| 146 | let mut key: [u8; 32] = [0; 32]; | ||
| 147 | if log_n > 14 { | ||
| 148 | println!("this may take a few seconds..."); | ||
| 149 | } | ||
| 150 | |||
| 151 | scrypt::scrypt(password.as_bytes(), salt, ¶ms, &mut key) | ||
| 152 | .context("scrypt failed to stretch password")?; | ||
| 153 | Ok(key) | ||
| 154 | } | ||
| 155 | |||
| 156 | #[cfg(test)] | ||
| 157 | mod tests { | ||
| 158 | use test_utils::*; | ||
| 159 | |||
| 160 | use super::*; | ||
| 161 | |||
| 162 | #[test] | ||
| 163 | fn encrypt_key_produces_string_prefixed_with() -> Result<()> { | ||
| 164 | let s = Encryptor.encrypt_key(&nostr::Keys::generate(), TEST_PASSWORD)?; | ||
| 165 | assert!(s.starts_with("ncryptsec")); | ||
| 166 | Ok(()) | ||
| 167 | } | ||
| 168 | |||
| 169 | #[test] | ||
| 170 | // ensures password encryption hasn't changed | ||
| 171 | fn decrypts_with_strong_password_from_reference_string() -> Result<()> { | ||
| 172 | let encryptor = Encryptor; | ||
| 173 | let decrypted_key = encryptor.decrypt_key(TEST_KEY_1_ENCRYPTED, TEST_PASSWORD)?; | ||
| 174 | |||
| 175 | assert_eq!( | ||
| 176 | format!( | ||
| 177 | "{}", | ||
| 178 | TEST_KEY_1_KEYS.secret_key().unwrap().to_bech32().unwrap() | ||
| 179 | ), | ||
| 180 | format!( | ||
| 181 | "{}", | ||
| 182 | decrypted_key.secret_key().unwrap().to_bech32().unwrap() | ||
| 183 | ), | ||
| 184 | ); | ||
| 185 | Ok(()) | ||
| 186 | } | ||
| 187 | |||
| 188 | #[test] | ||
| 189 | // ensures password encryption hasn't changed | ||
| 190 | fn decrypts_with_weak_password_from_reference_string() -> Result<()> { | ||
| 191 | let encryptor = Encryptor; | ||
| 192 | let decrypted_key = encryptor.decrypt_key(TEST_KEY_1_ENCRYPTED_WEAK, TEST_WEAK_PASSWORD)?; | ||
| 193 | |||
| 194 | assert_eq!( | ||
| 195 | format!( | ||
| 196 | "{}", | ||
| 197 | TEST_KEY_1_KEYS.secret_key().unwrap().to_bech32().unwrap() | ||
| 198 | ), | ||
| 199 | format!( | ||
| 200 | "{}", | ||
| 201 | decrypted_key.secret_key().unwrap().to_bech32().unwrap() | ||
| 202 | ), | ||
| 203 | ); | ||
| 204 | Ok(()) | ||
| 205 | } | ||
| 206 | |||
| 207 | #[test] | ||
| 208 | fn decrypts_key_encrypted_using_encrypt_key() -> Result<()> { | ||
| 209 | let encryptor = Encryptor; | ||
| 210 | let key = nostr::Keys::generate(); | ||
| 211 | let s = encryptor.encrypt_key(&key, TEST_PASSWORD)?; | ||
| 212 | let newkey = encryptor.decrypt_key(s.as_str(), TEST_PASSWORD)?; | ||
| 213 | |||
| 214 | assert_eq!( | ||
| 215 | format!("{}", key.secret_key().unwrap().to_bech32().unwrap()), | ||
| 216 | format!("{}", newkey.secret_key().unwrap().to_bech32().unwrap()), | ||
| 217 | ); | ||
| 218 | Ok(()) | ||
| 219 | } | ||
| 220 | |||
| 221 | #[test] | ||
| 222 | fn decrypt_key_successfully_decrypts_key_encrypted_using_encrypt_key() -> Result<()> { | ||
| 223 | let encryptor = Encryptor; | ||
| 224 | let key = nostr::Keys::generate(); | ||
| 225 | let s = encryptor.encrypt_key(&key, TEST_PASSWORD)?; | ||
| 226 | let newkey = encryptor.decrypt_key(s.as_str(), TEST_PASSWORD)?; | ||
| 227 | |||
| 228 | assert_eq!( | ||
| 229 | format!("{}", key.secret_key().unwrap().to_bech32().unwrap()), | ||
| 230 | format!("{}", newkey.secret_key().unwrap().to_bech32().unwrap()), | ||
| 231 | ); | ||
| 232 | Ok(()) | ||
| 233 | } | ||
| 234 | |||
| 235 | #[test] | ||
| 236 | fn password_to_key_returns_ok_with_standard_password() { | ||
| 237 | let salt = { | ||
| 238 | let mut salt: [u8; 16] = [0; 16]; | ||
| 239 | OsRng.fill_bytes(&mut salt); | ||
| 240 | salt | ||
| 241 | }; | ||
| 242 | |||
| 243 | let log2_rounds: u8 = 1; | ||
| 244 | |||
| 245 | assert!(password_to_key(TEST_PASSWORD, &salt, log2_rounds).is_ok()); | ||
| 246 | } | ||
| 247 | } | ||
diff --git a/src/key_handling/mod.rs b/src/key_handling/mod.rs index 913bd46..bcb10df 100644 --- a/src/key_handling/mod.rs +++ b/src/key_handling/mod.rs | |||
| @@ -1 +1,2 @@ | |||
| 1 | pub mod encryption; | ||
| 1 | pub mod users; | 2 | pub mod users; |
diff --git a/src/key_handling/users.rs b/src/key_handling/users.rs index bd1748a..1d2cc34 100644 --- a/src/key_handling/users.rs +++ b/src/key_handling/users.rs | |||
| @@ -1,47 +1,90 @@ | |||
| 1 | use anyhow::{Context, Result}; | 1 | use anyhow::{Context, Result}; |
| 2 | use nostr::prelude::*; | ||
| 3 | use zeroize::Zeroize; | ||
| 2 | 4 | ||
| 5 | use super::encryption::{EncryptDecrypt, Encryptor}; | ||
| 3 | use crate::{ | 6 | use crate::{ |
| 4 | cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, | 7 | cli_interactor::{Interactor, InteractorPrompt, PromptInputParms, PromptPasswordParms}, |
| 5 | config::{ConfigManagement, ConfigManager, MyConfig, UserRef}, | 8 | config::{self, ConfigManagement, ConfigManager}, |
| 6 | }; | 9 | }; |
| 7 | 10 | ||
| 8 | #[derive(Default)] | 11 | #[derive(Default)] |
| 9 | pub struct UserManager { | 12 | pub struct UserManager { |
| 10 | config_manager: ConfigManager, | 13 | config_manager: ConfigManager, |
| 11 | interactor: Interactor, | 14 | interactor: Interactor, |
| 15 | encryptor: Encryptor, | ||
| 12 | } | 16 | } |
| 13 | 17 | ||
| 14 | pub trait UserManagement { | 18 | pub trait UserManagement { |
| 15 | fn add(&self, nsec: &Option<String>) -> Result<()>; | 19 | fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys>; |
| 16 | } | 20 | } |
| 17 | 21 | ||
| 18 | #[cfg(test)] | 22 | #[cfg(test)] |
| 19 | use duplicate::duplicate_item; | 23 | use duplicate::duplicate_item; |
| 20 | #[cfg_attr(test, duplicate_item(UserManager; [UserManager]; [self::tests::MockUserManager]))] | 24 | #[cfg_attr(test, duplicate_item(UserManager; [UserManager]; [self::tests::MockUserManager]))] |
| 21 | impl UserManagement for UserManager { | 25 | impl UserManagement for UserManager { |
| 22 | fn add(&self, nsec: &Option<String>) -> Result<()> { | 26 | fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { |
| 23 | let nsec = match nsec.clone() { | 27 | let mut prompt = "login with nsec (or hex private key)"; |
| 24 | Some(nsec) => nsec, | 28 | let keys = loop { |
| 29 | let pk = match nsec.clone() { | ||
| 30 | Some(nsec) => nsec, | ||
| 31 | None => self | ||
| 32 | .interactor | ||
| 33 | .input(PromptInputParms::default().with_prompt(prompt)) | ||
| 34 | .context("failed to get nsec input from interactor")?, | ||
| 35 | }; | ||
| 36 | match Keys::from_sk_str(&pk) { | ||
| 37 | Ok(key) => { | ||
| 38 | break key; | ||
| 39 | } | ||
| 40 | Err(e) => { | ||
| 41 | if nsec.is_some() { | ||
| 42 | return Err(e).context( | ||
| 43 | "invalid nsec - supplied parameter could not be converted into a nostr private key", | ||
| 44 | ); | ||
| 45 | } | ||
| 46 | prompt = "invalid nsec. try again with nsec (or hex private key)"; | ||
| 47 | } | ||
| 48 | } | ||
| 49 | }; | ||
| 50 | |||
| 51 | let mut pass = match password.clone() { | ||
| 52 | Some(pass) => pass, | ||
| 25 | None => self | 53 | None => self |
| 26 | .interactor | 54 | .interactor |
| 27 | .input( | 55 | .password( |
| 28 | PromptInputParms::default().with_prompt("login with nsec (or hex private key)"), | 56 | PromptPasswordParms::default() |
| 57 | .with_prompt("encrypt with password") | ||
| 58 | .with_confirm(), | ||
| 29 | ) | 59 | ) |
| 30 | .context("failed to get nsec input from interactor.input")?, | 60 | .context("failed to get password input from interactor.password")?, |
| 31 | }; | 61 | }; |
| 32 | 62 | ||
| 63 | let encrypted_secret_key = self | ||
| 64 | .encryptor | ||
| 65 | .encrypt_key(&keys, &pass) | ||
| 66 | .context("failed to encrypt nsec with password.")?; | ||
| 67 | pass.zeroize(); | ||
| 68 | |||
| 69 | let user_ref = config::UserRef { | ||
| 70 | public_key: keys.public_key(), | ||
| 71 | encrypted_key: encrypted_secret_key, | ||
| 72 | }; | ||
| 73 | |||
| 74 | // remove any duplicate entries for key before adding it to config | ||
| 75 | let mut cfg = self.config_manager.load().context("failed to load application config to find and remove any old versions of the user's encrypted key")?; | ||
| 76 | cfg.users = cfg | ||
| 77 | .users | ||
| 78 | .clone() | ||
| 79 | .into_iter() | ||
| 80 | .filter(|r| !r.public_key.eq(&keys.public_key())) | ||
| 81 | .collect(); | ||
| 82 | cfg.users.push(user_ref); | ||
| 33 | self.config_manager | 83 | self.config_manager |
| 34 | .save(&MyConfig { | 84 | .save(&cfg) |
| 35 | users: vec![UserRef { | ||
| 36 | nsec: nsec.to_string(), | ||
| 37 | }], | ||
| 38 | ..MyConfig::default() | ||
| 39 | }) | ||
| 40 | .context("failed to save application configuration with new user details in")?; | 85 | .context("failed to save application configuration with new user details in")?; |
| 41 | 86 | ||
| 42 | println!("logged in as {nsec}"); | 87 | Ok(keys) |
| 43 | |||
| 44 | Ok(()) | ||
| 45 | } | 88 | } |
| 46 | } | 89 | } |
| 47 | 90 | ||
| @@ -50,12 +93,17 @@ mod tests { | |||
| 50 | use test_utils::*; | 93 | use test_utils::*; |
| 51 | 94 | ||
| 52 | use super::*; | 95 | use super::*; |
| 53 | use crate::{cli_interactor::MockInteractorPrompt, config::MockConfigManagement}; | 96 | use crate::{ |
| 97 | cli_interactor::MockInteractorPrompt, | ||
| 98 | config::{MockConfigManagement, MyConfig, UserRef}, | ||
| 99 | key_handling::encryption::MockEncryptDecrypt, | ||
| 100 | }; | ||
| 54 | 101 | ||
| 55 | #[derive(Default)] | 102 | #[derive(Default)] |
| 56 | pub struct MockUserManager { | 103 | pub struct MockUserManager { |
| 57 | pub config_manager: MockConfigManagement, | 104 | pub config_manager: MockConfigManagement, |
| 58 | pub interactor: MockInteractorPrompt, | 105 | pub interactor: MockInteractorPrompt, |
| 106 | pub encryptor: MockEncryptDecrypt, | ||
| 59 | } | 107 | } |
| 60 | 108 | ||
| 61 | mod add { | 109 | mod add { |
| @@ -70,28 +118,88 @@ mod tests { | |||
| 70 | self.interactor | 118 | self.interactor |
| 71 | .expect_input() | 119 | .expect_input() |
| 72 | .returning(|_| Ok(TEST_KEY_1_NSEC.into())); | 120 | .returning(|_| Ok(TEST_KEY_1_NSEC.into())); |
| 121 | self.interactor | ||
| 122 | .expect_password() | ||
| 123 | .returning(|_| Ok(TEST_PASSWORD.into())); | ||
| 124 | self.encryptor | ||
| 125 | .expect_encrypt_key() | ||
| 126 | .returning(|_, _| Ok(TEST_KEY_1_ENCRYPTED.into())); | ||
| 73 | self | 127 | self |
| 74 | } | 128 | } |
| 75 | } | 129 | } |
| 76 | 130 | ||
| 77 | mod when_nsec_is_passed { | 131 | fn reuable_user_isnt_prompted(nsec: &str) { |
| 132 | let mut m = MockUserManager::default().add_return_expected_responses(); | ||
| 133 | m.interactor = MockInteractorPrompt::default(); | ||
| 134 | m.interactor.expect_input().never(); | ||
| 135 | m.interactor.expect_password().never(); | ||
| 136 | let _ = m.add(&Some(nsec.into()), &Some(TEST_PASSWORD.to_string())); | ||
| 137 | } | ||
| 138 | |||
| 139 | fn reuable_config_isnt_modified(nsec: &str) { | ||
| 140 | let mut m = MockUserManager::default(); | ||
| 141 | m.config_manager.expect_save().never(); | ||
| 142 | let _ = m.add(&Some(nsec.into()), &Some(TEST_PASSWORD.to_string())); | ||
| 143 | } | ||
| 144 | |||
| 145 | mod when_valid_nsec_and_password_is_passed { | ||
| 78 | use super::*; | 146 | use super::*; |
| 79 | 147 | ||
| 80 | #[test] | 148 | #[test] |
| 81 | fn user_isnt_prompted() { | 149 | fn user_isnt_prompted() { |
| 150 | reuable_user_isnt_prompted(TEST_KEY_1_NSEC); | ||
| 151 | } | ||
| 152 | |||
| 153 | #[test] | ||
| 154 | fn results_in_correct_keys() { | ||
| 82 | let mut m = MockUserManager::default().add_return_expected_responses(); | 155 | let mut m = MockUserManager::default().add_return_expected_responses(); |
| 83 | m.interactor = MockInteractorPrompt::default(); | 156 | m.interactor = MockInteractorPrompt::default(); |
| 84 | m.interactor.expect_input().never(); | 157 | m.interactor.expect_input().never(); |
| 85 | 158 | m.interactor.expect_password().never(); | |
| 86 | let _ = m.add(&Some(TEST_KEY_1_NSEC.into())); | 159 | let r = m.add( |
| 160 | &Some(TEST_KEY_1_NSEC.into()), | ||
| 161 | &Some(TEST_PASSWORD.to_string()), | ||
| 162 | ); | ||
| 163 | assert!(r.is_ok(), "should result in keys"); | ||
| 164 | assert!( | ||
| 165 | r.is_ok_and(|k| k | ||
| 166 | .secret_key() | ||
| 167 | .is_ok_and(|k| k.display_secret().to_string().eq(TEST_KEY_1_SK_HEX))), | ||
| 168 | "keys should reflect nsec" | ||
| 169 | ); | ||
| 87 | } | 170 | } |
| 88 | } | 171 | } |
| 172 | mod when_invalid_nsec_is_passed_with_password { | ||
| 173 | use super::*; | ||
| 89 | 174 | ||
| 175 | #[test] | ||
| 176 | fn user_isnt_prompted() { | ||
| 177 | reuable_user_isnt_prompted(TEST_INVALID_NSEC); | ||
| 178 | } | ||
| 179 | |||
| 180 | #[test] | ||
| 181 | fn config_isnt_modified() { | ||
| 182 | reuable_config_isnt_modified(TEST_INVALID_NSEC); | ||
| 183 | } | ||
| 184 | |||
| 185 | #[test] | ||
| 186 | fn results_in_an_error() { | ||
| 187 | let m = MockUserManager::default(); | ||
| 188 | assert!( | ||
| 189 | m.add( | ||
| 190 | &Some(TEST_INVALID_NSEC.into()), | ||
| 191 | &Some(TEST_PASSWORD.to_string()) | ||
| 192 | ) | ||
| 193 | .is_err(), | ||
| 194 | "should result in an error" | ||
| 195 | ); | ||
| 196 | } | ||
| 197 | } | ||
| 90 | mod when_no_nsec_is_passed { | 198 | mod when_no_nsec_is_passed { |
| 91 | use super::*; | 199 | use super::*; |
| 92 | 200 | ||
| 93 | #[test] | 201 | #[test] |
| 94 | fn prompt_for_nsec() { | 202 | fn prompt_for_nsec_and_password() { |
| 95 | let mut m = MockUserManager::default().add_return_expected_responses(); | 203 | let mut m = MockUserManager::default().add_return_expected_responses(); |
| 96 | 204 | ||
| 97 | m.interactor = MockInteractorPrompt::new(); | 205 | m.interactor = MockInteractorPrompt::new(); |
| @@ -100,12 +208,31 @@ mod tests { | |||
| 100 | .once() | 208 | .once() |
| 101 | .withf(|p| p.prompt.eq("login with nsec (or hex private key)")) | 209 | .withf(|p| p.prompt.eq("login with nsec (or hex private key)")) |
| 102 | .returning(|_| Ok(TEST_KEY_1_NSEC.into())); | 210 | .returning(|_| Ok(TEST_KEY_1_NSEC.into())); |
| 211 | m.interactor | ||
| 212 | .expect_password() | ||
| 213 | .once() | ||
| 214 | .withf(|p| p.prompt.eq("encrypt with password")) | ||
| 215 | .returning(|_| Ok(TEST_KEY_1_NSEC.into())); | ||
| 103 | 216 | ||
| 104 | let _ = m.add(&None); | 217 | let _ = m.add(&None, &None); |
| 105 | } | 218 | } |
| 106 | 219 | ||
| 107 | #[test] | 220 | #[test] |
| 108 | fn stored_in_config() { | 221 | fn results_in_correct_keys() { |
| 222 | let m = MockUserManager::default().add_return_expected_responses(); | ||
| 223 | |||
| 224 | let r = m.add(&None, &None); | ||
| 225 | assert!(r.is_ok(), "should result in keys"); | ||
| 226 | assert!( | ||
| 227 | r.is_ok_and(|k| k | ||
| 228 | .secret_key() | ||
| 229 | .is_ok_and(|k| k.display_secret().to_string().eq(TEST_KEY_1_SK_HEX))), | ||
| 230 | "keys should reflect nsec" | ||
| 231 | ); | ||
| 232 | } | ||
| 233 | |||
| 234 | #[test] | ||
| 235 | fn stores_encrypted_key_in_config() { | ||
| 109 | let mut m = MockUserManager::default().add_return_expected_responses(); | 236 | let mut m = MockUserManager::default().add_return_expected_responses(); |
| 110 | 237 | ||
| 111 | m.config_manager = MockConfigManagement::new(); | 238 | m.config_manager = MockConfigManagement::new(); |
| @@ -114,10 +241,91 @@ mod tests { | |||
| 114 | .returning(|| Ok(MyConfig::default())); | 241 | .returning(|| Ok(MyConfig::default())); |
| 115 | m.config_manager | 242 | m.config_manager |
| 116 | .expect_save() | 243 | .expect_save() |
| 117 | .withf(|cfg| cfg.users.len().eq(&1) && cfg.users[0].nsec.eq(TEST_KEY_1_NSEC)) | 244 | .withf(|cfg| { |
| 245 | cfg.users.len().eq(&1) | ||
| 246 | && cfg.users[0].encrypted_key.eq(TEST_KEY_1_ENCRYPTED) | ||
| 247 | }) | ||
| 118 | .returning(|_| Ok(())); | 248 | .returning(|_| Ok(())); |
| 119 | 249 | ||
| 120 | let _ = m.add(&None); | 250 | let _ = m.add(&None, &None); |
| 251 | } | ||
| 252 | |||
| 253 | #[test] | ||
| 254 | fn stored_key_encrypted_with_password() { | ||
| 255 | let mut m = MockUserManager::default().add_return_expected_responses(); | ||
| 256 | |||
| 257 | m.encryptor = MockEncryptDecrypt::new(); | ||
| 258 | m.encryptor | ||
| 259 | .expect_encrypt_key() | ||
| 260 | .once() | ||
| 261 | .withf(|k, p| { | ||
| 262 | k.eq(&Keys::from_sk_str(TEST_KEY_1_NSEC).unwrap()) && p.eq(TEST_PASSWORD) | ||
| 263 | }) | ||
| 264 | .returning(|_, _| Ok(TEST_KEY_1_ENCRYPTED.into())); | ||
| 265 | |||
| 266 | let _ = m.add(&None, &None); | ||
| 267 | } | ||
| 268 | |||
| 269 | mod when_user_key_already_stored { | ||
| 270 | use super::*; | ||
| 271 | use crate::config::UserRef; | ||
| 272 | |||
| 273 | /// key overwritten as password may have changed | ||
| 274 | #[test] | ||
| 275 | fn key_not_saved_as_duplicate_but_encrypted_key_overwritten() { | ||
| 276 | let mut m = MockUserManager::default().add_return_expected_responses(); | ||
| 277 | |||
| 278 | m.config_manager = MockConfigManagement::default(); | ||
| 279 | m.config_manager.expect_load().returning(|| { | ||
| 280 | Ok(MyConfig { | ||
| 281 | users: vec![UserRef { | ||
| 282 | public_key: TEST_KEY_1_KEYS.public_key(), | ||
| 283 | // different key to TEST_KEY_1_ENCYPTED | ||
| 284 | encrypted_key: TEST_KEY_2_ENCRYPTED.into(), | ||
| 285 | }], | ||
| 286 | ..MyConfig::default() | ||
| 287 | }) | ||
| 288 | }); | ||
| 289 | m.config_manager | ||
| 290 | .expect_save() | ||
| 291 | .withf(|cfg| { | ||
| 292 | cfg.users.len() == 1 | ||
| 293 | && cfg.users[0].encrypted_key.eq(TEST_KEY_1_ENCRYPTED) | ||
| 294 | }) | ||
| 295 | .returning(|_| Ok(())); | ||
| 296 | |||
| 297 | let _ = m.add(&None, &None); | ||
| 298 | } | ||
| 299 | } | ||
| 300 | |||
| 301 | mod when_multiple_users_added { | ||
| 302 | use super::*; | ||
| 303 | |||
| 304 | #[test] | ||
| 305 | fn both_user_keys_are_stored() { | ||
| 306 | let mut m = MockUserManager::default().add_return_expected_responses(); | ||
| 307 | |||
| 308 | m.config_manager = MockConfigManagement::default(); | ||
| 309 | m.config_manager.expect_load().returning(|| { | ||
| 310 | Ok(MyConfig { | ||
| 311 | users: vec![UserRef { | ||
| 312 | public_key: TEST_KEY_2_KEYS.public_key(), | ||
| 313 | encrypted_key: TEST_KEY_2_ENCRYPTED.into(), | ||
| 314 | }], | ||
| 315 | ..MyConfig::default() | ||
| 316 | }) | ||
| 317 | }); | ||
| 318 | m.config_manager | ||
| 319 | .expect_save() | ||
| 320 | .withf(|cfg| { | ||
| 321 | cfg.users.len() == 2 | ||
| 322 | // latest user stored at end of array | ||
| 323 | && cfg.users[1].encrypted_key.eq(TEST_KEY_1_ENCRYPTED) | ||
| 324 | }) | ||
| 325 | .returning(|_| Ok(())); | ||
| 326 | |||
| 327 | let _ = m.add(&None, &None); | ||
| 328 | } | ||
| 121 | } | 329 | } |
| 122 | } | 330 | } |
| 123 | } | 331 | } |