2323import logging
2424import re
2525from 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
2728
29+ from typing_extensions import Self
30+
31+ from synapse ._pydantic_compat import (
32+ StrictBool ,
33+ StrictStr ,
34+ validator ,
35+ )
2836from synapse .api .auth .mas import MasDelegatedAuth
2937from synapse .api .errors import (
38+ Codes ,
3039 InteractiveAuthIncompleteError ,
3140 InvalidAPICallError ,
3241 SynapseError ,
3746 parse_integer ,
3847 parse_json_object_from_request ,
3948 parse_string ,
49+ validate_json_object ,
4050)
4151from synapse .http .site import SynapseRequest
4252from synapse .logging .opentracing import log_kv , set_tag
4353from synapse .rest .client ._base import client_patterns , interactive_auth_handler
4454from synapse .types import JsonDict , StreamToken
55+ from synapse .types .rest import RequestBodyModel
4556from synapse .util .cancellation import cancellable
4657
4758if TYPE_CHECKING :
@@ -59,7 +70,6 @@ class KeyUploadServlet(RestServlet):
5970 "device_keys": {
6071 "user_id": "<user_id>",
6172 "device_id": "<device_id>",
62- "valid_until_ts": <millisecond_timestamp>,
6373 "algorithms": [
6474 "m.olm.curve25519-aes-sha2",
6575 ]
@@ -111,12 +121,123 @@ def __init__(self, hs: "HomeServer"):
111121 self ._clock = hs .get_clock ()
112122 self ._store = hs .get_datastores ().main
113123
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+
114226 async def on_POST (
115227 self , request : SynapseRequest , device_id : Optional [str ]
116228 ) -> Tuple [int , JsonDict ]:
117229 requester = await self .auth .get_user_by_req (request , allow_guest = True )
118230 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.
119239 body = parse_json_object_from_request (request )
240+ validate_json_object (body , self .KeyUploadRequestBody )
120241
121242 if device_id is not None :
122243 # Providing the device_id should only be done for setting keys
@@ -149,8 +270,31 @@ async def on_POST(
149270 400 , "To upload keys, you must pass device_id when authenticating"
150271 )
151272
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+
152294 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 ,
154298 )
155299
156300 return 200 , result
0 commit comments