upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/login.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-06-28 15:16:43 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-06-28 15:16:43 +0100
commita82546b70303000b4fc053a1ee21d3d8c7d6ad66 (patch)
treef8c4238a5ae27759b3c1a6adb5c865b07e339a66 /src/login.rs
parent6b06e874119ceca1a9dac1b94dcfe6e06aacd7b9 (diff)
feat(login): login with nip46 remote signer
and save details in git config
Diffstat (limited to 'src/login.rs')
-rw-r--r--src/login.rs374
1 files changed, 308 insertions, 66 deletions
diff --git a/src/login.rs b/src/login.rs
index e1669c1..218a079 100644
--- a/src/login.rs
+++ b/src/login.rs
@@ -1,11 +1,13 @@
1use std::str::FromStr; 1use std::{fs::create_dir_all, str::FromStr, time::Duration};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
4use nostr::PublicKey; 4use nostr::{nips::nip46::NostrConnectURI, PublicKey};
5use nostr_database::Order; 5use nostr_database::Order;
6use nostr_sdk::{ 6use nostr_sdk::{
7 Alphabet, FromBech32, JsonUtil, Kind, NostrDatabase, NostrSigner, SingleLetterTag, ToBech32, 7 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrDatabase, NostrSigner, SingleLetterTag,
8 ToBech32,
8}; 9};
10use nostr_signer::Nip46Signer;
9use nostr_sqlite::SQLiteDatabase; 11use nostr_sqlite::SQLiteDatabase;
10 12
11#[cfg(not(test))] 13#[cfg(not(test))]
@@ -16,7 +18,7 @@ use crate::{
16 cli_interactor::{ 18 cli_interactor::{
17 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms, 19 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms,
18 }, 20 },
19 client::Connect, 21 client::{fetch_public_key, Connect},
20 config::{get_dirs, UserMetadata, UserRef, UserRelayRef, UserRelays}, 22 config::{get_dirs, UserMetadata, UserRef, UserRelayRef, UserRelays},
21 git::{Repo, RepoActions}, 23 git::{Repo, RepoActions},
22 key_handling::encryption::{decrypt_key, encrypt_key}, 24 key_handling::encryption::{decrypt_key, encrypt_key},
@@ -25,14 +27,25 @@ use crate::{
25/// handles the encrpytion and storage of key material 27/// handles the encrpytion and storage of key material
26pub async fn launch( 28pub async fn launch(
27 git_repo: &Repo, 29 git_repo: &Repo,
30 bunker_uri: &Option<String>,
31 bunker_app_key: &Option<String>,
28 nsec: &Option<String>, 32 nsec: &Option<String>,
29 password: &Option<String>, 33 password: &Option<String>,
30 #[cfg(test)] client: Option<&MockConnect>, 34 #[cfg(test)] client: Option<&MockConnect>,
31 #[cfg(not(test))] client: Option<&Client>, 35 #[cfg(not(test))] client: Option<&Client>,
32 change_user: bool, 36 change_user: bool,
33) -> Result<(NostrSigner, UserRef)> { 37) -> Result<(NostrSigner, UserRef)> {
34 if let Ok(keys) = match get_keys_without_prompts(git_repo, nsec, password, change_user) { 38 if let Ok(signer) = match get_signer_without_prompts(
35 Ok(keys) => Ok(keys), 39 git_repo,
40 bunker_uri,
41 bunker_app_key,
42 nsec,
43 password,
44 change_user,
45 )
46 .await
47 {
48 Ok(signer) => Ok(signer),
36 Err(error) => { 49 Err(error) => {
37 if error 50 if error
38 .to_string() 51 .to_string()
@@ -60,7 +73,7 @@ pub async fn launch(
60 .password(PromptPasswordParms::default().with_prompt("password")) 73 .password(PromptPasswordParms::default().with_prompt("password"))
61 .context("failed to get password input from interactor.password")?; 74 .context("failed to get password input from interactor.password")?;
62 if let Ok(keys) = get_keys_with_password(git_repo, &password) { 75 if let Ok(keys) = get_keys_with_password(git_repo, &password) {
63 break Ok(keys); 76 break Ok(NostrSigner::Keys(keys));
64 } 77 }
65 println!("incorrect password"); 78 println!("incorrect password");
66 } 79 }
@@ -73,9 +86,17 @@ pub async fn launch(
73 } 86 }
74 } { 87 } {
75 // get user ref 88 // get user ref
76 let user_ref = get_user_details(&keys.public_key(), client, git_repo).await?; 89 let user_ref = get_user_details(
90 &signer
91 .public_key()
92 .await
93 .context("cannot get public key from signer")?,
94 client,
95 git_repo,
96 )
97 .await?;
77 print_logged_in_as(&user_ref, client.is_none())?; 98 print_logged_in_as(&user_ref, client.is_none())?;
78 Ok((NostrSigner::Keys(keys), user_ref)) 99 Ok((signer, user_ref))
79 } else { 100 } else {
80 fresh_login(git_repo, client, change_user).await 101 fresh_login(git_repo, client, change_user).await
81 } 102 }
@@ -95,18 +116,45 @@ fn print_logged_in_as(user_ref: &UserRef, offline_mode: bool) -> Result<()> {
95 Ok(()) 116 Ok(())
96} 117}
97 118
98fn get_keys_without_prompts( 119async fn get_signer_without_prompts(
99 git_repo: &Repo, 120 git_repo: &Repo,
121 bunker_uri: &Option<String>,
122 bunker_app_key: &Option<String>,
100 nsec: &Option<String>, 123 nsec: &Option<String>,
101 password: &Option<String>, 124 password: &Option<String>,
102 save_local: bool, 125 save_local: bool,
103) -> Result<nostr::Keys> { 126) -> Result<NostrSigner> {
104 if let Some(nsec) = nsec { 127 if let Some(nsec) = nsec {
105 get_keys_from_nsec(git_repo, nsec, password, save_local) 128 Ok(NostrSigner::Keys(get_keys_from_nsec(
129 git_repo, nsec, password, save_local,
130 )?))
106 } else if let Some(password) = password { 131 } else if let Some(password) = password {
107 get_keys_with_password(git_repo, password) 132 Ok(NostrSigner::Keys(get_keys_with_password(
133 git_repo, password,
134 )?))
135 } else if let Some(bunker_uri) = bunker_uri {
136 if let Some(bunker_app_key) = bunker_app_key {
137 let signer = get_nip46_signer_from_uri_and_key(bunker_uri, bunker_app_key)
138 .await
139 .context("failed to connect with remote signer")?;
140 if save_local {
141 save_to_git_config(
142 git_repo,
143 &signer.public_key().await?.to_bech32()?,
144 &None,
145 &Some((bunker_uri.to_string(),bunker_app_key.to_string())),
146 false,
147 )
148 .context("failed to save bunker details local git config nostr.bunker-uri and nostr.bunker-app-key")?;
149 }
150 Ok(signer)
151 } else {
152 bail!(
153 "bunker-app-key parameter must be provided alongside bunker-uri. if unknown, login interactively."
154 )
155 }
108 } else if !save_local { 156 } else if !save_local {
109 get_keys_with_git_config_nsec_without_prompts(git_repo) 157 get_signer_with_git_config_nsec_or_bunker_without_prompts(git_repo).await
110 } else { 158 } else {
111 bail!("user wants prompts to specify new keys") 159 bail!("user wants prompts to specify new keys")
112 } 160 }
@@ -139,18 +187,82 @@ fn get_keys_from_nsec(
139 if let Some(password) = password { 187 if let Some(password) = password {
140 s = encrypt_key(&keys, password)?; 188 s = encrypt_key(&keys, password)?;
141 } 189 }
142 git_repo 190 save_to_git_config(
143 .save_git_config_item("nostr.nsec", &s, false) 191 git_repo,
144 .context("failed to save encrypted nsec in local git config nostr.nsec")?; 192 &keys.public_key().to_bech32()?,
145 git_repo.save_git_config_item("nostr.npub", &keys.public_key().to_bech32()?, false)?; 193 &Some(s),
194 &None,
195 false,
196 )
197 .context("failed to save encrypted nsec in local git config nostr.nsec")?;
146 } 198 }
147 Ok(keys) 199 Ok(keys)
148} 200}
149 201
202fn save_to_git_config(
203 git_repo: &Repo,
204 npub: &str,
205 nsec: &Option<String>,
206 bunker: &Option<(String, String)>,
207 global: bool,
208) -> Result<()> {
209 if let Err(error) = silently_save_to_git_config(git_repo, npub, nsec, bunker, global) {
210 println!(
211 "failed to save login details to {} git config",
212 if global { "global" } else { "local" }
213 );
214 if let Some(nsec) = nsec {
215 if nsec.contains("ncryptsec") {
216 println!("manually set git config nostr.nsec to: {nsec}");
217 } else {
218 println!("manually set git config nostr.nsec");
219 }
220 }
221 if let Some(bunker) = bunker {
222 println!("manually set git config as follows:");
223 println!("nostr.bunker-uri: {}", bunker.0);
224 println!("nostr.bunker-app-key: {}", bunker.1);
225 }
226 Err(error)
227 } else {
228 println!(
229 "saved login details to {} git config",
230 if global { "global" } else { "local" }
231 );
232 Ok(())
233 }
234}
235fn silently_save_to_git_config(
236 git_repo: &Repo,
237 npub: &str,
238 nsec: &Option<String>,
239 bunker: &Option<(String, String)>,
240 global: bool,
241) -> Result<()> {
242 // must do this first otherwise it might remove the global items just added
243 if global {
244 git_repo.remove_git_config_item("nostr.npub", false)?;
245 git_repo.remove_git_config_item("nostr.nsec", false)?;
246 git_repo.remove_git_config_item("nostr.bunker-uri", false)?;
247 git_repo.remove_git_config_item("nostr.bunker-app-key", false)?;
248 }
249 if let Some(bunker) = bunker {
250 git_repo.remove_git_config_item("nostr.nsec", global)?;
251 git_repo.save_git_config_item("nostr.bunker-uri", &bunker.0, global)?;
252 git_repo.save_git_config_item("nostr.bunker-app-key", &bunker.1, global)?;
253 }
254 if let Some(nsec) = nsec {
255 git_repo.save_git_config_item("nostr.nsec", nsec, global)?;
256 git_repo.remove_git_config_item("nostr.bunker-uri", global)?;
257 git_repo.remove_git_config_item("nostr.bunker-app-key", global)?;
258 }
259 git_repo.save_git_config_item("nostr.npub", npub, global)
260}
261
150fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> { 262fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> {
151 decrypt_key( 263 decrypt_key(
152 &git_repo 264 &git_repo
153 .get_git_config_item("nostr.nsec", false) 265 .get_git_config_item("nostr.nsec", None)
154 .context("failed get git config")? 266 .context("failed get git config")?
155 .context("git config item nostr.nsec doesn't exist so cannot decrypt it")?, 267 .context("git config item nostr.nsec doesn't exist so cannot decrypt it")?,
156 password, 268 password,
@@ -158,15 +270,74 @@ fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys
158 .context("failed to decrypt stored nsec key with provided password") 270 .context("failed to decrypt stored nsec key with provided password")
159} 271}
160 272
161fn get_keys_with_git_config_nsec_without_prompts(git_repo: &Repo) -> Result<nostr::Keys> { 273async fn get_nip46_signer_from_uri_and_key(uri: &str, app_key: &str) -> Result<NostrSigner> {
162 let nsec = &git_repo 274 let term = console::Term::stderr();
163 .get_git_config_item("nostr.nsec", false) 275 term.write_line("connecting to remote signer...")?;
164 .context("failed get git config")? 276 let uri = NostrConnectURI::parse(uri)?;
165 .context("git config item nostr.nsec doesn't exist")?; 277 let signer = NostrSigner::nip46(
166 if nsec.contains("ncryptsec") { 278 Nip46Signer::new(
167 bail!("git config item nostr.nsec is an ncryptsec") 279 uri,
280 nostr::Keys::from_str(app_key).context("invalid app key")?,
281 Duration::from_secs(30),
282 None,
283 )
284 .await?,
285 );
286 term.clear_last_lines(1)?;
287 Ok(signer)
288}
289
290async fn get_signer_with_git_config_nsec_or_bunker_without_prompts(
291 git_repo: &Repo,
292) -> Result<NostrSigner> {
293 if let Ok(local_nsec) = &git_repo
294 .get_git_config_item("nostr.nsec", Some(false))
295 .context("failed get local git config")?
296 .context("git local config item nostr.nsec doesn't exist")
297 {
298 if local_nsec.contains("ncryptsec") {
299 bail!("git global config item nostr.nsec is an ncryptsec")
300 }
301 Ok(NostrSigner::Keys(
302 nostr::Keys::from_str(local_nsec).context("invalid nsec parameter")?,
303 ))
304 } else if let Ok((uri, app_key)) = get_git_config_bunker_uri_and_app_key(git_repo, Some(false))
305 {
306 get_nip46_signer_from_uri_and_key(&uri, &app_key).await
307 } else if let Ok(global_nsec) = &git_repo
308 .get_git_config_item("nostr.nsec", Some(true))
309 .context("failed get global git config")?
310 .context("git global config item nostr.nsec doesn't exist")
311 {
312 if global_nsec.contains("ncryptsec") {
313 bail!("git global config item nostr.nsec is an ncryptsec")
314 }
315 Ok(NostrSigner::Keys(
316 nostr::Keys::from_str(global_nsec).context("invalid nsec parameter")?,
317 ))
318 } else if let Ok((uri, app_key)) = get_git_config_bunker_uri_and_app_key(git_repo, Some(true)) {
319 get_nip46_signer_from_uri_and_key(&uri, &app_key).await
320 } else {
321 bail!("cannot get nsec or bunker from git config")
168 } 322 }
169 nostr::Keys::from_str(nsec).context("invalid nsec parameter") 323}
324
325fn get_git_config_bunker_uri_and_app_key(
326 git_repo: &Repo,
327 global: Option<bool>,
328) -> Result<(String, String)> {
329 Ok((
330 git_repo
331 .get_git_config_item("nostr.bunker_url", global)
332 .context("failed get local git config")?
333 .context("git local config item nostr.bunker_url doesn't exist")?
334 .to_string(),
335 git_repo
336 .get_git_config_item("nostr.bunker-app-key", global)
337 .context("failed get local git config")?
338 .context("git local config item nostr.bunker-app-key doesn't exist")?
339 .to_string(),
340 ))
170} 341}
171 342
172async fn fresh_login( 343async fn fresh_login(
@@ -175,50 +346,119 @@ async fn fresh_login(
175 #[cfg(not(test))] client: Option<&Client>, 346 #[cfg(not(test))] client: Option<&Client>,
176 always_save: bool, 347 always_save: bool,
177) -> Result<(NostrSigner, UserRef)> { 348) -> Result<(NostrSigner, UserRef)> {
349 let mut public_key: Option<PublicKey> = None;
178 // prompt for nsec 350 // prompt for nsec
179 let mut prompt = "login with nsec"; 351 let mut prompt = "login with bunker uri / nsec";
180 let keys = loop { 352 let signer = loop {
181 match nostr::Keys::from_str( 353 let input = Interactor::default()
182 &Interactor::default() 354 .input(PromptInputParms::default().with_prompt(prompt))
183 .input(PromptInputParms::default().with_prompt(prompt)) 355 .context("failed to get nsec input from interactor")?;
184 .context("failed to get nsec input from interactor")?, 356 match nostr::Keys::from_str(&input) {
185 ) {
186 Ok(key) => { 357 Ok(key) => {
187 break key; 358 if let Err(error) = save_keys(git_repo, &key, always_save) {
188 } 359 println!("{error}");
189 Err(_) => { 360 }
190 prompt = "invalid nsec. try again with nsec (or hex private key)"; 361 break NostrSigner::Keys(key);
191 } 362 }
363 Err(_) => match NostrConnectURI::parse(&input) {
364 Ok(_) => {
365 let app_key = Keys::generate().secret_key()?.to_secret_hex();
366 match get_nip46_signer_from_uri_and_key(&input, &app_key).await {
367 Ok(signer) => {
368 let pub_key = fetch_public_key(&signer).await?;
369 if let Err(error) =
370 save_bunker(git_repo, &pub_key, &input, &app_key, always_save)
371 {
372 println!("{error}");
373 }
374 public_key = Some(pub_key);
375 break signer;
376 }
377 Err(_) => {
378 prompt = "invalid. try again with nostr address / nsec";
379 }
380 }
381 }
382 Err(_) => {
383 prompt = "invalid. try again with nostr address / nsec";
384 }
385 },
192 } 386 }
193 }; 387 };
388 let public_key = if let Some(public_key) = public_key {
389 public_key
390 } else {
391 signer.public_key().await?
392 };
194 // lookup profile 393 // lookup profile
195 // save keys 394 let user_ref = get_user_details(&public_key, client, git_repo).await?;
196 if let Err(error) = save_keys(git_repo, &keys, always_save) {
197 println!("{error}");
198 }
199 let user_ref = get_user_details(&keys.public_key(), client, git_repo).await?;
200 print_logged_in_as(&user_ref, client.is_none())?; 395 print_logged_in_as(&user_ref, client.is_none())?;
201 Ok((NostrSigner::Keys(keys), user_ref)) 396 Ok((signer, user_ref))
202} 397}
203 398
204fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> { 399fn save_bunker(
205 let store = always_save 400 git_repo: &Repo,
401 public_key: &PublicKey,
402 uri: &str,
403 app_key: &str,
404 always_save: bool,
405) -> Result<()> {
406 if always_save
206 || Interactor::default() 407 || Interactor::default()
207 .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?; 408 .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?
409 {
410 let global = !Interactor::default().confirm(
411 PromptConfirmParms::default()
412 .with_prompt("just for this repository?")
413 .with_default(false),
414 )?;
415 let npub = public_key.to_bech32()?;
416 if let Err(error) = save_to_git_config(
417 git_repo,
418 &npub,
419 &None,
420 &Some((uri.to_string(), app_key.to_string())),
421 global,
422 ) {
423 if global {
424 if Interactor::default().confirm(
425 PromptConfirmParms::default()
426 .with_prompt("save in repository git config?")
427 .with_default(true),
428 )? {
429 save_to_git_config(
430 git_repo,
431 &npub,
432 &None,
433 &Some((uri.to_string(), app_key.to_string())),
434 false,
435 )?;
436 }
437 } else {
438 Err(error)?;
439 }
440 };
441 }
442 Ok(())
443}
208 444
209 let global = !Interactor::default().confirm( 445fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> {
210 PromptConfirmParms::default() 446 if always_save
211 .with_prompt("just for this repository?") 447 || Interactor::default()
212 .with_default(false), 448 .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?
213 )?; 449 {
450 let global = !Interactor::default().confirm(
451 PromptConfirmParms::default()
452 .with_prompt("just for this repository?")
453 .with_default(false),
454 )?;
214 455
215 let encrypt = Interactor::default().confirm( 456 let encrypt = Interactor::default().confirm(
216 PromptConfirmParms::default() 457 PromptConfirmParms::default()
217 .with_prompt("require password?") 458 .with_prompt("require password?")
218 .with_default(false), 459 .with_default(false),
219 )?; 460 )?;
220 461
221 if store {
222 let npub = keys.public_key().to_bech32()?; 462 let npub = keys.public_key().to_bech32()?;
223 let nsec_string = if encrypt { 463 let nsec_string = if encrypt {
224 let password = Interactor::default() 464 let password = Interactor::default()
@@ -233,22 +473,20 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<(
233 keys.secret_key()?.to_bech32()? 473 keys.secret_key()?.to_bech32()?
234 }; 474 };
235 475
236 if let Err(error) = git_repo.save_git_config_item("nostr.nsec", &nsec_string, global) { 476 if let Err(error) =
477 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, global)
478 {
237 if global { 479 if global {
238 println!("failed to edit global git config instead");
239 if Interactor::default().confirm( 480 if Interactor::default().confirm(
240 PromptConfirmParms::default() 481 PromptConfirmParms::default()
241 .with_prompt("save in repository git config?") 482 .with_prompt("save in repository git config?")
242 .with_default(true), 483 .with_default(true),
243 )? { 484 )? {
244 git_repo.save_git_config_item("nostr.nsec", &nsec_string, false)?; 485 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?;
245 git_repo.save_git_config_item("nostr.npub", &npub, false)?;
246 } 486 }
247 } else { 487 } else {
248 bail!(error) 488 Err(error)?;
249 } 489 }
250 } else {
251 git_repo.save_git_config_item("nostr.npub", &npub, global)?;
252 }; 490 };
253 }; 491 };
254 Ok(()) 492 Ok(())
@@ -256,7 +494,7 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<(
256 494
257fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> { 495fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> {
258 git_repo 496 git_repo
259 .get_git_config_item(name, false) 497 .get_git_config_item(name, None)
260 .context("failed get git config")? 498 .context("failed get git config")?
261 .context(format!("git config item {name} doesn't exist")) 499 .context(format!("git config item {name} doesn't exist"))
262} 500}
@@ -350,6 +588,10 @@ async fn get_user_details(
350 println!("searching for profile and relay updates..."); 588 println!("searching for profile and relay updates...");
351 } 589 }
352 let database = SQLiteDatabase::open(if std::env::var("NGITTEST").is_err() { 590 let database = SQLiteDatabase::open(if std::env::var("NGITTEST").is_err() {
591 create_dir_all(get_dirs()?.config_dir()).context(format!(
592 "cannot create cache directory in: {:?}",
593 get_dirs()?.config_dir()
594 ))?;
353 get_dirs()?.config_dir().join("cache.sqlite") 595 get_dirs()?.config_dir().join("cache.sqlite")
354 } else { 596 } else {
355 git_repo.get_path()?.join(".git/test-global-cache.sqlite") 597 git_repo.get_path()?.join(".git/test-global-cache.sqlite")