summaryrefslogtreecommitdiffstats
path: root/src/client/database.rs
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/client/database.rs
parentformatting (diff)
downloadzagan-bet-f30b1a519771ac49e2b03640e742fdb0a246bd81.tar.gz
zagan-bet-f30b1a519771ac49e2b03640e742fdb0a246bd81.zip
v2.4.0: key stored credentials by certificate fingerprint
Diffstat (limited to 'src/client/database.rs')
-rw-r--r--src/client/database.rs116
1 files changed, 102 insertions, 14 deletions
diff --git a/src/client/database.rs b/src/client/database.rs
index e1cedaf..04a271a 100644
--- a/src/client/database.rs
+++ b/src/client/database.rs
@@ -1,5 +1,15 @@
1use rusqlite::Connection; 1use rusqlite::Connection;
2 2
3/// What `forget_host` did with the stored credential.
4#[derive(Debug, Default, PartialEq, Eq)]
5pub struct ForgetOutcome {
6 /// Addresses that still present the same certificate key.
7 pub siblings: Vec<String>,
8 /// True when no address for that server remained, so the credential was
9 /// dropped with it.
10 pub credentials_removed: bool,
11}
12
3pub fn init_db(conn: &Connection) { 13pub fn init_db(conn: &Connection) {
4 conn.execute( 14 conn.execute(
5 "CREATE TABLE IF NOT EXISTS hosts ( 15 "CREATE TABLE IF NOT EXISTS hosts (
@@ -12,9 +22,15 @@ pub fn init_db(conn: &Connection) {
12 .unwrap(); 22 .unwrap();
13 // Migration for databases created before TOFU trust. 23 // Migration for databases created before TOFU trust.
14 let _ = conn.execute("ALTER TABLE hosts ADD COLUMN fingerprint TEXT", []); 24 let _ = conn.execute("ALTER TABLE hosts ADD COLUMN fingerprint TEXT", []);
25
26 // Credentials belong to a server identity (the certificate key), not to a
27 // single address, so every address that presents the same key shares them.
28 if has_column(conn, "credentials", "host") {
29 migrate_credentials_to_fingerprints(conn);
30 }
15 conn.execute( 31 conn.execute(
16 "CREATE TABLE IF NOT EXISTS credentials ( 32 "CREATE TABLE IF NOT EXISTS credentials (
17 host TEXT PRIMARY KEY, 33 fingerprint TEXT PRIMARY KEY,
18 username TEXT NOT NULL, 34 username TEXT NOT NULL,
19 password TEXT NOT NULL 35 password TEXT NOT NULL
20 )", 36 )",
@@ -23,6 +39,42 @@ pub fn init_db(conn: &Connection) {
23 .unwrap(); 39 .unwrap();
24} 40}
25 41
42fn has_column(conn: &Connection, table: &str, column: &str) -> bool {
43 let Ok(mut statement) = conn.prepare(&format!("PRAGMA table_info({table})")) else {
44 return false;
45 };
46 let Ok(rows) = statement.query_map([], |row| row.get::<_, String>(1)) else {
47 return false;
48 };
49 rows.flatten().any(|name| name == column)
50}
51
52/// Re-key host-bound credentials by the fingerprint of their host.
53///
54/// Rows whose host was already forgotten have no identity to attach to and are
55/// dropped; those addresses have to re-authenticate once.
56fn migrate_credentials_to_fingerprints(conn: &Connection) {
57 let tx = conn.unchecked_transaction().unwrap();
58 tx.execute_batch(
59 "DROP TABLE IF EXISTS credentials_by_identity;
60 CREATE TABLE credentials_by_identity (
61 fingerprint TEXT PRIMARY KEY,
62 username TEXT NOT NULL,
63 password TEXT NOT NULL
64 );
65 INSERT OR REPLACE INTO credentials_by_identity (fingerprint, username, password)
66 SELECT h.fingerprint, c.username, c.password
67 FROM credentials c
68 JOIN hosts h ON h.host = c.host
69 WHERE h.fingerprint IS NOT NULL
70 ORDER BY h.last_used ASC, h.host ASC;
71 DROP TABLE credentials;
72 ALTER TABLE credentials_by_identity RENAME TO credentials;",
73 )
74 .unwrap();
75 tx.commit().unwrap();
76}
77
26pub fn get_last_host(conn: &Connection) -> Option<String> { 78pub fn get_last_host(conn: &Connection) -> Option<String> {
27 let mut stmt = conn 79 let mut stmt = conn
28 .prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1") 80 .prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1")
@@ -44,24 +96,27 @@ pub fn save_host(conn: &Connection, host: &str) {
44 .unwrap(); 96 .unwrap();
45} 97}
46 98
99/// Store credentials for a server identity (certificate fingerprint).
47pub fn save_credentials( 100pub fn save_credentials(
48 conn: &Connection, 101 conn: &Connection,
49 host: &str, 102 fingerprint: &str,
50 username: &str, 103 username: &str,
51 password: &str, 104 password: &str,
52) -> Result<(), Box<dyn std::error::Error>> { 105) -> Result<(), Box<dyn std::error::Error>> {
53 conn.execute( 106 conn.execute(
54 "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)", 107 "INSERT OR REPLACE INTO credentials (fingerprint, username, password)
55 (host, username, password), 108 VALUES (?1, ?2, ?3)",
109 (fingerprint, username, password),
56 )?; 110 )?;
57 Ok(()) 111 Ok(())
58} 112}
59 113
60pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { 114/// Credentials for a server identity, regardless of which address reached it.
115pub fn get_credentials(conn: &Connection, fingerprint: &str) -> Option<(String, String)> {
61 let mut stmt = conn 116 let mut stmt = conn
62 .prepare("SELECT username, password FROM credentials WHERE host = ?1") 117 .prepare("SELECT username, password FROM credentials WHERE fingerprint = ?1")
63 .unwrap(); 118 .unwrap();
64 let mut rows = stmt.query([host]).unwrap(); 119 let mut rows = stmt.query([fingerprint]).unwrap();
65 if let Ok(Some(row)) = rows.next() { 120 if let Ok(Some(row)) = rows.next() {
66 let username: String = row.get(0).unwrap(); 121 let username: String = row.get(0).unwrap();
67 let password: String = row.get(1).unwrap(); 122 let password: String = row.get(1).unwrap();
@@ -98,11 +153,43 @@ pub fn save_fingerprint(
98 Ok(()) 153 Ok(())
99} 154}
100 155
101/// Remove all trust and credentials for a host ("forget this host"). 156/// Addresses that present `fingerprint`, most recently used first.
102pub fn forget_host(conn: &Connection, host: &str) -> Result<(), rusqlite::Error> { 157pub fn hosts_with_fingerprint(conn: &Connection, fingerprint: &str) -> Vec<String> {
158 let mut stmt = conn
159 .prepare("SELECT host FROM hosts WHERE fingerprint = ?1 ORDER BY last_used DESC")
160 .unwrap();
161 let rows = stmt.query_map([fingerprint], |row| row.get::<_, String>(0)).unwrap();
162 rows.filter_map(|row| row.ok()).collect()
163}
164
165/// Remove trust for one address ("forget this host").
166///
167/// Credentials are shared by every address with the same certificate key, so
168/// they are only dropped once the last such address is forgotten.
169pub fn forget_host(conn: &Connection, host: &str) -> Result<ForgetOutcome, rusqlite::Error> {
170 let fingerprint = get_fingerprint(conn, host);
103 conn.execute("DELETE FROM hosts WHERE host = ?1", [host])?; 171 conn.execute("DELETE FROM hosts WHERE host = ?1", [host])?;
104 conn.execute("DELETE FROM credentials WHERE host = ?1", [host])?; 172
105 Ok(()) 173 let Some(fingerprint) = fingerprint else {
174 return Ok(ForgetOutcome::default());
175 };
176
177 let siblings = hosts_with_fingerprint(conn, &fingerprint);
178 if siblings.is_empty() {
179 conn.execute(
180 "DELETE FROM credentials WHERE fingerprint = ?1",
181 [&fingerprint],
182 )?;
183 return Ok(ForgetOutcome {
184 siblings,
185 credentials_removed: true,
186 });
187 }
188
189 Ok(ForgetOutcome {
190 siblings,
191 credentials_removed: false,
192 })
106} 193}
107 194
108/// List all remembered hosts with their fingerprints. 195/// List all remembered hosts with their fingerprints.
@@ -110,8 +197,9 @@ pub fn get_all_hosts(conn: &Connection) -> Vec<(String, Option<String>)> {
110 let mut stmt = conn 197 let mut stmt = conn
111 .prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC") 198 .prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC")
112 .unwrap(); 199 .unwrap();
113 let rows = stmt 200 let rows = stmt.query_map([], |row| {
114 .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) 201 Ok((row.get(0)?, row.get(1)?))
115 .unwrap(); 202 })
203 .unwrap();
116 rows.filter_map(|r| r.ok()).collect() 204 rows.filter_map(|r| r.ok()).collect()
117} 205}