1use rusqlite::{params, Connection, OptionalExtension};
14
15use crate::error::MatrixError;
16use crate::keys::CredentialKind;
17
18pub const NICK_MAX: usize = 64;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Identity {
23 pub id: i64,
25 pub nick: String,
26 pub localpart: String,
27 pub first_contact_ms: i64,
28}
29
30pub fn create_identities_schema(conn: &Connection) -> rusqlite::Result<()> {
31 conn.execute_batch(
32 r#"
33 CREATE TABLE IF NOT EXISTS identities (
34 id INTEGER PRIMARY KEY,
35 nick TEXT NOT NULL,
36 nick_ci TEXT NOT NULL UNIQUE,
37 localpart TEXT NOT NULL UNIQUE,
38 first_contact_ms INTEGER NOT NULL
39 );
40 CREATE TABLE IF NOT EXISTS reserved_localparts (
41 localpart TEXT PRIMARY KEY,
42 reason TEXT,
43 since_ms INTEGER NOT NULL
44 );
45 CREATE TABLE IF NOT EXISTS account_events_seen (
46 event_id TEXT PRIMARY KEY,
47 at_ms INTEGER NOT NULL
48 );
49 "#,
50 )
51}
52
53pub fn legal_localpart(nick: &str) -> Result<(), MatrixError> {
56 let ok = !nick.is_empty()
57 && nick.len() <= NICK_MAX
58 && nick.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '='));
59 if ok {
60 Ok(())
61 } else {
62 Err(MatrixError::invalid_nick("nick must be 1-64 of A-Z a-z 0-9 _ - . ="))
63 }
64}
65
66const COLS: &str = "id, nick, localpart, first_contact_ms";
67
68fn from_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Identity> {
69 Ok(Identity { id: r.get(0)?, nick: r.get(1)?, localpart: r.get(2)?, first_contact_ms: r.get(3)? })
70}
71
72pub fn identity_by_id(conn: &Connection, id: i64) -> rusqlite::Result<Option<Identity>> {
73 conn.query_row(&format!("SELECT {COLS} FROM identities WHERE id = ?1"), params![id], from_row).optional()
74}
75
76pub fn identity_by_nick(conn: &Connection, nick: &str) -> rusqlite::Result<Option<Identity>> {
77 conn.query_row(&format!("SELECT {COLS} FROM identities WHERE nick_ci = ?1"), params![nick.to_ascii_lowercase()], from_row).optional()
78}
79
80pub fn name_taken(conn: &Connection, lower: &str, except_id: i64) -> rusqlite::Result<bool> {
82 if crate::store::is_reserved_localpart(lower) {
83 return Ok(true);
84 }
85 let q = |sql: &str, p: &[&dyn rusqlite::ToSql]| -> rusqlite::Result<bool> { Ok(conn.query_row(sql, p, |_| Ok(())).optional()?.is_some()) };
86 Ok(q("SELECT 1 FROM identities WHERE id != ?2 AND (nick_ci = ?1 OR localpart = ?1)", &[&lower, &except_id])?
87 || q("SELECT 1 FROM reserved_localparts WHERE localpart = ?1", &[&lower])?
88 || q("SELECT 1 FROM matrix_users WHERE mxid = ?1 AND user_id != ?2", &[&crate::store::mxid_for_public_id(lower), &except_id])?)
89}
90
91fn next_id(conn: &Connection) -> rusqlite::Result<i64> {
92 conn.query_row(
93 "SELECT MAX((SELECT COALESCE(MAX(id), 0) FROM identities), (SELECT COALESCE(MAX(user_id), 0) FROM matrix_users)) + 1",
94 [],
95 |r| r.get(0),
96 )
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct Resolved {
102 pub identity: Identity,
103 pub device_id: String,
104 pub created: bool,
105}
106
107pub fn resolve_assertion(conn: &mut Connection, nick: &str, cred_ref: &str, now_ms: i64) -> Result<Resolved, MatrixError> {
112 let tx = conn.transaction()?;
113 let mut found = identity_by_nick(&tx, nick)?;
114 if found.is_none() {
115 if let Some(dev) = crate::keys::device_for_credential(&tx, CredentialKind::Web, cred_ref)? {
116 found = identity_by_id(&tx, dev.user_id)?;
117 if let Some(idn) = &found {
118 match rename_in(&tx, idn.id, nick) {
120 Ok(()) => found = identity_by_id(&tx, idn.id)?,
121 Err(e) => tracing::warn!(identity = idn.id, "rename by assertion not applied: {}", e.errcode),
122 }
123 }
124 }
125 }
126 let (identity, created) = match found {
127 Some(i) => {
128 if i.nick != nick && i.nick.eq_ignore_ascii_case(nick) {
129 tx.execute("UPDATE identities SET nick = ?2 WHERE id = ?1", params![i.id, nick])?;
130 }
131 (identity_by_id(&tx, i.id)?.ok_or_else(MatrixError::internal)?, false)
132 }
133 None => {
134 legal_localpart(nick)?;
135 let lower = nick.to_ascii_lowercase();
136 if name_taken(&tx, &lower, 0)? {
137 return Err(MatrixError::nick_conflict());
138 }
139 let id = next_id(&tx)?;
140 crate::store::ensure_matrix_user(&tx, id, &lower, &chrono::Utc::now().to_rfc3339())?;
141 tx.execute(
142 "INSERT INTO identities (id, nick, nick_ci, localpart, first_contact_ms) VALUES (?1, ?2, ?3, ?3, ?4)",
143 params![id, nick, lower, now_ms],
144 )?;
145 (identity_by_id(&tx, id)?.ok_or_else(MatrixError::internal)?, true)
146 }
147 };
148 let device_id = match crate::keys::device_for_credential(&tx, CredentialKind::Web, cred_ref)? {
149 Some(d) if d.user_id == identity.id => d.device_id,
150 Some(_) => return Err(MatrixError::forbidden("credential belongs to another identity")),
151 None => crate::keys::create_device(&tx, identity.id, CredentialKind::Web, cred_ref, &chrono::Utc::now().to_rfc3339())?,
152 };
153 tx.commit()?;
154 Ok(Resolved { identity, device_id, created })
155}
156
157fn rename_in(conn: &Connection, id: i64, new: &str) -> Result<(), MatrixError> {
159 legal_localpart(new)?;
160 let lower = new.to_ascii_lowercase();
161 if name_taken(conn, &lower, id)? {
162 return Err(MatrixError::nick_conflict());
163 }
164 conn.execute("UPDATE identities SET nick = ?2, nick_ci = ?3 WHERE id = ?1", params![id, new, lower])?;
165 Ok(())
166}
167
168pub fn apply_nick_changed(conn: &mut Connection, old: &str, new: &str) -> Result<Option<i64>, MatrixError> {
170 let tx = conn.transaction()?;
171 let Some(idn) = identity_by_nick(&tx, old)? else { return Ok(None) };
172 rename_in(&tx, idn.id, new)?;
173 tx.commit()?;
174 Ok(Some(idn.id))
175}
176
177pub fn apply_account_deleted(conn: &mut Connection, nick: &str, now_ms: i64) -> Result<bool, MatrixError> {
180 let Some(idn) = identity_by_nick(conn, nick)? else { return Ok(false) };
181 for d in crate::keys::list_devices(conn, idn.id)? {
182 crate::keys::delete_device(conn, idn.id, &d.device_id, &chrono::Utc::now().to_rfc3339())?;
183 }
184 let tx = conn.transaction()?;
185 tx.execute("INSERT OR IGNORE INTO reserved_localparts (localpart, reason, since_ms) VALUES (?1, 'identity retired', ?2)", params![idn.localpart, now_ms])?;
186 tx.execute("DELETE FROM identities WHERE id = ?1", params![idn.id])?;
187 tx.commit()?;
188 Ok(true)
189}
190
191pub fn apply_credential_revoked(conn: &mut Connection, cred_ref: &str) -> Result<Option<i64>, MatrixError> {
193 let hit = crate::keys::delete_device_by_credential(conn, CredentialKind::Web, cred_ref, &chrono::Utc::now().to_rfc3339())?;
194 Ok(hit.map(|(u, _)| u))
195}
196
197#[derive(Debug, Default, PartialEq, Eq)]
199pub struct ReconcileOutcome {
200 pub devices_removed: usize,
201 pub identities_retired: usize,
202 pub wake: Vec<i64>,
203 pub closed: Vec<i64>,
205}
206
207pub fn reconcile(conn: &mut Connection, snap: &m4a_seam::Reconcile, now_ms: i64) -> Result<ReconcileOutcome, MatrixError> {
211 let mut out = ReconcileOutcome::default();
212 let mut listed = std::collections::HashSet::new();
213 for live in &snap.nicks {
214 listed.insert(live.nick.to_ascii_lowercase());
215 let Some(idn) = identity_by_nick(conn, &live.nick)? else { continue };
216 for d in crate::keys::list_devices(conn, idn.id)? {
217 if d.credential_kind == CredentialKind::Web && !live.creds.iter().any(|c| *c == d.credential_ref) {
218 crate::keys::delete_device(conn, idn.id, &d.device_id, &chrono::Utc::now().to_rfc3339())?;
219 out.devices_removed += 1;
220 if !out.wake.contains(&idn.id) {
221 out.wake.push(idn.id);
222 out.closed.push(idn.id);
223 }
224 }
225 }
226 }
227 if snap.complete {
228 let all: Vec<String> = {
229 let mut st = conn.prepare("SELECT nick FROM identities")?;
230 let rows = st.query_map([], |r| r.get::<_, String>(0))?;
231 rows.collect::<Result<_, _>>()?
232 };
233 for nick in all.into_iter().filter(|n| !listed.contains(&n.to_ascii_lowercase())) {
234 let id = identity_by_nick(conn, &nick)?.map(|i| i.id);
235 if apply_account_deleted(conn, &nick, now_ms)? {
236 out.closed.extend(id);
237 out.identities_retired += 1;
238 }
239 }
240 }
241 Ok(out)
242}
243
244pub fn nick_of_user(conn: &Connection, user_id: i64) -> rusqlite::Result<Option<String>> {
246 conn.query_row("SELECT nick FROM identities WHERE id = ?1", params![user_id], |r| r.get(0)).optional()
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::store::create_matrix_schema;
253
254 fn db() -> Connection {
255 let c = Connection::open_in_memory().unwrap();
256 c.pragma_update(None, "foreign_keys", "ON").unwrap();
257 create_matrix_schema(&c).unwrap();
258 crate::keys::create_matrix_keys_schema(&c).unwrap();
259 c
260 }
261
262 #[test]
263 fn first_assertion_is_first_contact_and_freezes_the_localpart() {
264 let mut c = db();
265 let r = resolve_assertion(&mut c, "Carol", "c1", 10).unwrap();
266 assert!(r.created);
267 assert_eq!((r.identity.nick.as_str(), r.identity.localpart.as_str()), ("Carol", "carol"));
268 assert_eq!(crate::store::mxid_of(&c, r.identity.id).unwrap().as_deref(), Some("@carol:example.org"));
269 let again = resolve_assertion(&mut c, "carol", "c1", 20).unwrap();
270 assert!(!again.created);
271 assert_eq!(again.identity.id, r.identity.id);
272 assert_eq!(again.device_id, r.device_id);
273 assert_eq!(again.identity.nick, "carol", "case-only change follows");
274 let other = resolve_assertion(&mut c, "dave", "c2", 30).unwrap();
275 assert_ne!(other.device_id, r.device_id);
276 }
277
278 #[test]
279 fn rename_by_assertion_and_by_event_keep_the_localpart() {
280 let mut c = db();
281 let r = resolve_assertion(&mut c, "erin", "k1", 1).unwrap();
282 let healed = resolve_assertion(&mut c, "erin2", "k1", 2).unwrap();
283 assert!(!healed.created);
284 assert_eq!((healed.identity.id, healed.identity.nick.as_str(), healed.identity.localpart.as_str()), (r.identity.id, "erin2", "erin"));
285 resolve_assertion(&mut c, "frank", "k2", 3).unwrap();
286 assert_eq!(apply_nick_changed(&mut c, "erin2", "frank").unwrap_err().errcode, "M4A_NICK_CONFLICT");
287 assert_eq!(apply_nick_changed(&mut c, "erin2", "erin3").unwrap(), Some(r.identity.id));
288 assert_eq!(apply_nick_changed(&mut c, "nobody", "xyz").unwrap(), None);
289 assert!(name_taken(&c, "erin", 0).unwrap(), "frozen localpart stays reserved to others");
290 assert!(!name_taken(&c, "erin2", 0).unwrap(), "old nick string is free");
291 }
292
293 #[test]
294 fn collisions_and_legality() {
295 let mut c = db();
296 resolve_assertion(&mut c, "gina", "k1", 1).unwrap();
297 assert!(!resolve_assertion(&mut c, "GINA", "k9", 2).unwrap().created);
299 crate::store::ensure_matrix_user(&c, 900, "agentx", "t").unwrap();
301 assert_eq!(resolve_assertion(&mut c, "agentx", "k3", 3).unwrap_err().errcode, "M4A_NICK_CONFLICT");
302 for bad in ["a:b", "a b", "a/b", "", &"x".repeat(65)] {
303 assert_eq!(resolve_assertion(&mut c, bad, "k4", 4).unwrap_err().errcode, "M4A_INVALID_NICK", "{bad:?}");
304 }
305 let n: i64 = c.query_row("SELECT COUNT(*) FROM identities", [], |r| r.get(0)).unwrap();
306 assert_eq!(n, 1);
307 }
308
309 #[test]
310 fn revoke_and_delete_retire_devices_and_tombstone_the_localpart() {
311 let mut c = db();
312 let r = resolve_assertion(&mut c, "hank", "k1", 1).unwrap();
313 assert_eq!(apply_credential_revoked(&mut c, "k1").unwrap(), Some(r.identity.id));
314 assert_eq!(apply_credential_revoked(&mut c, "k1").unwrap(), None);
315 resolve_assertion(&mut c, "hank", "k5", 2).unwrap();
316 assert!(apply_account_deleted(&mut c, "hank", 3).unwrap());
317 assert!(!apply_account_deleted(&mut c, "hank", 3).unwrap());
318 assert!(name_taken(&c, "hank", 0).unwrap(), "retired localpart is never reissued");
319 assert_eq!(resolve_assertion(&mut c, "hank", "k6", 4).unwrap_err().errcode, "M4A_NICK_CONFLICT");
320 assert!(crate::keys::list_devices(&c, r.identity.id).unwrap().is_empty());
321 }
322
323 #[test]
324 fn remote_users_are_not_identities_and_ids_do_not_collide() {
325 let mut c = db();
326 crate::fed_rooms::ensure_remote_user(&c, "@bob:other.example", "t").unwrap();
327 crate::store::ensure_matrix_user(&c, 41, "legacyuser", "t").unwrap();
328 let r = resolve_assertion(&mut c, "ivy", "k1", 1).unwrap();
329 assert!(r.identity.id >= 42);
330 let n: i64 = c.query_row("SELECT COUNT(*) FROM identities", [], |r| r.get(0)).unwrap();
331 assert_eq!(n, 1);
332 }
333}