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