room_keys.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2017, 2018 New Vector Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from twisted.internet import defer
  17. from synapse.api.errors import Codes, NotFoundError, SynapseError
  18. from synapse.http.servlet import (
  19. RestServlet,
  20. parse_json_object_from_request,
  21. parse_string,
  22. )
  23. from ._base import client_patterns
  24. logger = logging.getLogger(__name__)
  25. class RoomKeysServlet(RestServlet):
  26. PATTERNS = client_patterns(
  27. "/room_keys/keys(/(?P<room_id>[^/]+))?(/(?P<session_id>[^/]+))?$"
  28. )
  29. def __init__(self, hs):
  30. """
  31. Args:
  32. hs (synapse.server.HomeServer): server
  33. """
  34. super(RoomKeysServlet, self).__init__()
  35. self.auth = hs.get_auth()
  36. self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
  37. @defer.inlineCallbacks
  38. def on_PUT(self, request, room_id, session_id):
  39. """
  40. Uploads one or more encrypted E2E room keys for backup purposes.
  41. room_id: the ID of the room the keys are for (optional)
  42. session_id: the ID for the E2E room keys for the room (optional)
  43. version: the version of the user's backup which this data is for.
  44. the version must already have been created via the /room_keys/version API.
  45. Each session has:
  46. * first_message_index: a numeric index indicating the oldest message
  47. encrypted by this session.
  48. * forwarded_count: how many times the uploading client claims this key
  49. has been shared (forwarded)
  50. * is_verified: whether the client that uploaded the keys claims they
  51. were sent by a device which they've verified
  52. * session_data: base64-encrypted data describing the session.
  53. Returns 200 OK on success with body {}
  54. Returns 403 Forbidden if the version in question is not the most recently
  55. created version (i.e. if this is an old client trying to write to a stale backup)
  56. Returns 404 Not Found if the version in question doesn't exist
  57. The API is designed to be otherwise agnostic to the room_key encryption
  58. algorithm being used. Sessions are merged with existing ones in the
  59. backup using the heuristics:
  60. * is_verified sessions always win over unverified sessions
  61. * older first_message_index always win over newer sessions
  62. * lower forwarded_count always wins over higher forwarded_count
  63. We trust the clients not to lie and corrupt their own backups.
  64. It also means that if your access_token is stolen, the attacker could
  65. delete your backup.
  66. POST /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1
  67. Content-Type: application/json
  68. {
  69. "first_message_index": 1,
  70. "forwarded_count": 1,
  71. "is_verified": false,
  72. "session_data": "SSBBTSBBIEZJU0gK"
  73. }
  74. Or...
  75. POST /room_keys/keys/!abc:matrix.org?version=1 HTTP/1.1
  76. Content-Type: application/json
  77. {
  78. "sessions": {
  79. "c0ff33": {
  80. "first_message_index": 1,
  81. "forwarded_count": 1,
  82. "is_verified": false,
  83. "session_data": "SSBBTSBBIEZJU0gK"
  84. }
  85. }
  86. }
  87. Or...
  88. POST /room_keys/keys?version=1 HTTP/1.1
  89. Content-Type: application/json
  90. {
  91. "rooms": {
  92. "!abc:matrix.org": {
  93. "sessions": {
  94. "c0ff33": {
  95. "first_message_index": 1,
  96. "forwarded_count": 1,
  97. "is_verified": false,
  98. "session_data": "SSBBTSBBIEZJU0gK"
  99. }
  100. }
  101. }
  102. }
  103. }
  104. """
  105. requester = yield self.auth.get_user_by_req(request, allow_guest=False)
  106. user_id = requester.user.to_string()
  107. body = parse_json_object_from_request(request)
  108. version = parse_string(request, "version")
  109. if session_id:
  110. body = {"sessions": {session_id: body}}
  111. if room_id:
  112. body = {"rooms": {room_id: body}}
  113. yield self.e2e_room_keys_handler.upload_room_keys(user_id, version, body)
  114. return (200, {})
  115. @defer.inlineCallbacks
  116. def on_GET(self, request, room_id, session_id):
  117. """
  118. Retrieves one or more encrypted E2E room keys for backup purposes.
  119. Symmetric with the PUT version of the API.
  120. room_id: the ID of the room to retrieve the keys for (optional)
  121. session_id: the ID for the E2E room keys to retrieve the keys for (optional)
  122. version: the version of the user's backup which this data is for.
  123. the version must already have been created via the /change_secret API.
  124. Returns as follows:
  125. GET /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1
  126. {
  127. "first_message_index": 1,
  128. "forwarded_count": 1,
  129. "is_verified": false,
  130. "session_data": "SSBBTSBBIEZJU0gK"
  131. }
  132. Or...
  133. GET /room_keys/keys/!abc:matrix.org?version=1 HTTP/1.1
  134. {
  135. "sessions": {
  136. "c0ff33": {
  137. "first_message_index": 1,
  138. "forwarded_count": 1,
  139. "is_verified": false,
  140. "session_data": "SSBBTSBBIEZJU0gK"
  141. }
  142. }
  143. }
  144. Or...
  145. GET /room_keys/keys?version=1 HTTP/1.1
  146. {
  147. "rooms": {
  148. "!abc:matrix.org": {
  149. "sessions": {
  150. "c0ff33": {
  151. "first_message_index": 1,
  152. "forwarded_count": 1,
  153. "is_verified": false,
  154. "session_data": "SSBBTSBBIEZJU0gK"
  155. }
  156. }
  157. }
  158. }
  159. }
  160. """
  161. requester = yield self.auth.get_user_by_req(request, allow_guest=False)
  162. user_id = requester.user.to_string()
  163. version = parse_string(request, "version")
  164. room_keys = yield self.e2e_room_keys_handler.get_room_keys(
  165. user_id, version, room_id, session_id
  166. )
  167. # Convert room_keys to the right format to return.
  168. if session_id:
  169. # If the client requests a specific session, but that session was
  170. # not backed up, then return an M_NOT_FOUND.
  171. if room_keys["rooms"] == {}:
  172. raise NotFoundError("No room_keys found")
  173. else:
  174. room_keys = room_keys["rooms"][room_id]["sessions"][session_id]
  175. elif room_id:
  176. # If the client requests all sessions from a room, but no sessions
  177. # are found, then return an empty result rather than an error, so
  178. # that clients don't have to handle an error condition, and an
  179. # empty result is valid. (Similarly if the client requests all
  180. # sessions from the backup, but in that case, room_keys is already
  181. # in the right format, so we don't need to do anything about it.)
  182. if room_keys["rooms"] == {}:
  183. room_keys = {"sessions": {}}
  184. else:
  185. room_keys = room_keys["rooms"][room_id]
  186. return (200, room_keys)
  187. @defer.inlineCallbacks
  188. def on_DELETE(self, request, room_id, session_id):
  189. """
  190. Deletes one or more encrypted E2E room keys for a user for backup purposes.
  191. DELETE /room_keys/keys/!abc:matrix.org/c0ff33?version=1
  192. HTTP/1.1 200 OK
  193. {}
  194. room_id: the ID of the room whose keys to delete (optional)
  195. session_id: the ID for the E2E session to delete (optional)
  196. version: the version of the user's backup which this data is for.
  197. the version must already have been created via the /change_secret API.
  198. """
  199. requester = yield self.auth.get_user_by_req(request, allow_guest=False)
  200. user_id = requester.user.to_string()
  201. version = parse_string(request, "version")
  202. yield self.e2e_room_keys_handler.delete_room_keys(
  203. user_id, version, room_id, session_id
  204. )
  205. return (200, {})
  206. class RoomKeysNewVersionServlet(RestServlet):
  207. PATTERNS = client_patterns("/room_keys/version$")
  208. def __init__(self, hs):
  209. """
  210. Args:
  211. hs (synapse.server.HomeServer): server
  212. """
  213. super(RoomKeysNewVersionServlet, self).__init__()
  214. self.auth = hs.get_auth()
  215. self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
  216. @defer.inlineCallbacks
  217. def on_POST(self, request):
  218. """
  219. Create a new backup version for this user's room_keys with the given
  220. info. The version is allocated by the server and returned to the user
  221. in the response. This API is intended to be used whenever the user
  222. changes the encryption key for their backups, ensuring that backups
  223. encrypted with different keys don't collide.
  224. It takes out an exclusive lock on this user's room_key backups, to ensure
  225. clients only upload to the current backup.
  226. The algorithm passed in the version info is a reverse-DNS namespaced
  227. identifier to describe the format of the encrypted backupped keys.
  228. The auth_data is { user_id: "user_id", nonce: <random string> }
  229. encrypted using the algorithm and current encryption key described above.
  230. POST /room_keys/version
  231. Content-Type: application/json
  232. {
  233. "algorithm": "m.megolm_backup.v1",
  234. "auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K"
  235. }
  236. HTTP/1.1 200 OK
  237. Content-Type: application/json
  238. {
  239. "version": 12345
  240. }
  241. """
  242. requester = yield self.auth.get_user_by_req(request, allow_guest=False)
  243. user_id = requester.user.to_string()
  244. info = parse_json_object_from_request(request)
  245. new_version = yield self.e2e_room_keys_handler.create_version(user_id, info)
  246. return (200, {"version": new_version})
  247. # we deliberately don't have a PUT /version, as these things really should
  248. # be immutable to avoid people footgunning
  249. class RoomKeysVersionServlet(RestServlet):
  250. PATTERNS = client_patterns("/room_keys/version(/(?P<version>[^/]+))?$")
  251. def __init__(self, hs):
  252. """
  253. Args:
  254. hs (synapse.server.HomeServer): server
  255. """
  256. super(RoomKeysVersionServlet, self).__init__()
  257. self.auth = hs.get_auth()
  258. self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
  259. @defer.inlineCallbacks
  260. def on_GET(self, request, version):
  261. """
  262. Retrieve the version information about a given version of the user's
  263. room_keys backup. If the version part is missing, returns info about the
  264. most current backup version (if any)
  265. It takes out an exclusive lock on this user's room_key backups, to ensure
  266. clients only upload to the current backup.
  267. Returns 404 if the given version does not exist.
  268. GET /room_keys/version/12345 HTTP/1.1
  269. {
  270. "version": "12345",
  271. "algorithm": "m.megolm_backup.v1",
  272. "auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K"
  273. }
  274. """
  275. requester = yield self.auth.get_user_by_req(request, allow_guest=False)
  276. user_id = requester.user.to_string()
  277. try:
  278. info = yield self.e2e_room_keys_handler.get_version_info(user_id, version)
  279. except SynapseError as e:
  280. if e.code == 404:
  281. raise SynapseError(404, "No backup found", Codes.NOT_FOUND)
  282. return (200, info)
  283. @defer.inlineCallbacks
  284. def on_DELETE(self, request, version):
  285. """
  286. Delete the information about a given version of the user's
  287. room_keys backup. If the version part is missing, deletes the most
  288. current backup version (if any). Doesn't delete the actual room data.
  289. DELETE /room_keys/version/12345 HTTP/1.1
  290. HTTP/1.1 200 OK
  291. {}
  292. """
  293. if version is None:
  294. raise SynapseError(400, "No version specified to delete", Codes.NOT_FOUND)
  295. requester = yield self.auth.get_user_by_req(request, allow_guest=False)
  296. user_id = requester.user.to_string()
  297. yield self.e2e_room_keys_handler.delete_version(user_id, version)
  298. return (200, {})
  299. @defer.inlineCallbacks
  300. def on_PUT(self, request, version):
  301. """
  302. Update the information about a given version of the user's room_keys backup.
  303. POST /room_keys/version/12345 HTTP/1.1
  304. Content-Type: application/json
  305. {
  306. "algorithm": "m.megolm_backup.v1",
  307. "auth_data": {
  308. "public_key": "abcdefg",
  309. "signatures": {
  310. "ed25519:something": "hijklmnop"
  311. }
  312. },
  313. "version": "42"
  314. }
  315. HTTP/1.1 200 OK
  316. Content-Type: application/json
  317. {}
  318. """
  319. requester = yield self.auth.get_user_by_req(request, allow_guest=False)
  320. user_id = requester.user.to_string()
  321. info = parse_json_object_from_request(request)
  322. if version is None:
  323. raise SynapseError(
  324. 400, "No version specified to update", Codes.MISSING_PARAM
  325. )
  326. yield self.e2e_room_keys_handler.update_version(user_id, version, info)
  327. return (200, {})
  328. def register_servlets(hs, http_server):
  329. RoomKeysServlet(hs).register(http_server)
  330. RoomKeysVersionServlet(hs).register(http_server)
  331. RoomKeysNewVersionServlet(hs).register(http_server)