agentmux_srv/backend/storage/
lan_peer_pubkey_pins.rs1use rusqlite::params;
30
31use super::error::StoreError;
32use super::store::Store;
33
34use agentmux_common::time::now_secs;
35
36impl Store {
37 #[cfg(test)]
39 pub fn lan_peer_pubkey_pin_load(&self, agent_id: &str) -> Result<Option<String>, StoreError> {
40 let key = agent_id.to_lowercase();
41 let conn = self.conn.lock().unwrap();
42 let mut stmt = conn.prepare("SELECT public_key FROM db_lan_peer_pubkey_pins WHERE agent_id = ?1")?;
43 match stmt.query_row(params![key], |row| row.get(0)) {
44 Ok(v) => Ok(Some(v)),
45 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
46 Err(e) => Err(e.into()),
47 }
48 }
49
50 pub fn lan_peer_pubkey_pin_get_or_set(
61 &self,
62 agent_id: &str,
63 observed_key_b64: &str,
64 ) -> Result<String, StoreError> {
65 let key = agent_id.to_lowercase();
66 let conn = self.conn.lock().unwrap();
67 conn.execute(
68 "INSERT OR IGNORE INTO db_lan_peer_pubkey_pins (agent_id, public_key, first_seen_at) \
69 VALUES (?1, ?2, ?3)",
70 params![key, observed_key_b64, now_secs()],
71 )?;
72 let mut stmt = conn.prepare("SELECT public_key FROM db_lan_peer_pubkey_pins WHERE agent_id = ?1")?;
73 stmt.query_row(params![key], |row| row.get(0)).map_err(Into::into)
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn object_store() -> Store {
82 Store::open(std::path::Path::new(":memory:")).unwrap()
83 }
84
85 #[test]
86 fn load_returns_none_when_unpinned() {
87 let store = object_store();
88 assert!(store.lan_peer_pubkey_pin_load("korp").unwrap().is_none());
89 }
90
91 #[test]
92 fn first_observation_pins_the_key() {
93 let store = object_store();
94 let pinned = store.lan_peer_pubkey_pin_get_or_set("korp", "key-a").unwrap();
95 assert_eq!(pinned, "key-a");
96 assert_eq!(store.lan_peer_pubkey_pin_load("korp").unwrap().as_deref(), Some("key-a"));
97 }
98
99 #[test]
100 fn a_later_different_key_does_not_overwrite_the_pin() {
101 let store = object_store();
102 store.lan_peer_pubkey_pin_get_or_set("korp", "key-a").unwrap();
103 let result = store.lan_peer_pubkey_pin_get_or_set("korp", "key-b-attacker").unwrap();
106 assert_eq!(
107 result, "key-a",
108 "the first-pinned key must be returned regardless of what a later observation claims"
109 );
110 assert_eq!(store.lan_peer_pubkey_pin_load("korp").unwrap().as_deref(), Some("key-a"));
111 }
112
113 #[test]
114 fn repeated_observation_of_the_same_key_is_idempotent() {
115 let store = object_store();
116 store.lan_peer_pubkey_pin_get_or_set("korp", "key-a").unwrap();
117 let result = store.lan_peer_pubkey_pin_get_or_set("korp", "key-a").unwrap();
118 assert_eq!(result, "key-a");
119 }
120
121 #[test]
122 fn different_agent_ids_pin_independently() {
123 let store = object_store();
124 store.lan_peer_pubkey_pin_get_or_set("korp", "key-a").unwrap();
125 store.lan_peer_pubkey_pin_get_or_set("loap", "key-b").unwrap();
126 assert_eq!(store.lan_peer_pubkey_pin_load("korp").unwrap().as_deref(), Some("key-a"));
127 assert_eq!(store.lan_peer_pubkey_pin_load("loap").unwrap().as_deref(), Some("key-b"));
128 }
129
130 #[test]
131 fn agent_id_lookup_is_case_insensitive() {
132 let store = object_store();
133 store.lan_peer_pubkey_pin_get_or_set("Korp", "key-a").unwrap();
134 assert_eq!(store.lan_peer_pubkey_pin_load("korp").unwrap().as_deref(), Some("key-a"));
135 let result = store.lan_peer_pubkey_pin_get_or_set("KORP", "key-b").unwrap();
136 assert_eq!(result, "key-a", "case-insensitive pin lookup must find the existing pin");
137 }
138}