Skip to main content

rightkit_send/
trust.rs

1//! Per-device trust for nearby sharing: every device seen on the network or in
2//! a request has one record, and the owner sets it to Allow, Ask or Deny.
3//!
4//! * Allow: a request is accepted without a card, files and text alike, but
5//!   only from a sender that proves a key (`receive::prepare_upload`): by the
6//!   client certificate it presents (`proof = "certificate"`, identity), or, for
7//!   senders that present none, by answering a connect-back handshake at the
8//!   address it calls from (`proof = "callback"`). A callback is relayable by
9//!   anyone on the network while the real device is online, so it is a
10//!   convenience tier only: it skips the card, but the sender stays unproven
11//!   (files kept apart, no clipboard, size caps). Without proof the request is
12//!   asked about.
13//! * Ask: every request raises a card, text included.
14//! * Deny: a request is answered 403 at once and counted in `refused`.
15//!
16//! The records live in the trust store file the host names (`Config::trust_path`).
17//! The legacy `known-devices.json` next to it is read once, when that file is absent.
18//! Fingerprints (always lowercase, see `normalize`) are the policy key; the
19//! alias, model and kind are only what the device claimed, and the owner's own
20//! name for it is `label`.
21
22use super::error::Error;
23use super::{now_ms, write_atomic};
24use serde::{Deserialize, Serialize};
25use std::collections::HashSet;
26use std::path::{Path, PathBuf};
27
28/// Unlabelled records the owner never decided on, kept at most; past this the
29/// oldest makes room. Allow, Deny and labelled records are never dropped.
30pub const MAX_UNDECIDED: usize = 200;
31/// How often an unchanged device's `last_seen_ms` is written to disk.
32const SAVE_EVERY_MS: u64 = 5 * 60 * 1000;
33/// How often a refusal count is written to disk.
34const SAVE_REFUSED_MS: u64 = 2000;
35
36pub const PROOF_CERTIFICATE: &str = "certificate";
37pub const PROOF_CALLBACK: &str = "callback";
38
39/// The one spelling of a fingerprint used as a key everywhere.
40pub 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/// What the owner decided about one device, and what it last claimed to be.
72#[derive(Clone, Debug, Serialize, Deserialize)]
73pub struct TrustEntry {
74    pub fingerprint: String,
75    /// The owner's name for the device; empty until they give one.
76    #[serde(default)]
77    pub label: String,
78    /// The alias the device announced itself with (a claim, not an identity).
79    #[serde(default)]
80    pub alias_seen: String,
81    /// What an unproven request claimed on a record that has proof; shown on
82    /// the card, never copied into `alias_seen`/`model`/`kind`.
83    #[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    /// How the device last proved the key behind its fingerprint:
95    /// "certificate" or "callback"; None when it never has.
96    #[serde(default)]
97    pub proof: Option<String>,
98    /// The owner set the state on purpose (so the record is never dropped).
99    #[serde(default)]
100    pub decided: bool,
101    /// Requests answered 403 because of Deny.
102    #[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    /// Whether the record may be dropped to make room.
125    fn evictable(&self) -> bool {
126        // A device that has proved its key keeps that memory: forgetting it would let a
127        // later request for the same fingerprint fall back to the weaker callback.
128        !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
153/// A claimed text cut to `max` characters with control characters removed.
154fn 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    /// The file on disk could not be read. Nothing is written while this is set:
168    /// the records in memory are not the owner's records, so writing them would
169    /// replace the file with a partial list. An explicit change reads the file
170    /// again first ([`TrustStore::lift_hold`]) and fails if it still cannot.
171    hold: bool,
172}
173
174impl TrustStore {
175    /// The records in `directory`.
176    ///
177    /// * A readable file is used (a stored default of Allow becomes Ask).
178    /// * No file, and no `.bad` set-aside next to it: the fingerprints of the old
179    ///   `known-devices.json` become **Ask** records, keeping any label (the old
180    ///   switch was off by default and a device was remembered after one accept,
181    ///   so that is no grant to skip the card).
182    /// * An unparsable file is set aside as `.bad` and an empty store is written
183    ///   at once, so a restart cannot migrate again and bring back old entries.
184    /// * Any other read error: an empty in-memory store, the file untouched and
185    ///   nothing written until the file has been read (see `hold`). While that
186    ///   lasts every device is treated as Ask: a stored Deny is not known.
187    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    /// Take the records of a parsed file (a stored default of Allow becomes Ask).
233    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    /// Called before every explicit change. With the hold on, reads the file again:
252    ///
253    /// * readable: its records become the store, and devices only sighted while
254    ///   the hold was on are added to them (an undecided one takes the stored
255    ///   default), so the change is made to the owner's full list;
256    /// * gone: there is nothing to lose, the hold ends;
257    /// * unparsable: set aside as `.bad`, as [`TrustStore::load`] does;
258    /// * still unreadable: an error, and nothing is changed or written.
259    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    /// Write everything out. Refused while the hold is on.
300    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    /// A save the owner did not ask for (a sighting, a refusal count).
318    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    /// What a request or send from `fingerprint` is subject to. A device with
335    /// no usable fingerprint can never be allowed.
336    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    /// Record a sighting. A first sighting takes the default state. `proof`
352    /// ("certificate" or "callback") is set only by a check that just passed;
353    /// without it, a record that already has proof keeps its alias, model and
354    /// kind and the claimed alias goes to `alias_claimed`.
355    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                    // A certificate outranks a callback.
392                    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    /// Whether this device has ever proved its key by certificate. Such a record
417    /// never accepts the weaker callback proof.
418    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    /// The device just answered a connect-back handshake for its fingerprint.
426    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    /// Keep at most `MAX_UNDECIDED` droppable records, oldest sighting first out,
440    /// leaving room for one more.
441    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    /// Count a request answered 403.
460    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    /// New devices start as Ask or Deny; Allow is only ever set per device.
522    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    /// Newest sighting first.
532    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    /// A list that cannot be read is never overwritten: an explicit change fails while it
553    /// stays unreadable, and once it can be read the change is made to the full list.
554    #[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        // Unreadable: a directory stands where the file was.
566        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        // Readable again: the same change now lands on the owner's full list.
586        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        // The device only sighted while the list was unreadable took the stored default.
597        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}