store_invite_servlet.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015 OpenMarket Ltd
  3. # Copyright 2019 New Vector Ltd
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. from __future__ import absolute_import
  17. import nacl.signing
  18. import random
  19. import string
  20. from email.header import Header
  21. from six import string_types
  22. from twisted.web.resource import Resource
  23. from unpaddedbase64 import encode_base64
  24. from sydent.db.invite_tokens import JoinTokenStore
  25. from sydent.db.threepid_associations import GlobalAssociationStore
  26. from sydent.http.servlets import get_args, send_cors, jsonwrap, MatrixRestError
  27. from sydent.http.auth import authV2
  28. from sydent.util.emailutils import sendEmail
  29. from sydent.util.stringutils import MAX_EMAIL_ADDRESS_LENGTH
  30. class StoreInviteServlet(Resource):
  31. def __init__(self, syd, require_auth=False):
  32. self.sydent = syd
  33. self.random = random.SystemRandom()
  34. self.require_auth = require_auth
  35. @jsonwrap
  36. def render_POST(self, request):
  37. send_cors(request)
  38. args = get_args(request, ("medium", "address", "room_id", "sender",))
  39. medium = args["medium"]
  40. address = args["address"]
  41. roomId = args["room_id"]
  42. sender = args["sender"]
  43. verified_sender = None
  44. if self.require_auth:
  45. account = authV2(self.sydent, request)
  46. verified_sender = sender
  47. if account.userId != sender:
  48. raise MatrixRestError(403, "M_UNAUTHORIZED", "'sender' doesn't match")
  49. globalAssocStore = GlobalAssociationStore(self.sydent)
  50. mxid = globalAssocStore.getMxid(medium, address)
  51. if mxid:
  52. request.setResponseCode(400)
  53. return {
  54. "errcode": "M_THREEPID_IN_USE",
  55. "error": "Binding already known",
  56. "mxid": mxid,
  57. }
  58. if medium != "email":
  59. request.setResponseCode(400)
  60. return {
  61. "errcode": "M_UNRECOGNIZED",
  62. "error": "Didn't understand medium '%s'" % (medium,),
  63. }
  64. if not (0 < len(address) <= MAX_EMAIL_ADDRESS_LENGTH):
  65. request.setResponseCode(400)
  66. return {
  67. 'errcode': 'M_INVALID_PARAM',
  68. 'error': 'Invalid email provided'
  69. }
  70. token = self._randomString(128)
  71. tokenStore = JoinTokenStore(self.sydent)
  72. ephemeralPrivateKey = nacl.signing.SigningKey.generate()
  73. ephemeralPublicKey = ephemeralPrivateKey.verify_key
  74. ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True)
  75. ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True)
  76. tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64)
  77. tokenStore.storeToken(medium, address, roomId, sender, token)
  78. # Variables to substitute in the template.
  79. substitutions = {}
  80. # Include all arguments sent via the request.
  81. for k, v in args.items():
  82. if isinstance(v, string_types):
  83. substitutions[k] = v
  84. substitutions["token"] = token
  85. # Substitutions that the template requires, but are optional to provide
  86. # to the API.
  87. extra_substitutions = [
  88. 'sender_display_name',
  89. 'token',
  90. 'room_name',
  91. 'bracketed_room_name',
  92. 'room_avatar_url',
  93. 'sender_avatar_url',
  94. 'guest_user_id',
  95. 'guest_access_token',
  96. ]
  97. for k in extra_substitutions:
  98. substitutions.setdefault(k, '')
  99. substitutions["bracketed_verified_sender"] = ""
  100. if verified_sender:
  101. substitutions["bracketed_verified_sender"] = "(%s) " % (verified_sender,)
  102. substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64
  103. if substitutions["room_name"] != '':
  104. substitutions["bracketed_room_name"] = "(%s) " % substitutions["room_name"]
  105. substitutions["web_client_location"] = self.sydent.default_web_client_location
  106. if 'org.matrix.web_client_location' in substitutions:
  107. substitutions["web_client_location"] = substitutions.pop("org.matrix.web_client_location")
  108. subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8')
  109. substitutions["subject_header_value"] = subject_header.encode()
  110. brand = self.sydent.brand_from_request(request)
  111. templateFile = self.sydent.get_branded_template(
  112. brand,
  113. "invite_template.eml",
  114. ('email', 'email.invite_template'),
  115. )
  116. sendEmail(self.sydent, templateFile, address, substitutions)
  117. pubKey = self.sydent.keyring.ed25519.verify_key
  118. pubKeyBase64 = encode_base64(pubKey.encode())
  119. baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),)
  120. keysToReturn = []
  121. keysToReturn.append({
  122. "public_key": pubKeyBase64,
  123. "key_validity_url": baseUrl + "/pubkey/isvalid",
  124. })
  125. keysToReturn.append({
  126. "public_key": ephemeralPublicKeyBase64,
  127. "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid",
  128. })
  129. resp = {
  130. "token": token,
  131. "public_key": pubKeyBase64,
  132. "public_keys": keysToReturn,
  133. "display_name": self.redact_email_address(address),
  134. }
  135. return resp
  136. def redact_email_address(self, address):
  137. """
  138. Redacts the content of a 3PID address. Redacts both the email's username and
  139. domain independently.
  140. :param address: The address to redact.
  141. :type address: unicode
  142. :return: The redacted address.
  143. :rtype: unicode
  144. """
  145. # Extract strings from the address
  146. username, domain = address.split(u"@", 1)
  147. # Obfuscate the username portion
  148. separator = self.sydent.third_party_invite_username_separator_string
  149. if separator:
  150. # Redact each component individually, if it has content.
  151. # (No content implies multiple sequential separators.)
  152. redacted_components = [
  153. self._redact(component, self.sydent.username_reveal_characters)
  154. if component else ""
  155. for component in username.split(separator)
  156. ]
  157. redacted_username = separator.join(redacted_components)
  158. else:
  159. redacted_username = self._redact(
  160. username, self.sydent.username_reveal_characters
  161. )
  162. # Obfuscate the domain portion
  163. redacted_domain = self._redact(domain, self.sydent.domain_reveal_characters)
  164. return redacted_username + u"@" + redacted_domain
  165. def _redact(self, s, characters_to_reveal):
  166. """
  167. Redacts the content of a string, using a given amount of characters to reveal.
  168. If the always_obfuscate config option is true, and the string is shorter than the
  169. given threshold, redact it based on length.
  170. :param s: The string to redact.
  171. :type s: unicode
  172. :param characters_to_reveal: How many characters of the string to leave before
  173. the ellipsis
  174. :type characters_to_reveal: int
  175. :return: The redacted string.
  176. :rtype: unicode
  177. """
  178. if len(s) <= characters_to_reveal:
  179. # The string is shorter than the configured amount of characters to show.
  180. if self.sydent.always_obfuscate:
  181. # If the string is shorter than the defined threshold, then we'll
  182. # redact based on size instead. This ensures that at least *some*
  183. # part of the string is obfuscated, regardless of its total length.
  184. if len(s) > 5:
  185. return s[:3] + u"…"
  186. if len(s) > 1:
  187. return s[0] + u"…"
  188. return u"…"
  189. # Otherwise just return the original string.
  190. return s
  191. # Truncate to the configured length and add an ellipses.
  192. return s[:characters_to_reveal] + u"…"
  193. def _randomString(self, length):
  194. """
  195. Generate a random string of the given length.
  196. :param length: The length of the string to generate.
  197. :type length: int
  198. :return: The generated string.
  199. :rtype: unicode
  200. """
  201. return u''.join(self.random.choice(string.ascii_letters) for _ in range(length))