1use super::error::Error;
23use super::{now_ms, write_atomic};
24use serde::{Deserialize, Serialize};
25use std::collections::HashSet;
26use std::path::{Path, PathBuf};
27
28pub const MAX_UNDECIDED: usize = 200;
31const SAVE_EVERY_MS: u64 = 5 * 60 * 1000;
33const SAVE_REFUSED_MS: u64 = 2000;
35
36pub const PROOF_CERTIFICATE: &str = "certificate";
37pub const PROOF_CALLBACK: &str = "callback";
38
39pub fn normalize(fingerprint: &str) -> String {
41 fingerprint.trim().to_ascii_lowercase()
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "lowercase")]
46pub enum TrustState {
47 Allow,
48 Ask,
49 Deny,
50}
51
52impl TrustState {
53 pub fn parse(text: &str) -> Option<TrustState> {
54 match text {
55 "allow" => Some(TrustState::Allow),
56 "ask" => Some(TrustState::Ask),
57 "deny" => Some(TrustState::Deny),
58 _ => None,
59 }
60 }
61
62 pub fn as_str(self) -> &'static str {
63 match self {
64 TrustState::Allow => "allow",
65 TrustState::Ask => "ask",
66 TrustState::Deny => "deny",
67 }
68 }
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize)]
73pub struct TrustEntry {
74 pub fingerprint: String,
75 #[serde(default)]
77 pub label: String,
78 #[serde(default)]
80 pub alias_seen: String,
81 #[serde(default)]
84 pub alias_claimed: String,
85 #[serde(default)]
86 pub model: Option<String>,
87 #[serde(default)]
88 pub kind: Option<String>,
89 #[serde(default)]
90 pub first_seen_ms: u64,
91 #[serde(default)]
92 pub last_seen_ms: u64,
93 pub state: TrustState,
94 #[serde(default)]
97 pub proof: Option<String>,
98 #[serde(default)]
100 pub decided: bool,
101 #[serde(default)]
103 pub refused: u64,
104}
105
106impl TrustEntry {
107 fn blank(fingerprint: String, state: TrustState, now: u64) -> TrustEntry {
108 TrustEntry {
109 fingerprint,
110 label: String::new(),
111 alias_seen: String::new(),
112 alias_claimed: String::new(),
113 model: None,
114 kind: None,
115 first_seen_ms: now,
116 last_seen_ms: now,
117 state,
118 proof: None,
119 decided: false,
120 refused: 0,
121 }
122 }
123
124 fn evictable(&self) -> bool {
126 !self.decided
129 && self.label.is_empty()
130 && self.state != TrustState::Allow
131 && self.proof.is_none()
132 }
133}
134
135#[derive(Serialize, Deserialize)]
136struct Stored {
137 #[serde(default = "one")]
138 version: u32,
139 #[serde(default = "ask")]
140 default_state: TrustState,
141 #[serde(default)]
142 devices: Vec<TrustEntry>,
143}
144
145fn one() -> u32 {
146 1
147}
148
149fn ask() -> TrustState {
150 TrustState::Ask
151}
152
153fn clean(text: &str, max: usize) -> String {
155 text.chars().filter(|c| !c.is_control()).take(max).collect()
156}
157
158fn valid_fingerprint(fingerprint: &str) -> bool {
159 !fingerprint.is_empty() && fingerprint.len() <= 128
160}
161
162pub(crate) struct TrustStore {
163 path: PathBuf,
164 default_state: TrustState,
165 devices: Vec<TrustEntry>,
166 saved_ms: u64,
167 hold: bool,
172}
173
174impl TrustStore {
175 pub fn load(trust_path: &Path) -> TrustStore {
188 let path = trust_path.to_path_buf();
189 let directory: PathBuf = path
190 .parent()
191 .map(Path::to_path_buf)
192 .unwrap_or_else(|| PathBuf::from("."));
193 let bad = path.with_extension("json.bad");
194 let mut store = TrustStore {
195 path: path.clone(),
196 default_state: TrustState::Ask,
197 devices: Vec::new(),
198 saved_ms: now_ms(),
199 hold: false,
200 };
201 match std::fs::read(&path) {
202 Ok(bytes) => match serde_json::from_slice::<Stored>(&bytes) {
203 Ok(stored) => store.adopt(stored),
204 Err(_) => {
205 let _ = std::fs::rename(&path, &bad);
206 let _ = store.save();
207 }
208 },
209 Err(e) if e.kind() == std::io::ErrorKind::NotFound && !bad.exists() => {
210 let now = now_ms();
211 let mut seen = HashSet::new();
212 for (fingerprint, label) in super::load_known(&directory.join("known-devices.json"))
213 {
214 let fingerprint = normalize(&fingerprint);
215 if !valid_fingerprint(&fingerprint) || !seen.insert(fingerprint.clone()) {
216 continue;
217 }
218 let mut entry = TrustEntry::blank(fingerprint, TrustState::Ask, now);
219 entry.label = clean(&label, 80).trim().to_string();
220 store.devices.push(entry);
221 }
222 if !store.devices.is_empty() {
223 let _ = store.save();
224 }
225 }
226 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
227 Err(_) => store.hold = true,
228 }
229 store
230 }
231
232 fn adopt(&mut self, stored: Stored) {
234 self.default_state = match stored.default_state {
235 TrustState::Deny => TrustState::Deny,
236 _ => TrustState::Ask,
237 };
238 let mut seen = HashSet::new();
239 self.devices = stored
240 .devices
241 .into_iter()
242 .map(|mut d| {
243 d.fingerprint = normalize(&d.fingerprint);
244 d
245 })
246 .filter(|d| valid_fingerprint(&d.fingerprint))
247 .filter(|d| seen.insert(d.fingerprint.clone()))
248 .collect();
249 }
250
251 fn lift_hold(&mut self) -> Result<(), Error> {
260 if !self.hold {
261 return Ok(());
262 }
263 match std::fs::read(&self.path) {
264 Ok(bytes) => match serde_json::from_slice::<Stored>(&bytes) {
265 Ok(stored) => {
266 let sighted = std::mem::take(&mut self.devices);
267 self.adopt(stored);
268 for mut entry in sighted {
269 if self
270 .devices
271 .iter()
272 .any(|d| d.fingerprint == entry.fingerprint)
273 {
274 continue;
275 }
276 if !entry.decided {
277 entry.state = self.default_state;
278 }
279 self.devices.push(entry);
280 }
281 }
282 Err(_) => {
283 let _ = std::fs::rename(&self.path, self.path.with_extension("json.bad"));
284 }
285 },
286 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
287 Err(e) => {
288 return Err(Error::TrustStore {
289 detail: format!(
290 "the stored list could not be read, so nothing was changed: {e}"
291 ),
292 });
293 }
294 }
295 self.hold = false;
296 Ok(())
297 }
298
299 fn save(&mut self) -> std::io::Result<()> {
301 if self.hold {
302 return Err(std::io::Error::other(
303 "the stored list could not be read; it is not overwritten",
304 ));
305 }
306 let stored = Stored {
307 version: 1,
308 default_state: self.default_state,
309 devices: self.devices.clone(),
310 };
311 let bytes = serde_json::to_vec_pretty(&stored).map_err(std::io::Error::other)?;
312 write_atomic(&self.path, &bytes)?;
313 self.saved_ms = now_ms();
314 Ok(())
315 }
316
317 fn autosave(&mut self) {
319 if !self.hold {
320 let _ = self.save();
321 }
322 }
323
324 fn explicit_save(&mut self) -> Result<(), Error> {
325 self.save().map_err(|e| Error::TrustStore {
326 detail: e.to_string(),
327 })
328 }
329
330 pub fn default_state(&self) -> TrustState {
331 self.default_state
332 }
333
334 pub fn state_of(&self, fingerprint: &str) -> TrustState {
337 let fingerprint = normalize(fingerprint);
338 if !valid_fingerprint(&fingerprint) {
339 return match self.default_state {
340 TrustState::Deny => TrustState::Deny,
341 _ => TrustState::Ask,
342 };
343 }
344 self.devices
345 .iter()
346 .find(|d| d.fingerprint == fingerprint)
347 .map(|d| d.state)
348 .unwrap_or(self.default_state)
349 }
350
351 pub fn observe(
356 &mut self,
357 fingerprint: &str,
358 alias: &str,
359 model: Option<&str>,
360 kind: Option<&str>,
361 proof: Option<&str>,
362 ) {
363 let fingerprint = normalize(fingerprint);
364 if !valid_fingerprint(&fingerprint) {
365 return;
366 }
367 let now = now_ms();
368 let alias = clean(alias, 80);
369 let model = model.map(|m| clean(m, 60)).filter(|m| !m.is_empty());
370 let kind = kind.map(|k| clean(k, 20)).filter(|k| !k.is_empty());
371 let stale = now.saturating_sub(self.saved_ms) >= SAVE_EVERY_MS;
372 let save = match self
373 .devices
374 .iter_mut()
375 .find(|d| d.fingerprint == fingerprint)
376 {
377 Some(entry) => {
378 entry.last_seen_ms = now;
379 let mut changed = false;
380 if entry.proof.is_some() && proof.is_none() {
381 changed |= entry.alias_claimed != alias;
382 entry.alias_claimed = alias;
383 } else {
384 changed |=
385 entry.alias_seen != alias || entry.model != model || entry.kind != kind;
386 entry.alias_seen = alias;
387 entry.model = model;
388 entry.kind = kind;
389 }
390 if let Some(proof) = proof {
391 let keep = entry.proof.as_deref() == Some(PROOF_CERTIFICATE);
393 if !keep && entry.proof.as_deref() != Some(proof) {
394 entry.proof = Some(proof.to_string());
395 changed = true;
396 }
397 }
398 changed || stale
399 }
400 None => {
401 self.make_room();
402 let mut entry = TrustEntry::blank(fingerprint, self.default_state, now);
403 entry.alias_seen = alias;
404 entry.model = model;
405 entry.kind = kind;
406 entry.proof = proof.map(str::to_string);
407 self.devices.push(entry);
408 true
409 }
410 };
411 if save {
412 self.autosave();
413 }
414 }
415
416 pub fn certificate_proven(&self, fingerprint: &str) -> bool {
419 let fingerprint = normalize(fingerprint);
420 self.devices
421 .iter()
422 .any(|d| d.fingerprint == fingerprint && d.proof.as_deref() == Some(PROOF_CERTIFICATE))
423 }
424
425 pub fn mark_callback(&mut self, fingerprint: &str) {
427 let fingerprint = normalize(fingerprint);
428 if let Some(entry) = self
429 .devices
430 .iter_mut()
431 .find(|d| d.fingerprint == fingerprint)
432 && entry.proof.is_none()
433 {
434 entry.proof = Some(PROOF_CALLBACK.to_string());
435 self.autosave();
436 }
437 }
438
439 fn make_room(&mut self) {
442 while self.devices.iter().filter(|d| d.evictable()).count() >= MAX_UNDECIDED {
443 let oldest = self
444 .devices
445 .iter()
446 .enumerate()
447 .filter(|(_, d)| d.evictable())
448 .min_by_key(|(_, d)| d.last_seen_ms)
449 .map(|(index, _)| index);
450 match oldest {
451 Some(index) => {
452 self.devices.remove(index);
453 }
454 None => break,
455 }
456 }
457 }
458
459 pub fn refuse(&mut self, fingerprint: &str) {
461 let fingerprint = normalize(fingerprint);
462 let due = now_ms().saturating_sub(self.saved_ms) >= SAVE_REFUSED_MS;
463 if let Some(entry) = self
464 .devices
465 .iter_mut()
466 .find(|d| d.fingerprint == fingerprint)
467 {
468 entry.refused = entry.refused.saturating_add(1);
469 if due {
470 self.autosave();
471 }
472 }
473 }
474
475 pub fn set_state(&mut self, fingerprint: &str, state: TrustState) -> Result<(), Error> {
476 let fingerprint = normalize(fingerprint);
477 if !valid_fingerprint(&fingerprint) {
478 return Err(Error::InvalidFingerprint);
479 }
480 self.lift_hold()?;
481 let index = match self
482 .devices
483 .iter()
484 .position(|d| d.fingerprint == fingerprint)
485 {
486 Some(index) => index,
487 None => {
488 self.devices
489 .push(TrustEntry::blank(fingerprint, state, now_ms()));
490 self.devices.len() - 1
491 }
492 };
493 self.devices[index].state = state;
494 self.devices[index].decided = true;
495 self.explicit_save()
496 }
497
498 pub fn set_label(&mut self, fingerprint: &str, label: &str) -> Result<(), Error> {
499 self.lift_hold()?;
500 let fingerprint = normalize(fingerprint);
501 let entry = self
502 .devices
503 .iter_mut()
504 .find(|d| d.fingerprint == fingerprint)
505 .ok_or(Error::NotListed)?;
506 entry.label = clean(label, 80).trim().to_string();
507 self.explicit_save()
508 }
509
510 pub fn forget(&mut self, fingerprint: &str) -> Result<(), Error> {
511 self.lift_hold()?;
512 let fingerprint = normalize(fingerprint);
513 let before = self.devices.len();
514 self.devices.retain(|d| d.fingerprint != fingerprint);
515 if self.devices.len() == before {
516 return Ok(());
517 }
518 self.explicit_save()
519 }
520
521 pub fn set_default(&mut self, state: TrustState) -> Result<(), Error> {
523 if state == TrustState::Allow {
524 return Err(Error::AllowNotDefault);
525 }
526 self.lift_hold()?;
527 self.default_state = state;
528 self.explicit_save()
529 }
530
531 pub fn list(&self) -> Vec<TrustEntry> {
533 let mut list = self.devices.clone();
534 list.sort_by_key(|a| std::cmp::Reverse(a.last_seen_ms));
535 list
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 fn store() -> (TrustStore, PathBuf) {
544 let dir = std::env::temp_dir().join(format!(
545 "rightkit-send-trust-{}",
546 crate::proto::random_hex(4)
547 ));
548 let path = dir.join("nearby_devices.json");
549 (TrustStore::load(&path), dir)
550 }
551
552 #[test]
555 fn a_change_after_an_unreadable_list_never_writes_a_partial_one() {
556 let (mut first, dir) = store();
557 let path = dir.join("nearby_devices.json");
558 let denied = "d".repeat(64);
559 let sighted = "5".repeat(64);
560 let allowed = "a".repeat(64);
561 first.set_default(TrustState::Deny).unwrap();
562 first.set_state(&denied, TrustState::Deny).unwrap();
563 let on_disk = std::fs::read(&path).unwrap();
564
565 let aside = dir.join("aside.json");
567 std::fs::rename(&path, &aside).unwrap();
568 std::fs::create_dir(&path).unwrap();
569 let mut held = TrustStore::load(&path);
570 held.observe(&sighted, "Tablet", None, None, None);
571 let error = held.set_state(&allowed, TrustState::Allow).unwrap_err();
572 assert!(matches!(error, Error::TrustStore { .. }), "{error}");
573 assert!(held.set_default(TrustState::Ask).is_err());
574 assert!(held.forget(&denied).is_err());
575 assert!(
576 path.is_dir(),
577 "nothing was written over the unreadable list"
578 );
579 assert_eq!(
580 held.state_of(&allowed),
581 TrustState::Ask,
582 "the refused change was not kept"
583 );
584
585 std::fs::remove_dir(&path).unwrap();
587 std::fs::rename(&aside, &path).unwrap();
588 assert_eq!(std::fs::read(&path).unwrap(), on_disk);
589 held.set_state(&allowed, TrustState::Allow).unwrap();
590
591 let reloaded = TrustStore::load(&path);
592 assert_eq!(reloaded.default_state(), TrustState::Deny);
593 assert_eq!(reloaded.state_of(&denied), TrustState::Deny);
594 assert_eq!(reloaded.state_of(&allowed), TrustState::Allow);
595 assert_eq!(reloaded.list().len(), 3);
596 assert_eq!(reloaded.state_of(&sighted), TrustState::Deny);
598 let _ = std::fs::remove_dir_all(&dir);
599 }
600
601 #[test]
602 fn a_certificate_proof_is_never_downgraded_to_a_callback() {
603 let (mut trust, dir) = store();
604 let fingerprint = "a".repeat(64);
605 trust.observe(&fingerprint, "Phone", None, None, Some(PROOF_CERTIFICATE));
606 trust.mark_callback(&fingerprint);
607 trust.observe(&fingerprint, "Phone", None, None, Some(PROOF_CALLBACK));
608 assert!(trust.certificate_proven(&fingerprint));
609 assert_eq!(trust.list()[0].proof.as_deref(), Some(PROOF_CERTIFICATE));
610 let _ = std::fs::remove_dir_all(&dir);
611 }
612
613 #[test]
614 fn a_callback_proof_is_recorded_only_on_an_unproven_record() {
615 let (mut trust, dir) = store();
616 let fingerprint = "e".repeat(64);
617 trust.observe(&fingerprint, "Phone", None, None, None);
618 trust.mark_callback(&fingerprint);
619 assert!(!trust.certificate_proven(&fingerprint));
620 assert_eq!(trust.list()[0].proof.as_deref(), Some(PROOF_CALLBACK));
621 let _ = std::fs::remove_dir_all(&dir);
622 }
623
624 #[test]
625 fn deny_is_the_state_of_a_device_and_new_devices_take_the_default() {
626 let (mut trust, dir) = store();
627 let fingerprint = "b".repeat(64);
628 trust.observe(&fingerprint, "Phone", None, None, None);
629 assert_eq!(trust.state_of(&fingerprint), TrustState::Ask);
630 trust.set_state(&fingerprint, TrustState::Deny).unwrap();
631 assert_eq!(trust.state_of(&fingerprint), TrustState::Deny);
632 assert_eq!(trust.state_of(&"c".repeat(64)), TrustState::Ask);
633 let _ = std::fs::remove_dir_all(&dir);
634 }
635
636 #[test]
637 fn invalid_keys_and_allow_defaults_are_typed_refusals() {
638 let (mut trust, dir) = store();
639 assert_eq!(
640 trust.set_state("", TrustState::Allow),
641 Err(Error::InvalidFingerprint)
642 );
643 assert_eq!(
644 trust.set_default(TrustState::Allow),
645 Err(Error::AllowNotDefault)
646 );
647 assert_eq!(
648 trust.set_label(&"d".repeat(64), "Name"),
649 Err(Error::NotListed)
650 );
651 let _ = std::fs::remove_dir_all(&dir);
652 }
653}