Skip to main content

mail4agent_server/
key_ops.rs

1//! Device keys, to-device, cross-signing, and backup decisions.
2//! Signature checks that the protocol requires stay here. Transport does not.
3
4use std::collections::{BTreeMap, HashMap, HashSet};
5
6use base64::engine::general_purpose::STANDARD_NO_PAD;
7use base64::Engine;
8use ed25519_dalek::{Signature, VerifyingKey};
9use rusqlite::Connection;
10
11use crate::error::MatrixError;
12use crate::keys::CrossSigningUsage;
13use crate::store::{Membership, TxnDedupEntry};
14
15/// Per-request cap on `PUT /sendToDevice`'s flattened target-device count
16/// (plan P9 brief: "≤ 1 000 target devices").
17pub const SEND_TO_DEVICE_MAX_TARGETS: usize = 1000;
18
19/// Per-target content-size cap (plan P9 brief: "≤ 64 KiB per content").
20pub const SEND_TO_DEVICE_MAX_CONTENT_BYTES: usize = 64 * 1024;
21
22/// The one key-backup algorithm this server accepts (plan P9 brief, MSC3270
23/// naming) — `m.megolm_backup.v1.curve25519-aes-sha2` (the PkEncryption
24/// backup) is refused outright, never even reaching storage.
25pub const BACKUP_ALGORITHM: &str = "m.megolm_backup.v1.aes-hmac-sha2";
26
27
28// ============================================================================
29// Shared gate: share-a-room visibility
30// ============================================================================
31
32/// Every user id the caller may see key material for through this module's
33/// share-a-room gate: `user_id` itself, plus every user who is JOINED or
34/// INVITED in a room the caller is JOINED to (plan P9 brief: `/keys/query`/
35/// `/keys/claim`/`PUT /sendToDevice` "restrict to users who share a room
36/// with the caller, or the caller"; the invited half is P16 S-e — see this
37/// module's own doc). The caller must be joined: a room where the caller is
38/// only invited, has left, or is banned contributes nothing. A target
39/// outside this set is dropped/emptied by the caller, never distinguished
40/// from "no such account".
41pub fn peers_sharing_a_room_with(conn: &Connection, user_id: i64) -> rusqlite::Result<HashSet<i64>> {
42    let mut ids = HashSet::new();
43    ids.insert(user_id);
44    for room_id in crate::store::rooms_for_user(conn, user_id, Some(Membership::Join))? {
45        for member in crate::store::room_members(conn, &room_id, None)? {
46            if matches!(member.membership, Membership::Join | Membership::Invite) {
47                ids.insert(member.user_id);
48            }
49        }
50    }
51    Ok(ids)
52}
53
54
55// ============================================================================
56// Device-list delta — the one `device_lists` computation `/sync` and
57// `/keys/changes` share
58// ============================================================================
59
60/// The `device_lists` object of a sync window: mxids, sorted and de-duplicated.
61pub struct DeviceListDelta {
62    /// Visible users whose device list changed in the window, plus users who
63    /// NEWLY share an encrypted room with the caller (Matrix: "or who now
64    /// share an encrypted room with the client since the previous sync") —
65    /// the client has never queried their keys.
66    pub changed: Vec<String>,
67    /// Users who no longer share any room with the caller: they left/were
68    /// banned from a shared room, or the caller itself left the room. A
69    /// user still sharing another room is never listed.
70    pub left: Vec<String>,
71}
72
73
74/// Users who entered (`entered`) or stopped sharing (`departed`, before the
75/// still-shares-another-room exclusion the caller applies) an ENCRYPTED room
76/// with `caller_user_id` in `(from_exclusive, to_inclusive]`. Encrypted rooms
77/// only: the client tracks device lists for those alone, and a public
78/// unencrypted channel would otherwise list every one of its members whenever
79/// the caller joins it.
80///
81/// - A joined caller who was already joined at `from_exclusive`: every member
82///   whose membership went from not-in-room (`leave`/`ban`/none) to
83///   `join`/`invite` inside the window entered. (`invite` -> `join` is not
84///   a new share: an invitee was already visible to the joined caller.)
85/// - A joined caller who was NOT joined at `from_exclusive` (just joined, or
86///   accepted an invite): every current `join`/`invite` member entered.
87/// - A caller who left/was banned after being joined at `from_exclusive`:
88///   every current `join`/`invite` member departed.
89pub fn shared_room_transitions(conn: &Connection, caller_user_id: i64, from_exclusive: i64, to_inclusive: i64) -> rusqlite::Result<(Vec<i64>, Vec<i64>)> {
90    let mut entered = Vec::new();
91    let mut departed = Vec::new();
92    let Some(caller_mxid) = crate::store::mxid_of(conn, caller_user_id)? else {
93        return Ok((entered, departed));
94    };
95    let in_room = |membership: Membership| matches!(membership, Membership::Join | Membership::Invite);
96
97    // Only a room with a member event inside the window can have changed
98    // who shares it with the caller (the caller's own join/leave is such an
99    // event too), so per-room work is limited to those — one query up front
100    // instead of a handful per room on every sync wake.
101    let caller_rooms = crate::store::rooms_for_user(conn, caller_user_id, None)?;
102    for room_id in crate::store::rooms_with_member_events_in_window(conn, &caller_rooms, from_exclusive, to_inclusive)? {
103        let Some(room) = crate::store::get_room(conn, &room_id)? else { continue };
104        if !room.is_encrypted {
105            continue;
106        }
107        let Some(own) = crate::store::room_member(conn, &room_id, caller_user_id)? else { continue };
108        let was_joined = crate::store::membership_at(conn, &room_id, &caller_mxid, from_exclusive)? == Some(Membership::Join);
109
110        match own.membership {
111            Membership::Join if was_joined => {
112                for mxid in crate::store::member_state_keys_in_window(conn, &room_id, from_exclusive, to_inclusive)? {
113                    let Some(user_id) = crate::store::user_id_of(conn, &mxid)? else { continue };
114                    if user_id == caller_user_id {
115                        continue;
116                    }
117                    let now_in = crate::store::room_member(conn, &room_id, user_id)?.is_some_and(|m| in_room(m.membership));
118                    let before_in = crate::store::membership_at(conn, &room_id, &mxid, from_exclusive)?.is_some_and(in_room);
119                    if now_in && !before_in {
120                        entered.push(user_id);
121                    }
122                }
123            }
124            Membership::Join => {
125                for member in crate::store::room_members(conn, &room_id, None)? {
126                    if member.user_id != caller_user_id && in_room(member.membership) {
127                        entered.push(member.user_id);
128                    }
129                }
130            }
131            Membership::Leave | Membership::Ban if was_joined => {
132                for member in crate::store::room_members(conn, &room_id, None)? {
133                    if member.user_id != caller_user_id && in_room(member.membership) {
134                        departed.push(member.user_id);
135                    }
136                }
137            }
138            Membership::Leave | Membership::Ban | Membership::Invite => {}
139        }
140    }
141    Ok((entered, departed))
142}
143
144
145/// The mxids of `user_ids`, sorted and de-duplicated (a user with no mxid row
146/// cannot be named to a client and is dropped).
147pub fn sorted_mxids(conn: &Connection, user_ids: &HashSet<i64>) -> rusqlite::Result<Vec<String>> {
148    let mut mxids = Vec::with_capacity(user_ids.len());
149    for &user_id in user_ids {
150        if let Some(mxid) = crate::store::mxid_of(conn, user_id)? {
151            mxids.push(mxid);
152        }
153    }
154    mxids.sort();
155    mxids.dedup();
156    Ok(mxids)
157}
158
159
160/// The `device_lists` delta of `(from_exclusive, to_inclusive]` as seen by
161/// `caller_user_id` — see [`DeviceListDelta`]. `/sync` (incremental) and
162/// `GET /keys/changes` both build their answer from this one function so the
163/// two can never disagree.
164pub fn device_list_delta(conn: &Connection, caller_user_id: i64, from_exclusive: i64, to_inclusive: i64) -> rusqlite::Result<DeviceListDelta> {
165    let visible = peers_sharing_a_room_with(conn, caller_user_id)?;
166    let (entered, departed) = shared_room_transitions(conn, caller_user_id, from_exclusive, to_inclusive)?;
167
168    let mut changed_ids: HashSet<i64> = HashSet::new();
169    for user_id in crate::keys::device_list_changes_between(conn, from_exclusive, to_inclusive)? {
170        if visible.contains(&user_id) {
171            changed_ids.insert(user_id);
172        }
173    }
174    changed_ids.extend(entered.into_iter().filter(|user_id| visible.contains(user_id)));
175
176    let caller_rooms = crate::store::rooms_for_user(conn, caller_user_id, None)?;
177    let mut left_ids: HashSet<i64> = HashSet::new();
178    for user_id in crate::store::user_ids_with_leave_transition_in_rooms(conn, &caller_rooms, from_exclusive, to_inclusive)? {
179        if !visible.contains(&user_id) {
180            left_ids.insert(user_id);
181        }
182    }
183    left_ids.extend(departed.into_iter().filter(|user_id| !visible.contains(user_id)));
184
185    Ok(DeviceListDelta { changed: sorted_mxids(conn, &changed_ids)?, left: sorted_mxids(conn, &left_ids)? })
186}
187
188
189// ============================================================================
190// POST /_matrix/client/v3/keys/upload
191// ============================================================================
192
193#[derive(serde::Deserialize, Default)]
194pub struct KeysUploadRequest {
195    #[serde(default)]
196    pub device_keys: Option<serde_json::Value>,
197    #[serde(default)]
198    pub one_time_keys: Option<BTreeMap<String, serde_json::Value>>,
199    #[serde(default)]
200    pub fallback_keys: Option<BTreeMap<String, serde_json::Value>>,
201}
202
203
204/// `device_keys.user_id`/`device_id` must name the authenticated caller
205/// (plan §2 manager decision 5 / P9 brief) — else `M_INVALID_PARAM`.
206pub fn check_device_keys_ownership(device_keys: &serde_json::Value, caller_mxid: &str, caller_device_id: &str) -> Result<(), MatrixError> {
207    let user_id = device_keys.get("user_id").and_then(|v| v.as_str());
208    let device_id = device_keys.get("device_id").and_then(|v| v.as_str());
209    if user_id != Some(caller_mxid) || device_id != Some(caller_device_id) {
210        return Err(MatrixError::invalid_param("device_keys.user_id/device_id must name the authenticated caller"));
211    }
212    Ok(())
213}
214
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum DeviceKeysUploadAction {
218    /// No device_keys row yet — first publish.
219    Insert,
220    /// Same identity keys resent — silent no-op.
221    Noop,
222    /// Authenticated device bearer replaced its Olm identity (lost sealed
223    /// store, same device id). Peers must re-query and re-share Megolm.
224    Reset,
225}
226
227
228/// Decide what a `device_keys` upload does for an already-authenticated
229/// device bearer. Compares only the `keys` object (curve25519/ed25519
230/// identity), not `algorithms`/`signatures`.
231///
232/// - No row yet → [`DeviceKeysUploadAction::Insert`].
233/// - Identical `keys` → [`DeviceKeysUploadAction::Noop`] (clients re-upload
234///   on restart).
235/// - Different `keys` → [`DeviceKeysUploadAction::Reset`]: the sealed store
236///   was lost and the same device bearer minted a new Olm identity. The
237///   bearer already authenticated this call, so we accept the new identity,
238///   invalidate old OTKs, and wake peers via `device_lists.changed`. A silent
239///   refuse used to black-hole mail: peers never re-queried keys and never
240///   received a Megolm re-share.
241pub fn decide_device_keys_upload(existing_keys_json: Option<&str>, new_keys_value: &serde_json::Value) -> Result<DeviceKeysUploadAction, MatrixError> {
242    let Some(existing_keys_json) = existing_keys_json else {
243        return Ok(DeviceKeysUploadAction::Insert);
244    };
245    let existing_keys: serde_json::Value = serde_json::from_str(existing_keys_json)?;
246    if &existing_keys == new_keys_value {
247        Ok(DeviceKeysUploadAction::Noop)
248    } else {
249        Ok(DeviceKeysUploadAction::Reset)
250    }
251}
252
253
254/// `algorithm:key_id` — every one-time/fallback key's own wire id (plan P9
255/// brief: "OTK ids algorithm:key_id format validated").
256pub fn split_algorithm_key_id(full_id: &str) -> Result<(&str, &str), MatrixError> {
257    let (algorithm, key_id) = full_id
258        .split_once(':')
259        .ok_or_else(|| MatrixError::invalid_param(format!("malformed one-time key id: {full_id}")))?;
260    if algorithm.is_empty() || key_id.is_empty() {
261        return Err(MatrixError::invalid_param(format!("malformed one-time key id: {full_id}")));
262    }
263    Ok((algorithm, key_id))
264}
265
266
267pub struct KeysUploadOutcome {
268    pub otk_counts: HashMap<String, i64>,
269    pub device_keys_changed: bool,
270}
271
272
273/// DB-only core for `POST /keys/upload`: ownership check on `device_keys`,
274/// then insert / no-op / authenticated identity reset (see
275/// [`decide_device_keys_upload`]), OTK-id validation, and storage. On
276/// [`DeviceKeysUploadAction::Reset`] old one-time and fallback keys for this
277/// device are wiped before the new identity is stored. Returns whether
278/// `device_keys` actually changed (the caller wakes peers when it did).
279pub fn apply_keys_upload(
280    conn: &mut Connection,
281    caller_user_id: i64,
282    caller_mxid: &str,
283    caller_device_id: &str,
284    request: &KeysUploadRequest,
285    now: &str,
286) -> Result<KeysUploadOutcome, MatrixError> {
287    let mut device_keys_changed = false;
288
289    if let Some(device_keys) = &request.device_keys {
290        check_device_keys_ownership(device_keys, caller_mxid, caller_device_id)?;
291        let new_keys_value = device_keys.get("keys").cloned().unwrap_or(serde_json::Value::Null);
292        let existing = crate::keys::device_keys_for(conn, &[caller_user_id])?
293            .into_iter()
294            .find(|d| d.device_id == caller_device_id);
295        let action = decide_device_keys_upload(existing.as_ref().map(|d| d.keys.as_str()), &new_keys_value)?;
296        if matches!(action, DeviceKeysUploadAction::Insert | DeviceKeysUploadAction::Reset) {
297            if action == DeviceKeysUploadAction::Reset {
298                crate::keys::clear_device_one_time_material(conn, caller_user_id, caller_device_id)?;
299            }
300            let algorithms_json = device_keys.get("algorithms").cloned().unwrap_or(serde_json::json!([])).to_string();
301            let keys_json = new_keys_value.to_string();
302            let signatures_json = device_keys.get("signatures").cloned().unwrap_or(serde_json::json!({})).to_string();
303            crate::keys::upsert_device_keys(conn, caller_user_id, caller_device_id, &algorithms_json, &keys_json, &signatures_json, now)?;
304            device_keys_changed = true;
305        }
306    }
307
308    if let Some(one_time_keys) = &request.one_time_keys {
309        let mut batch = Vec::with_capacity(one_time_keys.len());
310        for (full_id, value) in one_time_keys {
311            let (algorithm, _) = split_algorithm_key_id(full_id)?;
312            batch.push((full_id.clone(), algorithm.to_string(), value.to_string()));
313        }
314        crate::keys::add_one_time_keys(conn, caller_user_id, caller_device_id, &batch)?;
315    }
316
317    if let Some(fallback_keys) = &request.fallback_keys {
318        for (full_id, value) in fallback_keys {
319            let (algorithm, _) = split_algorithm_key_id(full_id)?;
320            crate::keys::upsert_fallback_key(conn, caller_user_id, caller_device_id, algorithm, full_id, &value.to_string(), now)?;
321        }
322    }
323
324    let otk_counts = crate::keys::count_one_time_keys(conn, caller_user_id, caller_device_id)?;
325    Ok(KeysUploadOutcome { otk_counts, device_keys_changed })
326}
327
328
329/// What `keys_upload`'s blocking half hands back: the caller's one-time-key
330/// counts, and the peers to wake when its device keys changed.
331
332
333
334// ============================================================================
335// POST /_matrix/client/v3/keys/query
336// ============================================================================
337
338#[derive(serde::Deserialize, Default)]
339pub struct KeysQueryRequest {
340    #[serde(default)]
341    pub device_keys: BTreeMap<String, Vec<String>>,
342}
343
344
345/// DB-only core for `POST /keys/query` (plan P9 brief): batch `device_keys`
346/// for every requested user this caller may see (self, or a shared-room
347/// peer — [`peers_sharing_a_room_with`]), plus `master_keys`/
348/// `self_signing_keys` for the same allowed set, and `user_signing_keys`
349/// ONLY for the caller (spec: a user-signing key is never shared with anyone
350/// else). A requested user this caller may NOT see, or an mxid naming no
351/// known account, gets an empty `device_keys` entry (`{}`) rather than being
352/// omitted — the caller learns nothing about whether the account even
353/// exists.
354pub fn build_keys_query_response(
355    conn: &Connection,
356    caller_user_id: i64,
357    caller_mxid: &str,
358    requested: &BTreeMap<String, Vec<String>>,
359) -> Result<serde_json::Value, MatrixError> {
360    let visible = peers_sharing_a_room_with(conn, caller_user_id)?;
361    build_keys_query_visible(conn, &visible, Some((caller_user_id, caller_mxid)), requested)
362}
363
364/// [`build_keys_query_response`] with an explicit visible-user set; `caller`
365/// is `None` for a federation request (no user-signing key is returned).
366pub fn build_keys_query_visible(
367    conn: &Connection,
368    visible: &HashSet<i64>,
369    caller: Option<(i64, &str)>,
370    requested: &BTreeMap<String, Vec<String>>,
371) -> Result<serde_json::Value, MatrixError> {
372
373    let mut device_keys_out = serde_json::Map::new();
374    let mut master_keys_out = serde_json::Map::new();
375    let mut self_signing_keys_out = serde_json::Map::new();
376
377    for (mxid, requested_device_ids) in requested {
378        let allowed_user_id = crate::store::user_id_of(conn, mxid)?.filter(|uid| visible.contains(uid));
379        let Some(target_user_id) = allowed_user_id else {
380            device_keys_out.insert(mxid.clone(), serde_json::json!({}));
381            continue;
382        };
383
384        let mut devices_out = serde_json::Map::new();
385        for device_keys in crate::keys::device_keys_for(conn, &[target_user_id])? {
386            if !requested_device_ids.is_empty() && !requested_device_ids.contains(&device_keys.device_id) {
387                continue;
388            }
389            let algorithms: serde_json::Value = serde_json::from_str(&device_keys.algorithms)?;
390            let keys: serde_json::Value = serde_json::from_str(&device_keys.keys)?;
391            let signatures: serde_json::Value = serde_json::from_str(&device_keys.signatures)?;
392            devices_out.insert(
393                device_keys.device_id.clone(),
394                serde_json::json!({
395                    "user_id": mxid,
396                    "device_id": device_keys.device_id,
397                    "algorithms": algorithms,
398                    "keys": keys,
399                    "signatures": signatures,
400                }),
401            );
402        }
403        device_keys_out.insert(mxid.clone(), serde_json::Value::Object(devices_out));
404
405        for cross_signing_key in crate::keys::cross_signing_keys_for(conn, &[target_user_id])? {
406            let value: serde_json::Value = serde_json::from_str(&cross_signing_key.key_json)?;
407            match cross_signing_key.usage {
408                CrossSigningUsage::Master => {
409                    master_keys_out.insert(mxid.clone(), value);
410                }
411                CrossSigningUsage::SelfSigning => {
412                    self_signing_keys_out.insert(mxid.clone(), value);
413                }
414                CrossSigningUsage::UserSigning => {} // never exposed for anyone but the caller themself, below
415            }
416        }
417    }
418
419    let mut user_signing_keys_out = serde_json::Map::new();
420    if let Some((caller_user_id, caller_mxid)) = caller {
421        if let Some(row) = crate::keys::cross_signing_key_for(conn, caller_user_id, CrossSigningUsage::UserSigning)? {
422            user_signing_keys_out.insert(caller_mxid.to_string(), serde_json::from_str(&row.key_json)?);
423        }
424    }
425
426    Ok(serde_json::json!({
427        "device_keys": device_keys_out,
428        "master_keys": master_keys_out,
429        "self_signing_keys": self_signing_keys_out,
430        "user_signing_keys": user_signing_keys_out,
431        "failures": {},
432    }))
433}
434
435
436// ============================================================================
437// POST /_matrix/client/v3/keys/claim
438// ============================================================================
439
440#[derive(serde::Deserialize, Default)]
441pub struct KeysClaimRequest {
442    #[serde(default)]
443    pub one_time_keys: BTreeMap<String, BTreeMap<String, String>>,
444}
445
446
447/// Total number of `(mxid, device)` pairs a `/keys/claim` request asks for —
448/// the cost charged against
449/// [`crate::typing::ClaimRateLimiter`] (plan P9 brief: "≤ 100
450/// device claims / minute per caller").
451pub fn count_claim_targets(requested: &BTreeMap<String, BTreeMap<String, String>>) -> usize {
452    requested.values().map(BTreeMap::len).sum()
453}
454
455
456/// DB-only core for `POST /keys/claim` (plan P9 brief): per requested
457/// `(mxid, device, algorithm)`, claims one OTK (or a reusable fallback) —
458/// restricted to the SAME share-a-room set as `/keys/query` — silently
459/// omitting any device that yields nothing (no such device, no keys left) or
460/// any user this caller may not see.
461pub fn build_keys_claim_response(conn: &mut Connection, caller_user_id: i64, requested: &BTreeMap<String, BTreeMap<String, String>>) -> Result<serde_json::Value, MatrixError> {
462    let visible = peers_sharing_a_room_with(conn, caller_user_id)?;
463    build_keys_claim_visible(conn, &visible, requested)
464}
465
466/// [`build_keys_claim_response`] with an explicit visible-user set (federation).
467pub fn build_keys_claim_visible(conn: &mut Connection, visible: &HashSet<i64>, requested: &BTreeMap<String, BTreeMap<String, String>>) -> Result<serde_json::Value, MatrixError> {
468    let mut out = serde_json::Map::new();
469
470    for (mxid, per_device) in requested {
471        let Some(target_user_id) = crate::store::user_id_of(conn, mxid)?.filter(|uid| visible.contains(uid)) else {
472            continue;
473        };
474        let mut devices_out = serde_json::Map::new();
475        for (device_id, algorithm) in per_device {
476            let Some((key_id, key_json)) = crate::keys::claim_one_time_key(conn, target_user_id, device_id, algorithm)? else {
477                continue;
478            };
479            let value: serde_json::Value = serde_json::from_str(&key_json)?;
480            devices_out.insert(device_id.clone(), serde_json::json!({ key_id: value }));
481        }
482        if !devices_out.is_empty() {
483            out.insert(mxid.clone(), serde_json::Value::Object(devices_out));
484        }
485    }
486
487    Ok(serde_json::json!({ "one_time_keys": out, "failures": {} }))
488}
489
490
491// ============================================================================
492// GET /_matrix/client/v3/keys/changes
493// ============================================================================
494
495#[derive(serde::Deserialize, Default)]
496pub struct KeysChangesQuery {
497    pub from: Option<String>,
498    pub to: Option<String>,
499}
500
501
502/// DB-only core for `GET /keys/changes` (plan §3.7 / P9 brief): the same
503/// `device_lists` delta `/sync` reports for `(from, to]`
504/// ([`device_list_delta`]) — device-list changes and newly shared encrypted
505/// rooms among users the caller can see (`changed`), and users who no longer
506/// share any room with the caller (`left`).
507pub fn build_keys_changes_response(conn: &Connection, caller_user_id: i64, from_exclusive: i64, to_inclusive: i64) -> Result<serde_json::Value, MatrixError> {
508    let delta = device_list_delta(conn, caller_user_id, from_exclusive, to_inclusive)?;
509    Ok(serde_json::json!({ "changed": delta.changed, "left": delta.left }))
510}
511
512
513// ============================================================================
514// POST /_matrix/client/v3/keys/device_signing/upload
515// ============================================================================
516
517#[derive(serde::Deserialize, Default)]
518pub struct DeviceSigningUploadRequest {
519    #[serde(default)]
520    pub master_key: Option<serde_json::Value>,
521    #[serde(default)]
522    pub self_signing_key: Option<serde_json::Value>,
523    #[serde(default)]
524    pub user_signing_key: Option<serde_json::Value>,
525}
526
527
528/// Extract the single `ed25519:<key_id>` entry from a cross-signing key
529/// object's `keys` map (Matrix's cross-signing keys always carry exactly
530/// one) — `(key_id, decoded 32-byte verifying key)`.
531pub fn master_verifying_key(master_key_object: &serde_json::Value) -> Result<(String, VerifyingKey), MatrixError> {
532    let keys = master_key_object
533        .get("keys")
534        .and_then(|v| v.as_object())
535        .ok_or_else(|| MatrixError::invalid_param("master_key.keys missing"))?;
536    let (full_key_id, value) = keys
537        .iter()
538        .find(|(k, _)| k.starts_with("ed25519:"))
539        .ok_or_else(|| MatrixError::invalid_param("master_key has no ed25519 key"))?;
540    let key_id = full_key_id.trim_start_matches("ed25519:").to_string();
541    let b64 = value.as_str().ok_or_else(|| MatrixError::invalid_param("master_key.keys value must be a string"))?;
542    let bytes = STANDARD_NO_PAD.decode(b64).map_err(|_| MatrixError::invalid_param("master_key public key is not valid base64"))?;
543    let array: [u8; 32] = bytes.try_into().map_err(|_| MatrixError::invalid_param("master_key public key must be 32 bytes"))?;
544    let verifying_key = VerifyingKey::from_bytes(&array).map_err(|_| MatrixError::invalid_param("master_key is not a valid ed25519 key"))?;
545    Ok((key_id, verifying_key))
546}
547
548
549/// Matrix canonical JSON (RFC 8259 subset: sorted keys, no insignificant
550/// whitespace) of `value` with `signatures`/`unsigned` stripped — the exact
551/// bytes a client signs when producing a cross-signing signature.
552/// `serde_json::Value::Object` is backed by a `BTreeMap` everywhere in this
553/// workspace (no `preserve_order` feature enabled anywhere in the dependency
554/// graph — `Cargo.toml`), so keys are already sorted at every nesting level,
555/// and its compact `to_string` emits no insignificant whitespace and leaves
556/// non-ASCII UTF-8 unescaped — together already exactly canonical JSON's
557/// shape, with no extra re-serialization step needed beyond stripping the
558/// two keys.
559pub fn canonical_json_without_signatures(value: &serde_json::Value) -> String {
560    let mut stripped = value.clone();
561    if let Some(obj) = stripped.as_object_mut() {
562        obj.remove("signatures");
563        obj.remove("unsigned");
564    }
565    stripped.to_string()
566}
567
568
569/// Verify that `target_key_object` (a `self_signing_key`/`user_signing_key`
570/// upload) carries a valid ed25519 signature by `master_key_object`'s own
571/// key, filed under `signatures[caller_mxid]["ed25519:<master_key_id>"]` —
572/// Matrix's own cross-signing trust chain (plan P9 brief: "verify with
573/// ed25519 over canonical JSON").
574pub fn verify_signed_by_master(target_key_object: &serde_json::Value, caller_mxid: &str, master_key_object: &serde_json::Value) -> Result<(), MatrixError> {
575    let (master_key_id, verifying_key) = master_verifying_key(master_key_object)?;
576    let signature_b64 = target_key_object
577        .get("signatures")
578        .and_then(|s| s.get(caller_mxid))
579        .and_then(|by_user| by_user.get(format!("ed25519:{master_key_id}").as_str()))
580        .and_then(|v| v.as_str())
581        .ok_or_else(|| MatrixError::invalid_param("missing signature by the master key"))?;
582    let sig_bytes = STANDARD_NO_PAD.decode(signature_b64).map_err(|_| MatrixError::invalid_param("signature is not valid base64"))?;
583    let sig_array: [u8; 64] = sig_bytes.try_into().map_err(|_| MatrixError::invalid_param("signature must be 64 bytes"))?;
584    let signature = Signature::from_bytes(&sig_array);
585    let message = canonical_json_without_signatures(target_key_object);
586    verifying_key
587        .verify_strict(message.as_bytes(), &signature)
588        .map_err(|_| MatrixError::invalid_param("signature does not verify against the master key"))
589}
590
591
592/// `user_id`/`usage` shape validation shared by all three cross-signing key
593/// kinds (plan P9 brief).
594pub fn validate_cross_signing_key_object(value: &serde_json::Value, caller_mxid: &str, expected_usage: &str) -> Result<(), MatrixError> {
595    let obj = value.as_object().ok_or_else(|| MatrixError::invalid_param("cross-signing key must be an object"))?;
596    if obj.get("user_id").and_then(|v| v.as_str()) != Some(caller_mxid) {
597        return Err(MatrixError::invalid_param("cross-signing key user_id must be the caller"));
598    }
599    let usage = obj.get("usage").and_then(|v| v.as_array()).ok_or_else(|| MatrixError::invalid_param("cross-signing key missing usage"))?;
600    if usage.len() != 1 || usage[0].as_str() != Some(expected_usage) {
601        return Err(MatrixError::invalid_param(format!("cross-signing key usage must be exactly [\"{expected_usage}\"]")));
602    }
603    if obj.get("keys").and_then(|v| v.as_object()).is_none_or(|k| k.is_empty()) {
604        return Err(MatrixError::invalid_param("cross-signing key missing keys"));
605    }
606    Ok(())
607}
608
609
610/// DB-only core for `POST /keys/device_signing/upload` (plan P9 brief):
611/// validates each provided key object's shape/ownership, verifies
612/// self_signing/user_signing against the master key (freshly uploaded in
613/// this SAME call, or the caller's existing one), and stores whatever was
614/// provided. No UIA (single factor, matches every other route in this
615/// tree). Replacing an existing master key is allowed (user reset). Returns
616/// whether anything was actually written — the caller only wakes peers when
617/// it did.
618pub fn apply_device_signing_upload(conn: &mut Connection, caller_user_id: i64, caller_mxid: &str, request: &DeviceSigningUploadRequest, now: &str) -> Result<bool, MatrixError> {
619    if let Some(master) = &request.master_key {
620        validate_cross_signing_key_object(master, caller_mxid, "master")?;
621    }
622    let master_for_verification: Option<serde_json::Value> = match &request.master_key {
623        Some(master) => Some(master.clone()),
624        None => crate::keys::cross_signing_key_for(conn, caller_user_id, CrossSigningUsage::Master)?
625            .map(|row| serde_json::from_str(&row.key_json))
626            .transpose()?,
627    };
628
629    if let Some(self_signing) = &request.self_signing_key {
630        validate_cross_signing_key_object(self_signing, caller_mxid, "self_signing")?;
631        let master = master_for_verification
632            .as_ref()
633            .ok_or_else(|| MatrixError::invalid_param("no master key on file to verify self_signing_key against"))?;
634        verify_signed_by_master(self_signing, caller_mxid, master)?;
635    }
636    if let Some(user_signing) = &request.user_signing_key {
637        validate_cross_signing_key_object(user_signing, caller_mxid, "user_signing")?;
638        let master = master_for_verification
639            .as_ref()
640            .ok_or_else(|| MatrixError::invalid_param("no master key on file to verify user_signing_key against"))?;
641        verify_signed_by_master(user_signing, caller_mxid, master)?;
642    }
643
644    let mut wrote = false;
645    if let Some(master) = &request.master_key {
646        crate::keys::upsert_cross_signing_key(conn, caller_user_id, CrossSigningUsage::Master, &master.to_string(), now)?;
647        wrote = true;
648    }
649    if let Some(self_signing) = &request.self_signing_key {
650        crate::keys::upsert_cross_signing_key(conn, caller_user_id, CrossSigningUsage::SelfSigning, &self_signing.to_string(), now)?;
651        wrote = true;
652    }
653    if let Some(user_signing) = &request.user_signing_key {
654        crate::keys::upsert_cross_signing_key(conn, caller_user_id, CrossSigningUsage::UserSigning, &user_signing.to_string(), now)?;
655        wrote = true;
656    }
657    Ok(wrote)
658}
659
660
661// ============================================================================
662// POST /_matrix/client/v3/keys/signatures/upload
663// ============================================================================
664
665/// Whether `user_id`'s master cross-signing key names `local_id` as one of
666/// its own `ed25519:<local_id>` entries — the user-signing case's
667/// authorization check (plan P9 brief: "target must be ... another user's
668/// master key").
669pub fn master_key_names_local_id(conn: &Connection, user_id: i64, local_id: &str) -> Result<bool, MatrixError> {
670    let Some(row) = crate::keys::cross_signing_key_for(conn, user_id, CrossSigningUsage::Master)? else {
671        return Ok(false);
672    };
673    let value: serde_json::Value = serde_json::from_str(&row.key_json)?;
674    let Some(keys) = value.get("keys").and_then(|k| k.as_object()) else {
675        return Ok(false);
676    };
677    Ok(keys.keys().any(|k| k.trim_start_matches("ed25519:") == local_id))
678}
679
680
681/// Whether any of `user_id`'s own cross-signing keys names `local_id` — part
682/// of the self-signing case's authorization check (plan P9 brief: "target
683/// must be the caller's own device/keys").
684fn cross_signing_key_names_local_id(conn: &Connection, user_id: i64, local_id: &str) -> Result<bool, MatrixError> {
685    for usage in [CrossSigningUsage::Master, CrossSigningUsage::SelfSigning, CrossSigningUsage::UserSigning] {
686        let Some(row) = crate::keys::cross_signing_key_for(conn, user_id, usage)? else {
687            continue;
688        };
689        let value: serde_json::Value = serde_json::from_str(&row.key_json)?;
690        if let Some(keys) = value.get("keys").and_then(|k| k.as_object()) {
691            if keys.keys().any(|k| k.trim_start_matches("ed25519:") == local_id) {
692                return Ok(true);
693            }
694        }
695    }
696    Ok(false)
697}
698
699
700/// Whether `caller_mxid` may file a signature against `(target_user_id,
701/// target_key_id)` (plan P9 brief: "the signer must be the caller; target
702/// must be the caller's own device/keys or (for user-signing) another
703/// user's master key"). This endpoint never verifies the signature bytes
704/// themselves — only this authorization shape — matching
705/// `crate::keys::cross_signing_signatures`'s own "opaque, never
706/// verified" contract (the one deliberate exception in this module is
707/// `/keys/device_signing/upload`'s master-key check, scoped to that
708/// endpoint alone — see this module's own doc).
709pub fn signature_target_is_authorized(conn: &Connection, caller_mxid: &str, target_mxid: &str, target_user_id: i64, target_key_id: &str) -> Result<bool, MatrixError> {
710    if target_mxid == caller_mxid {
711        Ok(crate::keys::get_device(conn, target_user_id, target_key_id)?.is_some() || cross_signing_key_names_local_id(conn, target_user_id, target_key_id)?)
712    } else {
713        master_key_names_local_id(conn, target_user_id, target_key_id)
714    }
715}
716
717
718/// DB-only core for `POST /keys/signatures/upload`: per submitted
719/// `(target_user, target_key_id)` pair, checks
720/// [`signature_target_is_authorized`] and stores the whole submitted value
721/// opaquely on success ([`crate::keys::add_signatures`]), or files a
722/// per-entry `M_INVALID_PARAM` failure. Returns the `failures` map (empty on
723/// full success).
724pub fn apply_signatures_upload(conn: &mut Connection, caller_user_id: i64, caller_mxid: &str, body: &serde_json::Map<String, serde_json::Value>, now: &str) -> Result<serde_json::Value, MatrixError> {
725    let mut failures = serde_json::Map::new();
726    for (target_mxid, per_key) in body {
727        let Some(per_key_obj) = per_key.as_object() else { continue };
728        let mut user_failures = serde_json::Map::new();
729        let target_user_id = crate::store::user_id_of(conn, target_mxid)?;
730
731        for (target_key_id, signed_value) in per_key_obj {
732            let authorized = match target_user_id {
733                Some(target_user_id) => signature_target_is_authorized(conn, caller_mxid, target_mxid, target_user_id, target_key_id)?,
734                None => false,
735            };
736            if !authorized {
737                user_failures.insert(
738                    target_key_id.clone(),
739                    serde_json::json!({ "errcode": "M_INVALID_PARAM", "error": "signature target not permitted" }),
740                );
741                continue;
742            }
743            let target_user_id = target_user_id.ok_or_else(MatrixError::internal)?;
744            crate::keys::add_signatures(conn, &[(caller_user_id, target_user_id, target_key_id.clone(), signed_value.to_string(), now.to_string())])?;
745        }
746        if !user_failures.is_empty() {
747            failures.insert(target_mxid.clone(), serde_json::Value::Object(user_failures));
748        }
749    }
750    Ok(serde_json::Value::Object(failures))
751}
752
753
754// ============================================================================
755// PUT /_matrix/client/v3/sendToDevice/{eventType}/{txnId}
756// ============================================================================
757
758#[derive(serde::Deserialize)]
759pub struct SendToDeviceRequest {
760    pub messages: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
761}
762
763
764/// Resolve `messages` into a flat per-device list, expanding `"*"` to every
765/// current device of that recipient (an explicit device entry for the same
766/// recipient always overrides the wildcard's content for that one device,
767/// never both delivered) and dropping — silently, per this module's own
768/// documented policy ("no enumeration via to-device") — any recipient who
769/// does not share a joined room with the sender and is not the sender
770/// themself. Enforces the per-content-size cap up front; the per-request
771/// device-count cap is enforced once, after expansion (a caller mistake,
772/// refused outright — unlike the stranger case, a cap violation is
773/// unambiguous and refusing it leaks nothing a working client didn't already
774/// know about its own request).
775pub fn expand_send_to_device_targets(conn: &Connection, sender_user_id: i64, messages: &BTreeMap<String, BTreeMap<String, serde_json::Value>>) -> Result<Vec<(i64, String, String)>, MatrixError> {
776    let visible = peers_sharing_a_room_with(conn, sender_user_id)?;
777    let mut targets = Vec::new();
778
779    for (mxid, per_device) in messages {
780        let Some(recipient_user_id) = crate::store::user_id_of(conn, mxid)?.filter(|uid| visible.contains(uid)) else {
781            continue;
782        };
783
784        let mut per_recipient: HashMap<String, String> = HashMap::new();
785        if let Some(wildcard_content) = per_device.get("*") {
786            let content_str = wildcard_content.to_string();
787            if content_str.len() > SEND_TO_DEVICE_MAX_CONTENT_BYTES {
788                return Err(MatrixError::bad_json("to-device content too large"));
789            }
790            for device in crate::keys::list_devices(conn, recipient_user_id)? {
791                per_recipient.insert(device.device_id, content_str.clone());
792            }
793        }
794        for (device_selector, content) in per_device {
795            if device_selector == "*" {
796                continue;
797            }
798            let content_str = content.to_string();
799            if content_str.len() > SEND_TO_DEVICE_MAX_CONTENT_BYTES {
800                return Err(MatrixError::bad_json("to-device content too large"));
801            }
802            per_recipient.insert(device_selector.clone(), content_str);
803        }
804
805        for (device_id, content_str) in per_recipient {
806            targets.push((recipient_user_id, device_id, content_str));
807        }
808    }
809
810    if targets.len() > SEND_TO_DEVICE_MAX_TARGETS {
811        return Err(MatrixError::invalid_param("too many target devices in one call"));
812    }
813    Ok(targets)
814}
815
816
817#[derive(Debug, Clone, PartialEq, Eq)]
818pub enum SendToDeviceOutcome {
819    New(HashSet<i64>),
820    AlreadySent,
821}
822
823
824/// DB-only core for `PUT /sendToDevice/{eventType}/{txnId}`: txn-deduped per
825/// `(sender_user_id, sender_device_id, txn_id)` — a repeat is a no-op — then
826/// [`expand_send_to_device_targets`] and one atomic dedup-checked enqueue
827/// ([`crate::keys::enqueue_to_device_deduped`]).
828pub fn apply_send_to_device(
829    conn: &mut Connection,
830    sender_user_id: i64,
831    sender_device_id: &str,
832    event_type: &str,
833    txn_id: &str,
834    messages: &BTreeMap<String, BTreeMap<String, serde_json::Value>>,
835    now: &str,
836) -> Result<SendToDeviceOutcome, MatrixError> {
837    if let TxnDedupEntry::Seen(_) = crate::store::txn_dedup_lookup(conn, sender_user_id, sender_device_id, txn_id)? {
838        return Ok(SendToDeviceOutcome::AlreadySent);
839    }
840    let targets = expand_send_to_device_targets(conn, sender_user_id, messages)?;
841    let wake_ids: HashSet<i64> = targets.iter().map(|(uid, ..)| *uid).collect();
842    let rows: Vec<(i64, String, String, String)> = targets.into_iter().map(|(uid, dev, content)| (uid, dev, event_type.to_string(), content)).collect();
843
844    match crate::keys::enqueue_to_device_deduped(conn, sender_user_id, sender_device_id, txn_id, &rows, now)? {
845        crate::keys::ToDeviceDedupOutcome::New => Ok(SendToDeviceOutcome::New(wake_ids)),
846        crate::keys::ToDeviceDedupOutcome::AlreadySent => Ok(SendToDeviceOutcome::AlreadySent),
847    }
848}
849
850
851// ============================================================================
852// Devices: GET/DELETE_devices, GET/PUT/DELETE devices/{id}, POST delete_devices
853// ============================================================================
854
855#[derive(serde::Deserialize, Default)]
856pub struct PutDeviceRequest {
857    #[serde(default)]
858    pub display_name: Option<String>,
859}
860
861
862#[derive(serde::Deserialize)]
863pub struct DeleteDevicesRequest {
864    pub devices: Vec<String>,
865}
866
867
868pub fn device_to_json(device: &crate::keys::Device) -> serde_json::Value {
869    let last_seen_ts = chrono::DateTime::parse_from_rfc3339(&device.last_seen_at).ok().map(|dt| dt.timestamp_millis());
870    serde_json::json!({
871        "device_id": device.device_id,
872        "display_name": device.display_name,
873        "last_seen_ts": last_seen_ts,
874    })
875}
876
877
878// ============================================================================
879// Key backup: room_keys/version[/{version}]
880// ============================================================================
881
882#[derive(serde::Deserialize)]
883pub struct BackupVersionCreateRequest {
884    pub algorithm: String,
885    pub auth_data: serde_json::Value,
886}
887
888
889#[derive(serde::Deserialize)]
890pub struct BackupVersionUpdateRequest {
891    #[serde(default)]
892    pub algorithm: Option<String>,
893    pub auth_data: serde_json::Value,
894}
895
896
897/// Refuse anything but this server's one symmetric key-backup algorithm
898/// (plan P9 brief, MSC3270 naming) — `m.megolm_backup.v1.curve25519-aes-sha2`
899/// (the PkEncryption backup) is refused outright.
900pub fn validate_backup_algorithm(algorithm: &str) -> Result<(), MatrixError> {
901    if algorithm == BACKUP_ALGORITHM {
902        Ok(())
903    } else {
904        Err(MatrixError::invalid_param(format!("unsupported key-backup algorithm: {algorithm}")))
905    }
906}
907
908
909pub fn backup_version_to_response(row: &crate::keys::KeyBackupVersion, count: i64) -> Result<serde_json::Value, MatrixError> {
910    Ok(serde_json::json!({
911        "version": row.version.to_string(),
912        "algorithm": row.algorithm,
913        "auth_data": serde_json::from_str::<serde_json::Value>(&row.auth_data)?,
914        "etag": row.etag.to_string(),
915        "count": count,
916    }))
917}
918
919
920// ============================================================================
921// Key backup: room_keys/keys[/{roomId}[/{sessionId}]]
922// ============================================================================
923
924#[derive(serde::Deserialize, Default)]
925pub struct VersionQuery {
926    pub version: Option<String>,
927}
928
929
930pub fn parse_required_version(raw: Option<&str>) -> Result<i64, MatrixError> {
931    let raw = raw.ok_or_else(|| MatrixError::invalid_param("version is required"))?;
932    raw.parse::<i64>().map_err(|_| MatrixError::invalid_param("version must be an integer"))
933}
934
935
936/// `?version=` must name the caller's CURRENT (non-deleted) backup version —
937/// plan P9 brief: "wrong/stale version → 403 M_WRONG_ROOM_KEYS_VERSION with
938/// current_version". Applies uniformly to GET/PUT/DELETE
939/// `room_keys/keys[...]`.
940pub fn require_current_backup_version(conn: &Connection, user_id: i64, requested_version: i64) -> Result<(), MatrixError> {
941    let current = crate::keys::current_backup_version(conn, user_id)?;
942    match current {
943        Some(row) if row.version == requested_version => Ok(()),
944        Some(row) => Err(MatrixError::wrong_room_keys_version(Some(row.version))),
945        None => Err(MatrixError::wrong_room_keys_version(None)),
946    }
947}
948
949
950/// Normalize the three `PUT room_keys/keys[...]` body shapes into a flat
951/// `(room_id, session_id, session_data_json)` list — `session_data_json` is
952/// the WHOLE submitted `KeyBackupData` object (opaque to this server, see
953/// `crate::keys::key_backup_sessions.session_data`'s own doc), not
954/// just its inner `session_data` field.
955pub fn normalize_put_backup_body(room_id: Option<&str>, session_id: Option<&str>, body: &serde_json::Value) -> Result<Vec<(String, String, String)>, MatrixError> {
956    match (room_id, session_id) {
957        (Some(room_id), Some(session_id)) => Ok(vec![(room_id.to_string(), session_id.to_string(), body.to_string())]),
958        (Some(room_id), None) => {
959            let sessions = body
960                .get("sessions")
961                .and_then(|v| v.as_object())
962                .ok_or_else(|| MatrixError::invalid_param("body must have a sessions object"))?;
963            Ok(sessions.iter().map(|(sid, data)| (room_id.to_string(), sid.clone(), data.to_string())).collect())
964        }
965        (None, _) => {
966            let rooms = body
967                .get("rooms")
968                .and_then(|v| v.as_object())
969                .ok_or_else(|| MatrixError::invalid_param("body must have a rooms object"))?;
970            let mut out = Vec::new();
971            for (rid, room_value) in rooms {
972                let sessions = room_value
973                    .get("sessions")
974                    .and_then(|v| v.as_object())
975                    .ok_or_else(|| MatrixError::invalid_param("each room must have a sessions object"))?;
976                for (sid, data) in sessions {
977                    out.push((rid.clone(), sid.clone(), data.to_string()));
978                }
979            }
980            Ok(out)
981        }
982    }
983}
984
985
986/// Shape a batch of stored [`crate::keys::KeyBackupSession`] rows into
987/// the GET response's three tiers.
988pub fn backup_sessions_to_response(room_id: Option<&str>, session_id: Option<&str>, sessions: Vec<crate::keys::KeyBackupSession>) -> Result<serde_json::Value, MatrixError> {
989    match (room_id, session_id) {
990        (Some(_), Some(_)) => {
991            let one = sessions.into_iter().next().ok_or_else(|| MatrixError::not_found("no such backup session"))?;
992            Ok(serde_json::from_str(&one.session_data)?)
993        }
994        (Some(_), None) => {
995            let mut sessions_out = serde_json::Map::new();
996            for s in sessions {
997                sessions_out.insert(s.session_id.clone(), serde_json::from_str(&s.session_data)?);
998            }
999            Ok(serde_json::json!({ "sessions": sessions_out }))
1000        }
1001        (None, _) => {
1002            let mut rooms_out = serde_json::Map::new();
1003            for s in sessions {
1004                let room_entry = rooms_out.entry(s.room_id.clone()).or_insert_with(|| serde_json::json!({ "sessions": {} }));
1005                room_entry["sessions"][s.session_id.as_str()] = serde_json::from_str(&s.session_data)?;
1006            }
1007            Ok(serde_json::json!({ "rooms": rooms_out }))
1008        }
1009    }
1010}
1011
1012
1013#[cfg(test)]
1014mod device_keys_upload_tests {
1015    use super::*;
1016    use rusqlite::Connection;
1017
1018    const T0: &str = "2026-10-06T00:00:00+00:00";
1019
1020    fn test_conn() -> Connection {
1021        let conn = Connection::open_in_memory().expect("memory");
1022        crate::store::create_matrix_schema(&conn).expect("schema");
1023        crate::keys::create_matrix_keys_schema(&conn).expect("keys");
1024        conn
1025    }
1026
1027    #[test]
1028    fn decide_insert_noop_and_reset() {
1029        let keys_a = serde_json::json!({"curve25519:D":"aaa","ed25519:D":"bbb"});
1030        let keys_b = serde_json::json!({"curve25519:D":"ccc","ed25519:D":"ddd"});
1031        assert_eq!(decide_device_keys_upload(None, &keys_a).unwrap(), DeviceKeysUploadAction::Insert);
1032        assert_eq!(
1033            decide_device_keys_upload(Some(&keys_a.to_string()), &keys_a).unwrap(),
1034            DeviceKeysUploadAction::Noop
1035        );
1036        assert_eq!(
1037            decide_device_keys_upload(Some(&keys_a.to_string()), &keys_b).unwrap(),
1038            DeviceKeysUploadAction::Reset
1039        );
1040    }
1041
1042    #[test]
1043    fn apply_keys_upload_reset_replaces_identity_clears_otks_and_logs_change() {
1044        let mut conn = test_conn();
1045        crate::store::ensure_matrix_user(&conn, 1, "alice00000000000000000000000001", T0).expect("user");
1046        let device_id = crate::keys::create_device(&conn, 1, crate::keys::CredentialKind::Bearer, "tok", T0).expect("device");
1047        let mxid = crate::store::mxid_of(&conn, 1).expect("mxid").expect("row");
1048
1049        let first = KeysUploadRequest {
1050            device_keys: Some(serde_json::json!({
1051                "user_id": mxid,
1052                "device_id": device_id,
1053                "algorithms": ["m.olm.v1.curve25519-aes-sha2"],
1054                "keys": {"curve25519:D":"oldcurve","ed25519:D":"olded"},
1055                "signatures": {}
1056            })),
1057            one_time_keys: Some(std::collections::BTreeMap::from([(
1058                "signed_curve25519:AAAAAQ".to_string(),
1059                serde_json::json!({"key":"otk1"}),
1060            )])),
1061            fallback_keys: Some(std::collections::BTreeMap::from([(
1062                "signed_curve25519:FALLBACK".to_string(),
1063                serde_json::json!({"key":"fb1"}),
1064            )])),
1065        };
1066        let first_out = apply_keys_upload(&mut conn, 1, &mxid, &device_id, &first, T0).expect("first");
1067        assert!(first_out.device_keys_changed);
1068        assert_eq!(crate::keys::count_one_time_keys(&conn, 1, &device_id).unwrap().get("signed_curve25519"), Some(&1));
1069
1070        let before_changes: i64 = conn
1071            .query_row("SELECT COUNT(*) FROM device_list_changes WHERE user_id = 1", [], |r| r.get(0))
1072            .unwrap();
1073
1074        let reset = KeysUploadRequest {
1075            device_keys: Some(serde_json::json!({
1076                "user_id": mxid,
1077                "device_id": device_id,
1078                "algorithms": ["m.olm.v1.curve25519-aes-sha2"],
1079                "keys": {"curve25519:D":"newcurve","ed25519:D":"newed"},
1080                "signatures": {}
1081            })),
1082            one_time_keys: Some(std::collections::BTreeMap::from([(
1083                "signed_curve25519:BBBBBQ".to_string(),
1084                serde_json::json!({"key":"otk2"}),
1085            )])),
1086            fallback_keys: None,
1087        };
1088        let reset_out = apply_keys_upload(&mut conn, 1, &mxid, &device_id, &reset, T0).expect("reset");
1089        assert!(reset_out.device_keys_changed);
1090
1091        let stored = crate::keys::device_keys_for(&conn, &[1]).unwrap();
1092        assert_eq!(stored.len(), 1);
1093        assert!(stored[0].keys.contains("newcurve"), "new identity stored: {}", stored[0].keys);
1094        assert!(!stored[0].keys.contains("oldcurve"));
1095
1096        let otk = crate::keys::count_one_time_keys(&conn, 1, &device_id).unwrap();
1097        assert_eq!(otk.get("signed_curve25519"), Some(&1), "old OTKs wiped; only the new batch remains");
1098        let fallback = crate::keys::unused_fallback_key_types(&conn, 1, &device_id).unwrap();
1099        assert!(fallback.is_empty(), "old fallback wiped on reset");
1100
1101        let after_changes: i64 = conn
1102            .query_row("SELECT COUNT(*) FROM device_list_changes WHERE user_id = 1", [], |r| r.get(0))
1103            .unwrap();
1104        assert_eq!(after_changes, before_changes + 1, "reset logs device_lists.changed");
1105    }
1106}