diff --git a/changelog.d/10485.bugfix b/changelog.d/10485.bugfix
new file mode 100644
index 0000000000..9b44006dc0
--- /dev/null
+++ b/changelog.d/10485.bugfix
@@ -0,0 +1 @@
+Fix a long-standing bug where Synapse would not inform clients that a device had exhausted its one-time-key pool, potentially causing problems decrypting events.
diff --git a/synapse/api/constants.py b/synapse/api/constants.py
index 8363c2bb0f..8c7ad2a407 100644
--- a/synapse/api/constants.py
+++ b/synapse/api/constants.py
@@ -127,6 +127,14 @@ class ToDeviceEventTypes:
RoomKeyRequest = "m.room_key_request"
+class DeviceKeyAlgorithms:
+ """Spec'd algorithms for the generation of per-device keys"""
+
+ ED25519 = "ed25519"
+ CURVE25519 = "curve25519"
+ SIGNED_CURVE25519 = "signed_curve25519"
+
+
class EduTypes:
Presence = "m.presence"
diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py
index 150a4f291e..f30bfcc93c 100644
--- a/synapse/handlers/sync.py
+++ b/synapse/handlers/sync.py
@@ -1093,6 +1093,10 @@ class SyncHandler:
one_time_key_counts: JsonDict = {}
unused_fallback_key_types: List[str] = []
if device_id:
+ # TODO: We should have a way to let clients differentiate between the states of:
+ # * no change in OTK count since the provided since token
+ # * the server has zero OTKs left for this device
+ # Spec issue: https://github.com/matrix-org/matrix-doc/issues/3298
one_time_key_counts = await self.store.count_e2e_one_time_keys(
user_id, device_id
)
diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py
index 78ae68ec68..1edc96042b 100644
--- a/synapse/storage/databases/main/end_to_end_keys.py
+++ b/synapse/storage/databases/main/end_to_end_keys.py
@@ -21,6 +21,7 @@ from canonicaljson import encode_canonical_json
from twisted.enterprise.adbapi import Connection
+from synapse.api.constants import DeviceKeyAlgorithms
from synapse.logging.opentracing import log_kv, set_tag, trace
from synapse.storage._base import SQLBaseStore, db_to_json
from synapse.storage.database import DatabasePool, make_in_list_sql_clause
@@ -381,9 +382,15 @@ class EndToEndKeyWorkerStore(EndToEndKeyBackgroundStore):
" GROUP BY algorithm"
)
txn.execute(sql, (user_id, device_id))
- result = {}
+
+ # Initially set the key count to 0. This ensures that the client will always
+ # receive *some count*, even if it's 0.
+ result = {DeviceKeyAlgorithms.SIGNED_CURVE25519: 0}
+
+ # Override entries with the count of any keys we pulled from the database
for algorithm, key_count in txn:
result[algorithm] = key_count
+
return result
return await self.db_pool.runInteraction(
diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py
index e0a24824cc..39e7b1ab25 100644
--- a/tests/handlers/test_e2e_keys.py
+++ b/tests/handlers/test_e2e_keys.py
@@ -47,12 +47,16 @@ class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
"alg2:k3": {"key": "key3"},
}
+ # Note that "signed_curve25519" is always returned in key count responses. This is necessary until
+ # https://github.com/matrix-org/matrix-doc/issues/3298 is fixed.
res = self.get_success(
self.handler.upload_keys_for_user(
local_user, device_id, {"one_time_keys": keys}
)
)
- self.assertDictEqual(res, {"one_time_key_counts": {"alg1": 1, "alg2": 2}})
+ self.assertDictEqual(
+ res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
+ )
# we should be able to change the signature without a problem
keys["alg2:k2"]["signatures"]["k1"] = "sig2"
@@ -61,7 +65,9 @@ class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
local_user, device_id, {"one_time_keys": keys}
)
)
- self.assertDictEqual(res, {"one_time_key_counts": {"alg1": 1, "alg2": 2}})
+ self.assertDictEqual(
+ res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
+ )
def test_change_one_time_keys(self):
"""attempts to change one-time-keys should be rejected"""
@@ -79,7 +85,9 @@ class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
local_user, device_id, {"one_time_keys": keys}
)
)
- self.assertDictEqual(res, {"one_time_key_counts": {"alg1": 1, "alg2": 2}})
+ self.assertDictEqual(
+ res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
+ )
# Error when changing string key
self.get_failure(
@@ -89,7 +97,7 @@ class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
SynapseError,
)
- # Error when replacing dict key with strin
+ # Error when replacing dict key with string
self.get_failure(
self.handler.upload_keys_for_user(
local_user, device_id, {"one_time_keys": {"alg2:k3": "key2"}}
@@ -131,7 +139,9 @@ class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
local_user, device_id, {"one_time_keys": keys}
)
)
- self.assertDictEqual(res, {"one_time_key_counts": {"alg1": 1}})
+ self.assertDictEqual(
+ res, {"one_time_key_counts": {"alg1": 1, "signed_curve25519": 0}}
+ )
res2 = self.get_success(
self.handler.claim_one_time_keys(
|