summaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-29 22:22:27 +0100
committersalmonik <salmonik@tuta.io>2026-10-29 22:22:27 +0100
commitf30b1a519771ac49e2b03640e742fdb0a246bd81 (patch)
treeb9a78039ba55a364e73df5dc174320dc8222f5c3 /src/bin
parentformatting (diff)
downloadzagan-bet-f30b1a519771ac49e2b03640e742fdb0a246bd81.tar.gz
zagan-bet-f30b1a519771ac49e2b03640e742fdb0a246bd81.zip
v2.4.0: key stored credentials by certificate fingerprint
Diffstat (limited to '')
-rw-r--r--src/bin/client/connection.rs34
-rw-r--r--src/bin/client/repl.rs57
2 files changed, 65 insertions, 26 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs
index 44cfea9..ec5642c 100644
--- a/src/bin/client/connection.rs
+++ b/src/bin/client/connection.rs
@@ -56,10 +56,10 @@ pub fn connect(
56 56
57 // TOFU: persist the fingerprint on first contact. 57 // TOFU: persist the fingerprint on first contact.
58 if !unsafe_mode { 58 if !unsafe_mode {
59 if let Some(fp) = observed { 59 if let Some(fp) = observed.as_deref() {
60 if database::get_fingerprint(conn, addr).is_none() { 60 if database::get_fingerprint(conn, addr).is_none() {
61 database::save_fingerprint(conn, addr, &fp)?; 61 database::save_fingerprint(conn, addr, fp)?;
62 for line in first_contact_lines(addr, &fp) { 62 for line in first_contact_lines(addr, fp) {
63 println!("{}", line); 63 println!("{}", line);
64 } 64 }
65 } 65 }
@@ -80,21 +80,25 @@ pub fn connect(
80 elapsed.as_secs_f64() * 1000.0 80 elapsed.as_secs_f64() * 1000.0
81 ); 81 );
82 82
83 if let Some((username, password)) = database::get_credentials(conn, addr) { 83 // Credentials are stored per server identity, so any address presenting
84 let auth_msg = format!("AUTH {} {}", username, password); 84 // this certificate key can log in with the same stored account.
85 tls.write_line(&auth_msg)?; 85 let identity = observed
86 let mut auth_response = String::new(); 86 .clone()
87 tls.read_line(&mut auth_response)?; 87 .or_else(|| database::get_fingerprint(conn, addr));
88 if auth_response.trim().starts_with("OK") { 88 if let Some(identity) = identity {
89 println!("Authenticated as {}", username); 89 if let Some((username, password)) = database::get_credentials(conn, &identity) {
90 Ok((tls, Some(username))) 90 let auth_msg = format!("AUTH {} {}", username, password);
91 } else { 91 tls.write_line(&auth_msg)?;
92 let mut auth_response = String::new();
93 tls.read_line(&mut auth_response)?;
94 if auth_response.trim().starts_with("OK") {
95 println!("Authenticated as {}", username);
96 return Ok((tls, Some(username)));
97 }
92 println!("Authentication failed: {}", auth_response.trim()); 98 println!("Authentication failed: {}", auth_response.trim());
93 Ok((tls, None))
94 } 99 }
95 } else {
96 Ok((tls, None))
97 } 100 }
101 Ok((tls, None))
98} 102}
99 103
100/// Wrapper so TLS handshake failures surface as errors with their message 104/// Wrapper so TLS handshake failures surface as errors with their message
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs
index 9af4ea9..d973596 100644
--- a/src/bin/client/repl.rs
+++ b/src/bin/client/repl.rs
@@ -245,7 +245,19 @@ impl ClientState {
245 Effect::ForgetHost(target) => { 245 Effect::ForgetHost(target) => {
246 self.commit_prompt_line(); 246 self.commit_prompt_line();
247 match database::forget_host(&self.conn, &target) { 247 match database::forget_host(&self.conn, &target) {
248 Ok(_) => println!("Cleared trust and stored credentials for {}.", target), 248 Ok(outcome) if outcome.credentials_removed => println!(
249 "Cleared trust and stored credentials for {}.",
250 target
251 ),
252 Ok(outcome) => {
253 println!("Cleared trust for {}.", target);
254 if !outcome.siblings.is_empty() {
255 println!(
256 "Stored credentials kept: this server is still reachable as {}.",
257 outcome.siblings.join(", ")
258 );
259 }
260 }
249 Err(error) => println!("Failed to forget host: {}", error), 261 Err(error) => println!("Failed to forget host: {}", error),
250 } 262 }
251 } 263 }
@@ -506,12 +518,17 @@ impl ClientState {
506 let response = response.trim(); 518 let response = response.trim();
507 519
508 if response.starts_with("OK") { 520 if response.starts_with("OK") {
509 if let Some(addr) = &self.addr { 521 match self.trusted_identity() {
510 if let Err(error) = 522 Some(identity) => {
511 database::save_credentials(&self.conn, addr, &username, &hashed) 523 if let Err(error) =
512 { 524 database::save_credentials(&self.conn, &identity, &username, &hashed)
513 println!("Failed to save credentials: {}", error); 525 {
526 println!("Failed to save credentials: {}", error);
527 }
514 } 528 }
529 None => println!(
530 "Credentials not saved: no verified certificate identity for this host."
531 ),
515 } 532 }
516 self.core.set_logged_in(Some(username.clone())); 533 self.core.set_logged_in(Some(username.clone()));
517 println!("Logged in as {}", username); 534 println!("Logged in as {}", username);
@@ -567,14 +584,27 @@ impl ClientState {
567 return; 584 return;
568 } 585 }
569 let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); 586 let hashed = format!("{:x}", Sha256::digest(password.as_bytes()));
570 if let Some(addr) = &self.addr { 587 let Some(identity) = self.trusted_identity() else {
571 match database::save_credentials(&self.conn, addr, &username, &hashed) { 588 println!(
572 Ok(_) => println!("Credentials saved for {}", addr), 589 "No verified certificate for this host. Connect first so its key can be trusted."
573 Err(error) => println!("Failed to save credentials: {}", error), 590 );
574 } 591 return;
592 };
593 match database::save_credentials(&self.conn, &identity, &username, &hashed) {
594 Ok(_) => println!(
595 "Credentials saved for server {} (every address with this key).",
596 short_fingerprint(&identity)
597 ),
598 Err(error) => println!("Failed to save credentials: {}", error),
575 } 599 }
576 } 600 }
577 601
602 /// Certificate identity of the current address, once it has been verified.
603 fn trusted_identity(&self) -> Option<String> {
604 let addr = self.addr.as_deref()?;
605 database::get_fingerprint(&self.conn, addr)
606 }
607
578 fn list_hosts(&mut self) { 608 fn list_hosts(&mut self) {
579 let hosts = database::get_all_hosts(&self.conn); 609 let hosts = database::get_all_hosts(&self.conn);
580 for line in host_lines(&hosts) { 610 for line in host_lines(&hosts) {
@@ -583,6 +613,11 @@ impl ClientState {
583 } 613 }
584} 614}
585 615
616/// First few hex characters of a fingerprint, enough to tell servers apart.
617fn short_fingerprint(fingerprint: &str) -> String {
618 fingerprint.chars().take(16).collect()
619}
620
586fn style_roulette_number(text: &str, number: u8) -> String { 621fn style_roulette_number(text: &str, number: u8) -> String {
587 match roulette_color(number) { 622 match roulette_color(number) {
588 "red" => text.red().to_string(), 623 "red" => text.red().to_string(),