23
23
import logging
24
24
import re
25
25
from collections import Counter
26
- from typing import TYPE_CHECKING , Any , Dict , Optional , Tuple
26
+ from http import HTTPStatus
27
+ from typing import TYPE_CHECKING , Any , Dict , List , Mapping , Optional , Tuple , Union
27
28
29
+ from typing_extensions import Self
30
+
31
+ from synapse ._pydantic_compat import (
32
+ StrictBool ,
33
+ StrictStr ,
34
+ validator ,
35
+ )
28
36
from synapse .api .auth .mas import MasDelegatedAuth
29
37
from synapse .api .errors import (
38
+ Codes ,
30
39
InteractiveAuthIncompleteError ,
31
40
InvalidAPICallError ,
32
41
SynapseError ,
37
46
parse_integer ,
38
47
parse_json_object_from_request ,
39
48
parse_string ,
49
+ validate_json_object ,
40
50
)
41
51
from synapse .http .site import SynapseRequest
42
52
from synapse .logging .opentracing import log_kv , set_tag
43
53
from synapse .rest .client ._base import client_patterns , interactive_auth_handler
44
54
from synapse .types import JsonDict , StreamToken
55
+ from synapse .types .rest import RequestBodyModel
45
56
from synapse .util .cancellation import cancellable
46
57
47
58
if TYPE_CHECKING :
@@ -59,7 +70,6 @@ class KeyUploadServlet(RestServlet):
59
70
"device_keys": {
60
71
"user_id": "<user_id>",
61
72
"device_id": "<device_id>",
62
- "valid_until_ts": <millisecond_timestamp>,
63
73
"algorithms": [
64
74
"m.olm.curve25519-aes-sha2",
65
75
]
@@ -111,12 +121,123 @@ def __init__(self, hs: "HomeServer"):
111
121
self ._clock = hs .get_clock ()
112
122
self ._store = hs .get_datastores ().main
113
123
124
+ class KeyUploadRequestBody (RequestBodyModel ):
125
+ """
126
+ The body of a `POST /_matrix/client/v3/keys/upload` request.
127
+
128
+ Based on https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3keysupload.
129
+ """
130
+
131
+ class DeviceKeys (RequestBodyModel ):
132
+ algorithms : List [StrictStr ]
133
+ """The encryption algorithms supported by this device."""
134
+
135
+ device_id : StrictStr
136
+ """The ID of the device these keys belong to. Must match the device ID used when logging in."""
137
+
138
+ keys : Mapping [StrictStr , StrictStr ]
139
+ """
140
+ Public identity keys. The names of the properties should be in the
141
+ format `<algorithm>:<device_id>`. The keys themselves should be encoded as
142
+ specified by the key algorithm.
143
+ """
144
+
145
+ signatures : Mapping [StrictStr , Mapping [StrictStr , StrictStr ]]
146
+ """Signatures for the device key object. A map from user ID, to a map from "<algorithm>:<device_id>" to the signature."""
147
+
148
+ user_id : StrictStr
149
+ """The ID of the user the device belongs to. Must match the user ID used when logging in."""
150
+
151
+ class KeyObject (RequestBodyModel ):
152
+ key : StrictStr
153
+ """The key, encoded using unpadded base64."""
154
+
155
+ fallback : Optional [StrictBool ] = False
156
+ """Whether this is a fallback key. Only used when handling fallback keys."""
157
+
158
+ signatures : Mapping [StrictStr , Mapping [StrictStr , StrictStr ]]
159
+ """Signature for the device. Mapped from user ID to another map of key signing identifier to the signature itself.
160
+
161
+ See the following for more detail: https://spec.matrix.org/v1.16/appendices/#signing-details
162
+ """
163
+
164
+ device_keys : Optional [DeviceKeys ] = None
165
+ """Identity keys for the device. May be absent if no new identity keys are required."""
166
+
167
+ fallback_keys : Optional [Mapping [StrictStr , Union [StrictStr , KeyObject ]]]
168
+ """
169
+ The public key which should be used if the device's one-time keys are
170
+ exhausted. The fallback key is not deleted once used, but should be
171
+ replaced when additional one-time keys are being uploaded. The server
172
+ will notify the client of the fallback key being used through `/sync`.
173
+
174
+ There can only be at most one key per algorithm uploaded, and the server
175
+ will only persist one key per algorithm.
176
+
177
+ When uploading a signed key, an additional fallback: true key should be
178
+ included to denote that the key is a fallback key.
179
+
180
+ May be absent if a new fallback key is not required.
181
+ """
182
+
183
+ @validator ("fallback_keys" , pre = True )
184
+ def validate_fallback_keys (cls : Self , v : Any ) -> Any :
185
+ if v is None :
186
+ return v
187
+ if not isinstance (v , dict ):
188
+ raise TypeError ("fallback_keys must be a mapping" )
189
+
190
+ for k in v .keys ():
191
+ if not len (k .split (":" )) == 2 :
192
+ raise SynapseError (
193
+ code = HTTPStatus .BAD_REQUEST ,
194
+ errcode = Codes .BAD_JSON ,
195
+ msg = f"Invalid fallback_keys key { k !r} . "
196
+ 'Expected "<algorithm>:<device_id>".' ,
197
+ )
198
+ return v
199
+
200
+ one_time_keys : Optional [Mapping [StrictStr , Union [StrictStr , KeyObject ]]] = None
201
+ """
202
+ One-time public keys for "pre-key" messages. The names of the properties
203
+ should be in the format `<algorithm>:<key_id>`.
204
+
205
+ The format of the key is determined by the key algorithm, see:
206
+ https://spec.matrix.org/v1.16/client-server-api/#key-algorithms.
207
+ """
208
+
209
+ @validator ("one_time_keys" , pre = True )
210
+ def validate_one_time_keys (cls : Self , v : Any ) -> Any :
211
+ if v is None :
212
+ return v
213
+ if not isinstance (v , dict ):
214
+ raise TypeError ("one_time_keys must be a mapping" )
215
+
216
+ for k , _ in v .items ():
217
+ if not len (k .split (":" )) == 2 :
218
+ raise SynapseError (
219
+ code = HTTPStatus .BAD_REQUEST ,
220
+ errcode = Codes .BAD_JSON ,
221
+ msg = f"Invalid one_time_keys key { k !r} . "
222
+ 'Expected "<algorithm>:<device_id>".' ,
223
+ )
224
+ return v
225
+
114
226
async def on_POST (
115
227
self , request : SynapseRequest , device_id : Optional [str ]
116
228
) -> Tuple [int , JsonDict ]:
117
229
requester = await self .auth .get_user_by_req (request , allow_guest = True )
118
230
user_id = requester .user .to_string ()
231
+
232
+ # Parse the request body. Validate separately, as the handler expects a
233
+ # plain dict, rather than any parsed object.
234
+ #
235
+ # Note: It would be nice to work with a parsed object, but the handler
236
+ # needs to encode portions of the request body as canonical JSON before
237
+ # storing the result in the DB. There's little point in converted to a
238
+ # parsed object and then back to a dict.
119
239
body = parse_json_object_from_request (request )
240
+ validate_json_object (body , self .KeyUploadRequestBody )
120
241
121
242
if device_id is not None :
122
243
# Providing the device_id should only be done for setting keys
@@ -149,8 +270,31 @@ async def on_POST(
149
270
400 , "To upload keys, you must pass device_id when authenticating"
150
271
)
151
272
273
+ if "device_keys" in body :
274
+ # Validate the provided `user_id` and `device_id` fields in
275
+ # `device_keys` match that of the requesting user. We can't do
276
+ # this directly in the pydantic model as we don't have access
277
+ # to the requester yet.
278
+ #
279
+ # TODO: We could use ValidationInfo when we switch to Pydantic v2.
280
+ # https://docs.pydantic.dev/latest/concepts/validators/#validation-info
281
+ if body ["device_keys" ]["user_id" ] != user_id :
282
+ raise SynapseError (
283
+ code = HTTPStatus .BAD_REQUEST ,
284
+ errcode = Codes .BAD_JSON ,
285
+ msg = "Provided `user_id` in `device_keys` does not match that of the authenticated user" ,
286
+ )
287
+ if body ["device_keys" ]["device_id" ] != device_id :
288
+ raise SynapseError (
289
+ code = HTTPStatus .BAD_REQUEST ,
290
+ errcode = Codes .BAD_JSON ,
291
+ msg = "Provided `device_id` in `device_keys` does not match that of the authenticated user device" ,
292
+ )
293
+
152
294
result = await self .e2e_keys_handler .upload_keys_for_user (
153
- user_id = user_id , device_id = device_id , keys = body
295
+ user_id = user_id ,
296
+ device_id = device_id ,
297
+ keys = body ,
154
298
)
155
299
156
300
return 200 , result
0 commit comments