diff options
Diffstat (limited to 'src/bin/client')
| -rw-r--r-- | src/bin/client/connection.rs | 34 | ||||
| -rw-r--r-- | src/bin/client/repl.rs | 57 |
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. | ||
| 617 | fn short_fingerprint(fingerprint: &str) -> String { | ||
| 618 | fingerprint.chars().take(16).collect() | ||
| 619 | } | ||
| 620 | |||
| 586 | fn style_roulette_number(text: &str, number: u8) -> String { | 621 | fn 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(), |
