1use std::sync::Arc;
9
10use ed25519_dalek::{Signer, SigningKey};
11use m4a_seam::keyproof;
12use serde::{Deserialize, Serialize};
13use zeroize::Zeroizing;
14
15use crate::backend::BackendKind;
16use crate::error::{AgentError, Result};
17use crate::vault::{random_bytes, KeyVault};
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct SessionIdentity {
22 pub session_id: String,
23 pub tier: BackendKind,
24 pub server_ref: String,
26 pub key_id: String,
27 pub public_key: String,
29 pub nick: Option<String>,
31 pub enrolled: bool,
32 #[serde(default)]
35 pub local_session: Option<String>,
36 #[serde(default)]
39 pub requested_nick: Option<String>,
40}
41
42fn seed_label(sid: &str) -> String {
43 format!("identity-key/{sid}")
44}
45fn record_label(sid: &str) -> String {
46 format!("identity/{sid}")
47}
48pub fn token_label(sid: &str) -> String {
50 format!("session-token/{sid}")
51}
52
53fn valid_session_id(s: &str) -> bool {
54 !s.is_empty() && s.len() <= 64 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
55}
56
57impl SessionIdentity {
58 pub fn sign(&self, vault: &dyn KeyVault, msg: &[u8]) -> Result<String> {
60 let sk = signing_key(vault, &self.session_id)?;
61 Ok(keyproof::encode(&sk.sign(msg).to_bytes()))
62 }
63
64 pub fn sign_login(&self, vault: &dyn KeyVault, c: &keyproof::Challenge) -> Result<String> {
66 self.sign(vault, &keyproof::login_message(c, &self.key_id))
67 }
68
69 pub fn sign_enroll(&self, vault: &dyn KeyVault, audience: &str, invite: &str) -> Result<String> {
71 self.sign(vault, &keyproof::enroll_message(audience, invite, &self.public_key))
72 }
73}
74
75fn signing_key(vault: &dyn KeyVault, sid: &str) -> Result<SigningKey> {
76 let seed = vault.get(&seed_label(sid))?.ok_or_else(|| AgentError::Identity("the identity key is missing from the vault".into()))?;
77 let seed: Zeroizing<[u8; 32]> = Zeroizing::new(<[u8; 32]>::try_from(&seed[..]).map_err(|_| AgentError::Identity("the identity key has the wrong size".into()))?);
78 Ok(SigningKey::from_bytes(&seed))
79}
80
81#[derive(Clone)]
83pub struct IdentityStore {
84 vault: Arc<dyn KeyVault>,
85}
86
87impl IdentityStore {
88 pub fn new(vault: Arc<dyn KeyVault>) -> Self {
89 Self { vault }
90 }
91
92 pub fn vault(&self) -> &dyn KeyVault {
93 &*self.vault
94 }
95
96 pub fn resolve(&self, session_id: &str, tier: BackendKind, server_ref: &str) -> Result<SessionIdentity> {
99 if !valid_session_id(session_id) {
100 return Err(AgentError::Identity("session id must be 1-64 characters of [A-Za-z0-9._-]".into()));
101 }
102 let server_ref = server_ref.trim_end_matches('/').to_string();
103 if let Some(raw) = self.vault.get(&record_label(session_id))? {
104 let id: SessionIdentity = serde_json::from_slice(&raw).map_err(|e| AgentError::Identity(format!("stored identity: {e}")))?;
105 if id.tier != tier || id.server_ref != server_ref {
106 return Err(AgentError::Identity("this session is bound to another tier or server; use a new session id".into()));
107 }
108 signing_key(&*self.vault, session_id)?; return Ok(id);
110 }
111 let seed = random_bytes(32);
112 let sk = SigningKey::from_bytes(<&[u8; 32]>::try_from(&seed[..]).map_err(|_| AgentError::Identity("seed".into()))?);
113 let pk = sk.verifying_key();
114 let id = SessionIdentity { session_id: session_id.to_string(), tier, server_ref, key_id: keyproof::key_id_of(pk.as_bytes()), public_key: keyproof::encode(pk.as_bytes()), nick: None, enrolled: false, local_session: None, requested_nick: None };
115 self.vault.put(&seed_label(session_id), &seed)?;
117 self.save(&id)?;
118 Ok(id)
119 }
120
121 pub fn save(&self, id: &SessionIdentity) -> Result<()> {
122 self.vault.put(&record_label(&id.session_id), &serde_json::to_vec(id).map_err(|e| AgentError::Identity(e.to_string()))?)
123 }
124
125 pub fn bind_local(&self, id: &mut SessionIdentity, local_id: &str) -> Result<()> {
128 if !valid_session_id(local_id) {
129 return Err(AgentError::Identity("local session id is not a valid id".into()));
130 }
131 if let Some(have) = &id.local_session {
132 return if have == local_id { Ok(()) } else { Err(AgentError::Identity("this session is already linked to another local session".into())) };
133 }
134 let label = format!("local-link/{local_id}");
135 if let Some(owner) = self.vault.get(&label)? {
136 if owner[..] != *id.session_id.as_bytes() {
137 return Err(AgentError::Identity("that local session belongs to another identity".into()));
138 }
139 }
140 self.vault.put(&label, id.session_id.as_bytes())?;
141 id.local_session = Some(local_id.to_string());
142 self.save(id)
143 }
144
145 pub fn adopt_nick(&self, id: &mut SessionIdentity, nick: &str) -> Result<()> {
148 match &id.nick {
149 Some(n) if n == nick => Ok(()),
150 Some(_) => Err(AgentError::Identity("this identity already has a different nick".into())),
151 None => {
152 id.nick = Some(nick.to_string());
153 self.save(id)
154 }
155 }
156 }
157
158 pub fn store_key(&self, session_id: &str) -> Result<Zeroizing<Vec<u8>>> {
160 let label = format!("store-key/{session_id}");
161 if let Some(k) = self.vault.get(&label)? {
162 return Ok(k);
163 }
164 let k = random_bytes(32);
165 self.vault.put(&label, &k)?;
166 Ok(k)
167 }
168
169 pub fn forget(&self, session_id: &str) -> Result<()> {
172 if let Some(raw) = self.vault.get(&record_label(session_id))? {
173 if let Ok(id) = serde_json::from_slice::<SessionIdentity>(&raw) {
174 if let Some(l) = id.local_session {
175 self.vault.delete(&format!("local-link/{l}"))?;
176 }
177 }
178 }
179 for l in [seed_label(session_id), record_label(session_id), token_label(session_id), format!("store-key/{session_id}")] {
180 self.vault.delete(&l)?;
181 }
182 Ok(())
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::vault::MemoryVault;
190
191 fn store() -> IdentityStore {
192 IdentityStore::new(Arc::new(MemoryVault::new()))
193 }
194
195 #[test]
196 fn an_identity_is_generated_once_and_found_again() {
197 let s = store();
198 let a = s.resolve("s1", BackendKind::Server, "https://p.example/").unwrap();
199 assert!(!a.enrolled && a.nick.is_none());
200 assert_eq!(a.server_ref, "https://p.example");
201 assert_eq!(a.key_id, keyproof::key_id_of(&keyproof::decode(&a.public_key).unwrap()));
202 assert_eq!(s.resolve("s1", BackendKind::Server, "https://p.example").unwrap(), a);
203 let b = s.resolve("s2", BackendKind::Server, "https://p.example").unwrap();
204 assert_ne!(a.key_id, b.key_id, "one session, one identity");
205 }
206
207 #[test]
208 fn a_session_cannot_be_moved_to_another_server_or_tier() {
209 let s = store();
210 s.resolve("s1", BackendKind::Server, "https://p.example").unwrap();
211 assert!(s.resolve("s1", BackendKind::Server, "https://other.example").is_err());
212 assert!(s.resolve("s1", BackendKind::Matrix, "https://p.example").is_err());
213 assert!(s.resolve("../x", BackendKind::Server, "https://p.example").is_err());
214 assert!(s.resolve("", BackendKind::Server, "https://p.example").is_err());
215 }
216
217 #[test]
218 fn signatures_verify_under_the_public_key_and_forget_makes_a_new_identity() {
219 let s = store();
220 let id = s.resolve("s1", BackendKind::Matrix, "https://p.example").unwrap();
221 let c = keyproof::Challenge { challenge_id: "c".into(), nonce: "n".into(), expires_ms: 5, audience: "aud".into() };
222 let sig = id.sign_login(s.vault(), &c).unwrap();
223 assert!(keyproof::verify(&id.public_key, &keyproof::login_message(&c, &id.key_id), &sig));
224 let sig = id.sign_enroll(s.vault(), "aud", "inv").unwrap();
225 assert!(keyproof::verify(&id.public_key, &keyproof::enroll_message("aud", "inv", &id.public_key), &sig));
226 s.forget("s1").unwrap();
227 assert!(id.sign(s.vault(), b"x").is_err(), "the key is gone");
228 let again = s.resolve("s1", BackendKind::Matrix, "https://p.example").unwrap();
229 assert_ne!(again.key_id, id.key_id);
230 }
231
232 #[test]
233 fn links_and_nicks_are_one_to_one_and_frozen() {
234 let s = store();
235 let mut a = s.resolve("s1", BackendKind::Server, "https://p.example").unwrap();
236 let mut b = s.resolve("s2", BackendKind::Server, "https://p.example").unwrap();
237 s.bind_local(&mut a, "local-1").unwrap();
238 s.bind_local(&mut a, "local-1").unwrap();
239 assert!(s.bind_local(&mut a, "local-2").is_err(), "one session, one local id");
240 assert!(s.bind_local(&mut b, "local-1").is_err(), "one local id, one session");
241 assert_eq!(s.resolve("s1", BackendKind::Server, "https://p.example").unwrap().local_session.as_deref(), Some("local-1"));
242 s.adopt_nick(&mut a, "kestrel").unwrap();
243 s.adopt_nick(&mut a, "kestrel").unwrap();
244 assert!(s.adopt_nick(&mut a, "other").is_err());
245 s.forget("s1").unwrap();
246 let mut c = s.resolve("s3", BackendKind::Server, "https://p.example").unwrap();
247 s.bind_local(&mut c, "local-1").unwrap(); }
249
250 #[test]
251 fn the_debug_output_and_record_carry_no_private_key() {
252 let s = store();
253 let id = s.resolve("s1", BackendKind::Server, "https://p.example").unwrap();
254 let seed = s.vault().get(&seed_label("s1")).unwrap().unwrap();
255 let seed_b64 = keyproof::encode(&seed);
256 assert!(!format!("{id:?}").contains(&seed_b64));
257 assert!(!String::from_utf8_lossy(&serde_json::to_vec(&id).unwrap()).contains(&seed_b64));
258 }
259}