From f30b1a519771ac49e2b03640e742fdb0a246bd81 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 22:22:27 +0100 Subject: v2.4.0: key stored credentials by certificate fingerprint --- src/bin/client/connection.rs | 34 ++++++++++++++------------ src/bin/client/repl.rs | 57 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 26 deletions(-) (limited to 'src/bin/client') 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( // TOFU: persist the fingerprint on first contact. if !unsafe_mode { - if let Some(fp) = observed { + if let Some(fp) = observed.as_deref() { if database::get_fingerprint(conn, addr).is_none() { - database::save_fingerprint(conn, addr, &fp)?; - for line in first_contact_lines(addr, &fp) { + database::save_fingerprint(conn, addr, fp)?; + for line in first_contact_lines(addr, fp) { println!("{}", line); } } @@ -80,21 +80,25 @@ pub fn connect( elapsed.as_secs_f64() * 1000.0 ); - if let Some((username, password)) = database::get_credentials(conn, addr) { - let auth_msg = format!("AUTH {} {}", username, password); - tls.write_line(&auth_msg)?; - let mut auth_response = String::new(); - tls.read_line(&mut auth_response)?; - if auth_response.trim().starts_with("OK") { - println!("Authenticated as {}", username); - Ok((tls, Some(username))) - } else { + // Credentials are stored per server identity, so any address presenting + // this certificate key can log in with the same stored account. + let identity = observed + .clone() + .or_else(|| database::get_fingerprint(conn, addr)); + if let Some(identity) = identity { + if let Some((username, password)) = database::get_credentials(conn, &identity) { + let auth_msg = format!("AUTH {} {}", username, password); + tls.write_line(&auth_msg)?; + let mut auth_response = String::new(); + tls.read_line(&mut auth_response)?; + if auth_response.trim().starts_with("OK") { + println!("Authenticated as {}", username); + return Ok((tls, Some(username))); + } println!("Authentication failed: {}", auth_response.trim()); - Ok((tls, None)) } - } else { - Ok((tls, None)) } + Ok((tls, None)) } /// 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 { Effect::ForgetHost(target) => { self.commit_prompt_line(); match database::forget_host(&self.conn, &target) { - Ok(_) => println!("Cleared trust and stored credentials for {}.", target), + Ok(outcome) if outcome.credentials_removed => println!( + "Cleared trust and stored credentials for {}.", + target + ), + Ok(outcome) => { + println!("Cleared trust for {}.", target); + if !outcome.siblings.is_empty() { + println!( + "Stored credentials kept: this server is still reachable as {}.", + outcome.siblings.join(", ") + ); + } + } Err(error) => println!("Failed to forget host: {}", error), } } @@ -506,12 +518,17 @@ impl ClientState { let response = response.trim(); if response.starts_with("OK") { - if let Some(addr) = &self.addr { - if let Err(error) = - database::save_credentials(&self.conn, addr, &username, &hashed) - { - println!("Failed to save credentials: {}", error); + match self.trusted_identity() { + Some(identity) => { + if let Err(error) = + database::save_credentials(&self.conn, &identity, &username, &hashed) + { + println!("Failed to save credentials: {}", error); + } } + None => println!( + "Credentials not saved: no verified certificate identity for this host." + ), } self.core.set_logged_in(Some(username.clone())); println!("Logged in as {}", username); @@ -567,14 +584,27 @@ impl ClientState { return; } let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); - if let Some(addr) = &self.addr { - match database::save_credentials(&self.conn, addr, &username, &hashed) { - Ok(_) => println!("Credentials saved for {}", addr), - Err(error) => println!("Failed to save credentials: {}", error), - } + let Some(identity) = self.trusted_identity() else { + println!( + "No verified certificate for this host. Connect first so its key can be trusted." + ); + return; + }; + match database::save_credentials(&self.conn, &identity, &username, &hashed) { + Ok(_) => println!( + "Credentials saved for server {} (every address with this key).", + short_fingerprint(&identity) + ), + Err(error) => println!("Failed to save credentials: {}", error), } } + /// Certificate identity of the current address, once it has been verified. + fn trusted_identity(&self) -> Option { + let addr = self.addr.as_deref()?; + database::get_fingerprint(&self.conn, addr) + } + fn list_hosts(&mut self) { let hosts = database::get_all_hosts(&self.conn); for line in host_lines(&hosts) { @@ -583,6 +613,11 @@ impl ClientState { } } +/// First few hex characters of a fingerprint, enough to tell servers apart. +fn short_fingerprint(fingerprint: &str) -> String { + fingerprint.chars().take(16).collect() +} + fn style_roulette_number(text: &str, number: u8) -> String { match roulette_color(number) { "red" => text.red().to_string(), -- cgit v1.2.3