Skip to content

Krille/refactor user device keys#2414

Draft
krille-chan wants to merge 6 commits into
mainfrom
krille/refactor-user-device-keys
Draft

Krille/refactor user device keys#2414
krille-chan wants to merge 6 commits into
mainfrom
krille/refactor-user-device-keys

Conversation

@krille-chan

@krille-chan krille-chan commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

closes https://famedly.atlassian.net/browse/FM-758

What does this change?

We no longer load all user device keys on app start and we no longer update them on every sync. Instead we load and update them on demand. Means Client.userDeviceKeys[userId] has been replaced by the async Client.fetchUserDeviceKeysList(userId). This needed a very huge refactoring of the SDK as we no longer hold keys in memory but have them all database centric.

This should make the whole key handling more stable as we have no more in memory cache which can get outdated. It should also improve the performance as we no longer need to load all keys on app start. It should also speed up sync as we no longer update outdated keys on every sync.
Instead it could slow down sending the first encrypted message in a room for a few milliseconds. But this should actually be the better deal.

TODOS:

  • In my life test with Famedly App it seems for some reason to call POST /keys/query more often than I would expect (while not thaaat much often)
  • Provide full live demo of Famedly App and FluffyChat with this

@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches core E2EE trust, key distribution, and verification with a breaking public API and changed sync/key-fetch timing, which can affect who receives room keys and when keys are considered verified.

Overview
Breaking refactor of how device and cross-signing keys are stored and accessed. The client no longer keeps a global in-memory userDeviceKeys map or runs updateUserDeviceKeys() after every sync; callers must use await client.fetchUserDeviceKeysLists(userIds), which merges per-user DB cache with /keys/query when lists are missing or outdated.

Trust and encryption eligibility move to async APIs on keys (verified, encryptToDevice, hasValidSignatureChain, signed, isUnknownSession, etc.), with signature validation fetching keys on demand and optional prefetchedKeys / keysList binding so in-memory verification state stays consistent. Init drops userDeviceKeysLoading; device-list sync events only mark users outdated in the database instead of maintaining tracked encrypted-room user sets.

Encryption paths (megolm session setup, olm, SSSS, cross-signing, verification, VoIP to-device) and Room.getUserDeviceKeys are updated to fetch keys for the users they need at use time. Docs and tests follow the new API; fake API cross-signing upload persists via the database layer.

Reviewed by Cursor Bugbot for commit 6e6561d. Bugbot is set up for automated code reviews on this repo. Configure here.

@krille-chan
krille-chan marked this pull request as draft July 16, 2026 15:34

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Trust state lost on refresh
    • The outdated cached lists collected in oldDeviceKeys are now used as the base (via oldDeviceKeys.remove) when merging fresh server keys, and any user omitted by the server is retained, preserving verification/blocked/TOFU/signature state.
  • ✅ Fixed: Verification skips key refetch
    • The redundant nested checks in the Request/Ready/Start branches now mark the user outdated and refetch device keys from the server before cancelling, restoring the pre-refactor refetch behavior.

Create PR

Or push these changes by commenting:

@cursor push e86c84d55b
Preview (e86c84d55b)
diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart
--- a/lib/encryption/utils/key_verification.dart
+++ b/lib/encryption/utils/key_verification.dart
@@ -367,7 +367,7 @@
     Logs().i('[Key Verification] Received type $type: $payload');
     // Fetch once upfront to avoid multiple DB round-trips within the switch.
     final knownMethods = await knownVerificationMethods;
-    final userKeys = await client.fetchUserDeviceKeysLists({userId});
+    var userKeys = await client.fetchUserDeviceKeysLists({userId});
     try {
       var thisLastStep = lastStep;
       switch (type) {
@@ -392,6 +392,10 @@
 
           // ensure we have the other sides keys
           if (userKeys[userId]?.deviceKeys[deviceId!] == null) {
+            // The device might be new and not yet in our cache, so force a
+            // refresh from the server before giving up.
+            await client.database.storeUserDeviceKeysInfo(userId, true);
+            userKeys = await client.fetchUserDeviceKeysLists({userId});
             if (userKeys[userId]?.deviceKeys[deviceId!] == null) {
               await cancel('im.fluffychat.unknown_device');
               return;
@@ -438,6 +442,10 @@
 
           // ensure we have the other sides keys
           if (userKeys[userId]?.deviceKeys[deviceId!] == null) {
+            // The device might be new and not yet in our cache, so force a
+            // refresh from the server before giving up.
+            await client.database.storeUserDeviceKeysInfo(userId, true);
+            userKeys = await client.fetchUserDeviceKeysLists({userId});
             if (userKeys[userId]?.deviceKeys[deviceId!] == null) {
               await cancel('im.fluffychat.unknown_device');
               return;
@@ -524,6 +532,10 @@
 
           // ensure we have the other sides keys
           if (userKeys[userId]?.deviceKeys[deviceId!] == null) {
+            // The device might be new and not yet in our cache, so force a
+            // refresh from the server before giving up.
+            await client.database.storeUserDeviceKeysInfo(userId, true);
+            userKeys = await client.fetchUserDeviceKeysLists({userId});
             if (userKeys[userId]?.deviceKeys[deviceId!] == null) {
               await cancel('im.fluffychat.unknown_device');
               return;

diff --git a/lib/src/client.dart b/lib/src/client.dart
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -3311,10 +3311,8 @@
     if (deviceKeys != null) {
       for (final rawDeviceKeyListEntry in deviceKeys.entries) {
         final userId = rawDeviceKeyListEntry.key;
-        final userKeys = userDeviceKeys[userId] ??= DeviceKeysList(
-          userId,
-          this,
-        );
+        final userKeys = userDeviceKeys[userId] ??=
+            oldDeviceKeys.remove(userId) ?? DeviceKeysList(userId, this);
         final oldKeys = Map<String, DeviceKeys>.from(userKeys.deviceKeys);
         userKeys.deviceKeys.clear();
         for (final rawDeviceKeyEntry in rawDeviceKeyListEntry.value.entries) {
@@ -3433,10 +3431,8 @@
       }
       for (final crossSigningKeyListEntry in keys.entries) {
         final userId = crossSigningKeyListEntry.key;
-        final userKeys = userDeviceKeys[userId] ??= DeviceKeysList(
-          userId,
-          this,
-        );
+        final userKeys = userDeviceKeys[userId] ??=
+            oldDeviceKeys.remove(userId) ?? DeviceKeysList(userId, this);
         final oldKeys = Map<String, CrossSigningKey>.from(
           userKeys.crossSigningKeys,
         );
@@ -3496,6 +3492,12 @@
       }
     }
 
+    // Keep the previously cached (still outdated) lists for any user the
+    // server failed to return, so we don't lose their trust state.
+    for (final oldEntry in oldDeviceKeys.entries) {
+      userDeviceKeys.putIfAbsent(oldEntry.key, () => oldEntry.value);
+    }
+
     // now process all the failures
     if (response.failures != null) {
       for (final failureDomain in response.failures?.keys ?? <String>[]) {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 6e6561d. Configure here.

Comment thread lib/src/client.dart
Comment thread lib/encryption/utils/key_verification.dart
@krille-chan
krille-chan force-pushed the krille/refactor-user-device-keys branch from 6e6561d to 630ca76 Compare July 16, 2026 15:44

@td-famedly td-famedly left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will block this for my review. Please ping me once it's ready

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.69725% with 58 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.74%. Comparing base (5c1c48a) to head (5b113af).

Files with missing lines Patch % Lines
lib/src/client.dart 86.95% 21 Missing ⚠️
lib/encryption/utils/key_verification.dart 86.56% 9 Missing ⚠️
lib/fake_matrix_api.dart 75.75% 8 Missing ⚠️
lib/src/voip/backend/livekit_backend.dart 0.00% 7 Missing ⚠️
...ehydrated_devices/msc_3814_dehydrated_devices.dart 0.00% 4 Missing ⚠️
lib/encryption/encryption.dart 0.00% 3 Missing ⚠️
lib/src/database/matrix_sdk_database.dart 94.64% 3 Missing ⚠️
lib/src/voip/call_session.dart 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2414      +/-   ##
==========================================
+ Coverage   59.66%   59.74%   +0.08%     
==========================================
  Files         161      161              
  Lines       20329    20398      +69     
==========================================
+ Hits        12129    12187      +58     
- Misses       8200     8211      +11     
Files with missing lines Coverage Δ
lib/encryption/cross_signing.dart 93.25% <100.00%> (+0.23%) ⬆️
lib/encryption/key_manager.dart 87.09% <100.00%> (-0.26%) ⬇️
lib/encryption/olm_manager.dart 87.31% <100.00%> (+1.55%) ⬆️
lib/encryption/ssss.dart 86.82% <100.00%> (-0.27%) ⬇️
lib/encryption/utils/bootstrap.dart 83.68% <100.00%> (+0.90%) ⬆️
lib/src/database/database_api.dart 0.00% <ø> (ø)
lib/src/room.dart 76.31% <100.00%> (-0.13%) ⬇️
lib/src/utils/device_keys_list.dart 90.90% <100.00%> (+0.13%) ⬆️
lib/encryption/encryption.dart 84.09% <0.00%> (-0.49%) ⬇️
lib/src/database/matrix_sdk_database.dart 92.46% <94.64%> (+0.73%) ⬆️
... and 6 more

... and 2 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5c1c48a...5b113af. Read the comment docs.

@krille-chan
krille-chan force-pushed the krille/refactor-user-device-keys branch 8 times, most recently from 541dd05 to 8e9b093 Compare July 17, 2026 13:02
@krille-chan
krille-chan force-pushed the krille/refactor-user-device-keys branch from 6e4a21f to 5b113af Compare July 18, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants