store_invite_servlet.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015 OpenMarket 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. from __future__ import absolute_import
  16. import nacl.signing
  17. import random
  18. import string
  19. from email.header import Header
  20. from six import string_types
  21. from twisted.web.resource import Resource
  22. from unpaddedbase64 import encode_base64
  23. from sydent.db.invite_tokens import JoinTokenStore
  24. from sydent.db.threepid_associations import GlobalAssociationStore
  25. from sydent.http.servlets import get_args, send_cors, jsonwrap
  26. from sydent.http.auth import authIfV2
  27. from sydent.util.emailutils import sendEmail
  28. class StoreInviteServlet(Resource):
  29. def __init__(self, syd):
  30. self.sydent = syd
  31. self.random = random.SystemRandom()
  32. @jsonwrap
  33. def render_POST(self, request):
  34. send_cors(request)
  35. authIfV2(self.sydent, request)
  36. args = get_args(request, ("medium", "address", "room_id", "sender",))
  37. medium = args["medium"]
  38. address = args["address"]
  39. roomId = args["room_id"]
  40. sender = args["sender"]
  41. globalAssocStore = GlobalAssociationStore(self.sydent)
  42. mxid = globalAssocStore.getMxid(medium, address)
  43. if mxid:
  44. request.setResponseCode(400)
  45. return {
  46. "errcode": "M_THREEPID_IN_USE",
  47. "error": "Binding already known",
  48. "mxid": mxid,
  49. }
  50. if medium != "email":
  51. request.setResponseCode(400)
  52. return {
  53. "errcode": "M_UNRECOGNIZED",
  54. "error": "Didn't understand medium '%s'" % (medium,),
  55. }
  56. token = self._randomString(128)
  57. tokenStore = JoinTokenStore(self.sydent)
  58. ephemeralPrivateKey = nacl.signing.SigningKey.generate()
  59. ephemeralPublicKey = ephemeralPrivateKey.verify_key
  60. ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True)
  61. ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True)
  62. tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64)
  63. tokenStore.storeToken(medium, address, roomId, sender, token)
  64. # Variables to substitute in the template.
  65. substitutions = {}
  66. # Include all arguments sent via the request.
  67. for k, v in args.items():
  68. if isinstance(v, string_types):
  69. substitutions[k] = v
  70. substitutions["token"] = token
  71. # Substitutions that the template requires, but are optional to provide
  72. # to the API.
  73. extra_substitutions = [
  74. 'sender_display_name',
  75. 'token',
  76. 'room_name',
  77. 'bracketed_room_name',
  78. 'room_avatar_url',
  79. 'sender_avatar_url',
  80. 'guest_user_id',
  81. 'guest_access_token',
  82. ]
  83. for k in extra_substitutions:
  84. substitutions.setdefault(k, '')
  85. substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64
  86. if substitutions["room_name"] != '':
  87. substitutions["bracketed_room_name"] = "(%s)" % substitutions["room_name"]
  88. substitutions["web_client_location"] = self.sydent.default_web_client_location
  89. if 'org.matrix.web_client_location' in substitutions:
  90. substitutions["web_client_location"] = substitutions.pop("org.matrix.web_client_location")
  91. subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8')
  92. substitutions["subject_header_value"] = subject_header.encode()
  93. sendEmail(self.sydent, "email.invite_template", address, substitutions)
  94. pubKey = self.sydent.keyring.ed25519.verify_key
  95. pubKeyBase64 = encode_base64(pubKey.encode())
  96. baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),)
  97. keysToReturn = []
  98. keysToReturn.append({
  99. "public_key": pubKeyBase64,
  100. "key_validity_url": baseUrl + "/pubkey/isvalid",
  101. })
  102. keysToReturn.append({
  103. "public_key": ephemeralPublicKeyBase64,
  104. "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid",
  105. })
  106. resp = {
  107. "token": token,
  108. "public_key": pubKeyBase64,
  109. "public_keys": keysToReturn,
  110. "display_name": self.redact_email_address(address),
  111. }
  112. return resp
  113. def redact_email_address(self, address):
  114. """
  115. Redacts the content of a 3PID address. Redacts both the email's username and
  116. domain independently.
  117. :param address: The address to redact.
  118. :type address: unicode
  119. :return: The redacted address.
  120. :rtype: unicode
  121. """
  122. # Extract strings from the address
  123. username, domain = address.split(u"@", 1)
  124. # Obfuscate strings
  125. redacted_username = self._redact(username, self.sydent.username_obfuscate_characters)
  126. redacted_domain = self._redact(domain, self.sydent.domain_obfuscate_characters)
  127. return redacted_username + u"@" + redacted_domain
  128. def _redact(self, s, characters_to_reveal):
  129. """
  130. Redacts the content of a string, using a given amount of characters to reveal.
  131. If the string is shorter than the given threshold, redact it based on length.
  132. :param s: The string to redact.
  133. :type s: unicode
  134. :param characters_to_reveal: How many characters of the string to leave before
  135. the '...'
  136. :type characters_to_reveal: int
  137. :return: The redacted string.
  138. :rtype: unicode
  139. """
  140. # If the string is shorter than the defined threshold, redact based on length
  141. if len(s) <= characters_to_reveal:
  142. if len(s) > 5:
  143. return s[:3] + u"..."
  144. if len(s) > 1:
  145. return s[0] + u"..."
  146. return u"..."
  147. # Otherwise truncate it and add an ellipses
  148. return s[:characters_to_reveal] + u"..."
  149. def _randomString(self, length):
  150. """
  151. Generate a random string of the given length.
  152. :param length: The length of the string to generate.
  153. :type length: int
  154. :return: The generated string.
  155. :rtype: unicode
  156. """
  157. return u''.join(self.random.choice(string.ascii_letters) for _ in range(length))