1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
use rusqlite::Connection;
/// What `forget_host` did with the stored credential.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ForgetOutcome {
/// Addresses that still present the same certificate key.
pub siblings: Vec<String>,
/// True when no address for that server remained, so the credential was
/// dropped with it.
pub credentials_removed: bool,
}
pub fn init_db(conn: &Connection) {
conn.execute(
"CREATE TABLE IF NOT EXISTS hosts (
host TEXT PRIMARY KEY,
last_used DATETIME DEFAULT CURRENT_TIMESTAMP,
fingerprint TEXT
)",
[],
)
.unwrap();
// Credentials belong to a server identity (the certificate key), not to a
// single address, so every address that presents the same key shares them.
conn.execute(
"CREATE TABLE IF NOT EXISTS credentials (
fingerprint TEXT PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL
)",
[],
)
.unwrap();
}
pub fn get_last_host(conn: &Connection) -> Option<String> {
let mut stmt = conn
.prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1")
.unwrap();
let mut rows = stmt.query([]).unwrap();
if let Ok(Some(row)) = rows.next() {
row.get(0).ok()
} else {
None
}
}
pub fn save_host(conn: &Connection, host: &str) {
conn.execute(
"INSERT INTO hosts (host) VALUES (?1)
ON CONFLICT(host) DO UPDATE SET last_used = CURRENT_TIMESTAMP",
[host],
)
.unwrap();
}
/// Store credentials for a server identity (certificate fingerprint).
pub fn save_credentials(
conn: &Connection,
fingerprint: &str,
username: &str,
password: &str,
) -> Result<(), Box<dyn std::error::Error>> {
conn.execute(
"INSERT OR REPLACE INTO credentials (fingerprint, username, password)
VALUES (?1, ?2, ?3)",
(fingerprint, username, password),
)?;
Ok(())
}
/// Credentials for a server identity, regardless of which address reached it.
pub fn get_credentials(conn: &Connection, fingerprint: &str) -> Option<(String, String)> {
let mut stmt = conn
.prepare("SELECT username, password FROM credentials WHERE fingerprint = ?1")
.unwrap();
let mut rows = stmt.query([fingerprint]).unwrap();
if let Ok(Some(row)) = rows.next() {
let username: String = row.get(0).unwrap();
let password: String = row.get(1).unwrap();
Some((username, password))
} else {
None
}
}
/// SHA-256 SPKI fingerprint of a host's certificate, if trusted.
pub fn get_fingerprint(conn: &Connection, host: &str) -> Option<String> {
let mut stmt = conn
.prepare("SELECT fingerprint FROM hosts WHERE host = ?1")
.ok()?;
let mut rows = stmt.query([host]).ok()?;
match rows.next() {
Ok(Some(row)) => row.get(0).ok(),
_ => None,
}
}
pub fn save_fingerprint(
conn: &Connection,
host: &str,
fingerprint: &str,
) -> Result<(), rusqlite::Error> {
// Hosts may not exist yet (fingerprint can be saved before first
// successful connect completes save_host).
conn.execute("INSERT OR IGNORE INTO hosts (host) VALUES (?1)", [host])?;
conn.execute(
"UPDATE hosts SET fingerprint = ?2 WHERE host = ?1",
(host, fingerprint),
)?;
Ok(())
}
/// Addresses that present `fingerprint`, most recently used first.
pub fn hosts_with_fingerprint(conn: &Connection, fingerprint: &str) -> Vec<String> {
let mut stmt = conn
.prepare("SELECT host FROM hosts WHERE fingerprint = ?1 ORDER BY last_used DESC")
.unwrap();
let rows = stmt.query_map([fingerprint], |row| row.get::<_, String>(0)).unwrap();
rows.filter_map(|row| row.ok()).collect()
}
/// Remove trust for one address ("forget this host").
///
/// Credentials are shared by every address with the same certificate key, so
/// they are only dropped once the last such address is forgotten.
pub fn forget_host(conn: &Connection, host: &str) -> Result<ForgetOutcome, rusqlite::Error> {
let fingerprint = get_fingerprint(conn, host);
conn.execute("DELETE FROM hosts WHERE host = ?1", [host])?;
let Some(fingerprint) = fingerprint else {
return Ok(ForgetOutcome::default());
};
let siblings = hosts_with_fingerprint(conn, &fingerprint);
if siblings.is_empty() {
conn.execute(
"DELETE FROM credentials WHERE fingerprint = ?1",
[&fingerprint],
)?;
return Ok(ForgetOutcome {
siblings,
credentials_removed: true,
});
}
Ok(ForgetOutcome {
siblings,
credentials_removed: false,
})
}
/// Forget the saved login for a server identity ("forget saved login").
///
/// Trust is untouched: the pinned key stays in `hosts`, so the next connection
/// only asks for a password. Returns whether a credential was actually stored.
pub fn forget_credentials(
conn: &Connection,
fingerprint: &str,
) -> Result<bool, rusqlite::Error> {
let removed = conn.execute(
"DELETE FROM credentials WHERE fingerprint = ?1",
[fingerprint],
)?;
Ok(removed > 0)
}
/// List all remembered hosts with their fingerprints.
pub fn get_all_hosts(conn: &Connection) -> Vec<(String, Option<String>)> {
let mut stmt = conn
.prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC")
.unwrap();
let rows = stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?))
})
.unwrap();
rows.filter_map(|r| r.ok()).collect()
}
|