upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2023-09-01 00:00:00 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2023-09-01 00:00:00 +0000
commit96660a90e4cd296a2922d7a547de4cd9d0b1928b (patch)
treee5216e22ee1a3e1653d8d1ecd856f4f03615d6a1 /tests
parent6423baebd92e45c9be85157c443dff42e65d8d14 (diff)
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.
Diffstat (limited to 'tests')
-rw-r--r--tests/login.rs334
1 files changed, 292 insertions, 42 deletions
diff --git a/tests/login.rs b/tests/login.rs
index a7e1889..a75608d 100644
--- a/tests/login.rs
+++ b/tests/login.rs
@@ -3,6 +3,9 @@ use serial_test::serial;
3use test_utils::*; 3use test_utils::*;
4 4
5static EXPECTED_NSEC_PROMPT: &str = "login with nsec (or hex private key)"; 5static EXPECTED_NSEC_PROMPT: &str = "login with nsec (or hex private key)";
6static EXPECTED_SET_PASSWORD_PROMPT: &str = "encrypt with password";
7static EXPECTED_SET_PASSWORD_CONFIRM_PROMPT: &str = "confirm password";
8static EXPECTED_PASSWORD_PROMPT: &str = "password";
6 9
7fn standard_login() -> Result<CliTester> { 10fn standard_login() -> Result<CliTester> {
8 let mut p = CliTester::new(["login"]); 11 let mut p = CliTester::new(["login"]);
@@ -10,6 +13,10 @@ fn standard_login() -> Result<CliTester> {
10 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)? 13 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)?
11 .succeeds_with(TEST_KEY_1_NSEC)?; 14 .succeeds_with(TEST_KEY_1_NSEC)?;
12 15
16 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
17 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
18 .succeeds_with(TEST_PASSWORD)?;
19
13 p.expect_end_eventually()?; 20 p.expect_end_eventually()?;
14 Ok(p) 21 Ok(p)
15} 22}
@@ -19,11 +26,10 @@ mod when_first_time_login {
19 26
20 #[test] 27 #[test]
21 #[serial] 28 #[serial]
22 fn prompts_for_nsec() -> Result<()> { 29 fn prompts_for_nsec_and_password() -> Result<()> {
23 with_fresh_config(|| { 30 before()?;
24 standard_login()?; 31 standard_login()?;
25 Ok(()) 32 after()
26 })
27 } 33 }
28 34
29 #[test] 35 #[test]
@@ -35,36 +41,137 @@ mod when_first_time_login {
35 p.expect_input(EXPECTED_NSEC_PROMPT)? 41 p.expect_input(EXPECTED_NSEC_PROMPT)?
36 .succeeds_with(TEST_KEY_1_NSEC)?; 42 .succeeds_with(TEST_KEY_1_NSEC)?;
37 43
38 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str()) 44 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
45 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
46 .succeeds_with(TEST_PASSWORD)?;
47
48 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
39 }) 49 })
40 } 50 }
41 51
42 #[test] 52 #[test]
43 #[serial] 53 #[serial]
44 fn next_time_returns_logged_in_as_npub() -> Result<()> { 54 fn succeeds_with_hex_secret_key_in_place_of_nsec() -> Result<()> {
55 with_fresh_config(|| {
56 let mut p = CliTester::new(["login"]);
57
58 p.expect_input(EXPECTED_NSEC_PROMPT)?
59 .succeeds_with(TEST_KEY_1_SK_HEX)?;
60
61 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
62 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
63 .succeeds_with(TEST_PASSWORD)?;
64
65 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
66 })
67 }
68
69 mod when_invalid_nsec {
70 use super::*;
71
72 #[test]
73 #[serial]
74 fn prompts_for_nsec_until_valid() -> Result<()> {
75 with_fresh_config(|| {
76 let invalid_nsec_response =
77 "invalid nsec. try again with nsec (or hex private key)";
78
79 let mut p = CliTester::new(["login"]);
80
81 p.expect_input(EXPECTED_NSEC_PROMPT)?
82 // this behaviour is intentional. rejecting the response with dialoguer hides
83 // the original input from the user so they cannot see the
84 // mistake they made.
85 .succeeds_with(TEST_INVALID_NSEC)?;
86
87 p.expect_input(invalid_nsec_response)?
88 .succeeds_with(TEST_INVALID_NSEC)?;
89
90 p.expect_input(invalid_nsec_response)?
91 .succeeds_with(TEST_KEY_1_NSEC)?;
92
93 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
94 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
95 .succeeds_with(TEST_PASSWORD)?;
96
97 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
98 })
99 }
100 }
101}
102
103mod when_second_time_login {
104 use super::*;
105
106 #[test]
107 #[serial]
108 fn prints_login_as_npub() -> Result<()> {
45 with_fresh_config(|| { 109 with_fresh_config(|| {
46 standard_login()?.exit()?; 110 standard_login()?.exit()?;
47 111
48 CliTester::new(["login"]) 112 CliTester::new(["login"])
49 .expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())? 113 .expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
50 .exit() 114 .exit()
51 }) 115 })
52 } 116 }
117
118 #[test]
119 #[serial]
120 fn prompts_for_password_and_succeeds_with_logged_in_as_npub() -> Result<()> {
121 with_fresh_config(|| {
122 standard_login()?.exit()?;
123
124 let mut p = CliTester::new(["login"]);
125
126 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
127 .expect_password(EXPECTED_PASSWORD_PROMPT)?
128 .succeeds_with(TEST_PASSWORD)?;
129
130 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
131 })
132 }
133
134 #[test]
135 #[serial]
136 fn when_invalid_password_exit_with_error() -> Result<()> {
137 with_fresh_config(|| {
138 standard_login()?.exit()?;
139
140 let mut p = CliTester::new(["login"]);
141
142 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
143 .expect_password(EXPECTED_PASSWORD_PROMPT)?
144 .succeeds_with(TEST_INVALID_PASSWORD)?;
145 p.expect_end_with(format!("Error: failed to log in as {}\r\n\r\nCaused by:\r\n 0: failed to decrypt key with provided password\r\n 1: failed to decrypt\r\n", TEST_KEY_1_NPUB).as_str())
146 })
147 }
53} 148}
54 149
55mod when_called_with_nsec_parameter { 150mod when_called_with_nsec_parameter_only {
56 use super::*; 151 use super::*;
57 152
58 #[test] 153 #[test]
59 #[serial] 154 #[serial]
60 fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { 155 fn valid_nsec_param_succeeds_without_prompts() -> Result<()> {
61 with_fresh_config(|| { 156 with_fresh_config(|| {
62 CliTester::new(["--nsec", TEST_KEY_2_NSEC, "login"]) 157 CliTester::new(["login", "--nsec", TEST_KEY_1_NSEC])
63 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())?; 158 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
159 })
160 }
161
162 #[test]
163 #[serial]
164 fn forgets_identity() -> Result<()> {
165 with_fresh_config(|| {
166 CliTester::new(["login", "--nsec", TEST_KEY_1_NSEC])
167 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
64 168
65 CliTester::new(["login"]) 169 let mut p = CliTester::new(["login"]);
66 .expect(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())? 170
67 .exit() 171 p.expect_input(EXPECTED_NSEC_PROMPT)?
172 .succeeds_with(TEST_KEY_1_NSEC)?;
173
174 p.exit()
68 }) 175 })
69 } 176 }
70 177
@@ -77,69 +184,212 @@ mod when_called_with_nsec_parameter {
77 with_fresh_config(|| { 184 with_fresh_config(|| {
78 standard_login()?.exit()?; 185 standard_login()?.exit()?;
79 186
80 CliTester::new(["--nsec", TEST_KEY_2_NSEC, "login"]) 187 CliTester::new(["login", "--nsec", TEST_KEY_2_NSEC])
81 .expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())? 188 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str())
82 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())?;
83
84 CliTester::new(["login"])
85 .expect(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())?
86 .exit()
87 }) 189 })
88 } 190 }
89 } 191 }
192 #[test]
193 #[serial]
194 fn invalid_nsec_param_fails_without_prompts() -> Result<()> {
195 with_fresh_config(|| {
196 CliTester::new(["login", "--nsec", TEST_INVALID_NSEC]).expect_end_with(
197 "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n",
198 )
199 })
200 }
90} 201}
91 202
92mod when_logged_in { 203mod when_called_with_nsec_and_password_parameter {
93 use super::*; 204 use super::*;
94 205
95 #[test] 206 #[test]
96 #[serial] 207 #[serial]
97 fn returns_logged_in_as_npub() -> Result<()> { 208 fn valid_nsec_param_succeeds_without_prompts() -> Result<()> {
98 with_fresh_config(|| { 209 with_fresh_config(|| {
99 standard_login()?.exit()?; 210 CliTester::new([
211 "login",
212 "--nsec",
213 TEST_KEY_1_NSEC,
214 "--password",
215 TEST_PASSWORD,
216 ])
217 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
218 })
219 }
220
221 #[test]
222 #[serial]
223 fn remembers_identity() -> Result<()> {
224 with_fresh_config(|| {
225 CliTester::new([
226 "login",
227 "--nsec",
228 TEST_KEY_1_NSEC,
229 "--password",
230 TEST_PASSWORD,
231 ])
232 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
100 233
101 CliTester::new(["login"]) 234 CliTester::new(["login"])
102 .expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())? 235 .expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
103 .exit() 236 .exit()
104 }) 237 })
105 } 238 }
106 239
107 #[test] 240 #[test]
108 #[serial] 241 #[serial]
109 fn prompts_to_log_in_with_different_nsec() -> Result<()> { 242 fn parameters_can_be_called_globally() -> Result<()> {
110 with_fresh_config(|| { 243 with_fresh_config(|| {
111 standard_login()?.exit()?; 244 CliTester::new([
112 245 "--nsec",
113 let mut p = CliTester::new(["login"]); 246 TEST_KEY_1_NSEC,
114 p.expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())?; 247 "--password",
115 248 TEST_PASSWORD,
116 p.expect_input(EXPECTED_NSEC_PROMPT)? 249 "login",
117 .succeeds_with(TEST_KEY_2_NSEC)?; 250 ])
118 251 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
119 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())
120 }) 252 })
121 } 253 }
254
122 mod when_logging_in_as_different_nsec { 255 mod when_logging_in_as_different_nsec {
123 use super::*; 256 use super::*;
124 257
125 #[test] 258 #[test]
126 #[serial] 259 #[serial]
127 fn confirmed_as_logged_in_as_additional_user() -> Result<()> { 260 fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> {
128 with_fresh_config(|| { 261 with_fresh_config(|| {
129 standard_login()?.exit()?; 262 standard_login()?.exit()?;
130 263
131 let mut p = CliTester::new(["login"]); 264 CliTester::new([
132 p.expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())?; 265 "login",
266 "--nsec",
267 TEST_KEY_2_NSEC,
268 "--password",
269 TEST_PASSWORD,
270 ])
271 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str())
272 })
273 }
133 274
134 p.expect_input(EXPECTED_NSEC_PROMPT)? 275 #[test]
135 .succeeds_with(TEST_KEY_2_NSEC)?; 276 #[serial]
277 fn remembers_identity() -> Result<()> {
278 with_fresh_config(|| {
279 standard_login()?.exit()?;
136 280
137 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())?; 281 CliTester::new([
282 "login",
283 "--nsec",
284 TEST_KEY_2_NSEC,
285 "--password",
286 TEST_PASSWORD,
287 ])
288 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str())?;
138 289
139 CliTester::new(["login"]) 290 CliTester::new(["login"])
140 .expect(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())? 291 .expect(format!("login as {}\r\n", TEST_KEY_2_NPUB).as_str())?
141 .exit() 292 .exit()
142 }) 293 })
143 } 294 }
144 } 295 }
296
297 mod when_provided_with_new_password {
298 use super::*;
299
300 #[test]
301 #[serial]
302 fn password_changes() -> Result<()> {
303 with_fresh_config(|| {
304 standard_login()?.exit()?;
305
306 CliTester::new([
307 "login",
308 "--nsec",
309 TEST_KEY_1_NSEC,
310 "--password",
311 TEST_INVALID_PASSWORD,
312 ])
313 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
314
315 CliTester::new(["--password", TEST_INVALID_PASSWORD, "login"])
316 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
317 })
318 }
319 }
320
321 #[test]
322 #[serial]
323 fn invalid_nsec_param_fails_without_prompts() -> Result<()> {
324 with_fresh_config(|| {
325 CliTester::new([
326 "login",
327 "--nsec",
328 TEST_INVALID_NSEC,
329 "--password",
330 TEST_PASSWORD,
331 ])
332 .expect_end_with(
333 "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n",
334 )
335 })
336 }
337}
338
339mod when_called_with_password_parameter_only {
340 use super::*;
341
342 #[test]
343 #[serial]
344 fn when_nsec_stored_logs_in_without_prompts() -> Result<()> {
345 with_fresh_config(|| {
346 standard_login()?.exit()?;
347
348 CliTester::new(["login", "--password", TEST_PASSWORD])
349 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
350 })
351 }
352
353 #[test]
354 #[serial]
355 fn when_no_nsec_stored_logs_error() -> Result<()> {
356 with_fresh_config(|| {
357 CliTester::new(["login", "--password", TEST_PASSWORD])
358 .expect_end_with("Error: no nsec available to decrypt with specified password\r\n")
359 })
360 }
361}
362
363mod when_weak_password {
364 use super::*;
365
366 #[test]
367 #[serial]
368 // combined into a single test as it is computationally expensive to run
369 fn warns_it_might_take_a_few_seconds_then_succeeds_then_second_login_prompts_for_password_then_warns_again_then_succeeds()
370 -> Result<()> {
371 with_fresh_config(|| {
372 let mut p = CliTester::new_with_timeout(10000, ["login"]);
373 p.expect_input(EXPECTED_NSEC_PROMPT)?
374 .succeeds_with(TEST_KEY_1_NSEC)?;
375
376 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
377 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
378 .succeeds_with(TEST_WEAK_PASSWORD)?;
379
380 p.expect("this may take a few seconds...\r\n")?;
381
382 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
383
384 p = CliTester::new_with_timeout(10000, ["login"]);
385
386 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
387 .expect_password(EXPECTED_PASSWORD_PROMPT)?
388 .succeeds_with(TEST_WEAK_PASSWORD)?;
389
390 p.expect("this may take a few seconds...\r\n")?;
391
392 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
393 })
394 }
145} 395}