sueta / client / src / app / trust.ts
 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
/**
 * Local trust store: trust-on-first-use bindings of display name → identity
 * key, plus explicitly verified identities. Lives only in this browser.
 *
 * - TOFU: the first identity key seen for a display name is remembered; if the
 *   same name later shows up with a DIFFERENT key, the UI warns loudly.
 * - Verified: after comparing fingerprints out-of-band, you mark an identity
 *   verified; the checkmark follows the KEY (not the name) from then on.
 */

export interface TrustVerdict {
  verified: boolean;
  /** Set when this display name was previously seen with a different key. */
  keyChanged: boolean;
}

interface TrustState {
  names: Record<string, string>; // lowercased display name → publicKeyB64 (TOFU)
  verified: Record<string, string>; // publicKeyB64 → name it was verified as
}

const KEY = "sueta:trust";

function load(): TrustState {
  try {
    const s = JSON.parse(localStorage.getItem(KEY) ?? "{}");
    return { names: s.names ?? {}, verified: s.verified ?? {} };
  } catch {
    return { names: {}, verified: {} };
  }
}

function persist(s: TrustState): void {
  try {
    localStorage.setItem(KEY, JSON.stringify(s));
  } catch {}
}

/** Record a sighting of (name, key); returns the verdict to render. */
export function observe(name: string, publicKeyB64: string): TrustVerdict {
  const s = load();
  const n = name.trim().toLowerCase();
  const known = s.names[n];
  const keyChanged = !!known && known !== publicKeyB64;
  if (!known) {
    s.names[n] = publicKeyB64; // trust on first use
    persist(s);
  }
  return { verified: publicKeyB64 in s.verified, keyChanged };
}

export function setVerified(publicKeyB64: string, name: string, on: boolean): void {
  const s = load();
  if (on) {
    s.verified[publicKeyB64] = name;
    // verifying also (re)binds the name to this key, clearing stale TOFU warnings
    s.names[name.trim().toLowerCase()] = publicKeyB64;
  } else {
    delete s.verified[publicKeyB64];
  }
  persist(s);
}

export function isVerified(publicKeyB64: string): boolean {
  return publicKeyB64 in load().verified;
}