auth.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. # mautrix-signal - A Matrix-Signal puppeting bridge
  2. # Copyright (C) 2020 Tulir Asokan
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. from typing import Union
  17. import io
  18. from mausignald.errors import UnexpectedResponse, TimeoutException
  19. from mautrix.appservice import IntentAPI
  20. from mautrix.types import MediaMessageEventContent, MessageType, ImageInfo
  21. from mautrix.bridge.commands import HelpSection, command_handler
  22. from .. import puppet as pu
  23. from .typehint import CommandEvent
  24. try:
  25. import qrcode
  26. import PIL as _
  27. except ImportError:
  28. qrcode = None
  29. SECTION_AUTH = HelpSection("Authentication", 10, "")
  30. remove_extra_chars = str.maketrans("", "", " .,-()")
  31. async def make_qr(intent: IntentAPI, data: Union[str, bytes], body: str = None
  32. ) -> MediaMessageEventContent:
  33. # TODO always encrypt QR codes?
  34. buffer = io.BytesIO()
  35. image = qrcode.make(data)
  36. size = image.pixel_size
  37. image.save(buffer, "PNG")
  38. qr = buffer.getvalue()
  39. mxc = await intent.upload_media(qr, "image/png", "qr.png", len(qr))
  40. return MediaMessageEventContent(body=body or data, url=mxc, msgtype=MessageType.IMAGE,
  41. info=ImageInfo(mimetype="image/png", size=len(qr),
  42. width=size, height=size))
  43. @command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
  44. help_text="Link the bridge as a secondary device", help_args="[device name]")
  45. async def link(evt: CommandEvent) -> None:
  46. if qrcode is None:
  47. await evt.reply("Can't generate QR code: qrcode and/or PIL not installed")
  48. return
  49. if await evt.sender.is_logged_in():
  50. await evt.reply("You're already logged in. "
  51. "If you want to relink, log out with `$cmdprefix+sp logout` first.")
  52. return
  53. # TODO make default device name configurable
  54. device_name = " ".join(evt.args) or "Mautrix-Signal bridge"
  55. sess = await evt.bridge.signal.start_link()
  56. content = await make_qr(evt.az.intent, sess.uri)
  57. event_id = await evt.az.intent.send_message(evt.room_id, content)
  58. try:
  59. account = await evt.bridge.signal.finish_link(session_id=sess.session_id, overwrite=True,
  60. device_name=device_name)
  61. except TimeoutException:
  62. await evt.reply("Linking timed out, please try again.")
  63. except Exception:
  64. evt.log.exception("Fatal error while waiting for linking to finish")
  65. await evt.reply("Fatal error while waiting for linking to finish "
  66. "(see logs for more details)")
  67. else:
  68. await evt.sender.on_signin(account)
  69. await evt.reply(f"Successfully logged in as {pu.Puppet.fmt_phone(evt.sender.username)}")
  70. finally:
  71. await evt.main_intent.redact(evt.room_id, event_id)
  72. @command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
  73. help_text="Sign into Signal as the primary device", help_args="<phone>")
  74. async def register(evt: CommandEvent) -> None:
  75. if len(evt.args) == 0:
  76. await evt.reply("**Usage**: $cmdprefix+sp register [--voice] [--captcha <token>] <phone>")
  77. return
  78. if await evt.sender.is_logged_in():
  79. await evt.reply("You're already logged in. "
  80. "If you want to re-register, log out with `$cmdprefix+sp logout` first.")
  81. return
  82. voice = False
  83. captcha = None
  84. while True:
  85. flag = evt.args[0].lower()
  86. if flag == "--voice" or flag == "-v":
  87. voice = True
  88. evt.args = evt.args[1:]
  89. elif flag == "--captcha" or flag == "-c":
  90. if "=" in evt.args[0]:
  91. captcha = evt.args[0].split("=", 1)[1]
  92. evt.args = evt.args[1:]
  93. else:
  94. captcha = evt.args[1]
  95. evt.args = evt.args[2:]
  96. else:
  97. break
  98. phone = evt.args[0].translate(remove_extra_chars)
  99. if not phone.startswith("+") or not phone[1:].isdecimal():
  100. await evt.reply(f"Please enter the phone number in international format (E.164)")
  101. return
  102. username = await evt.bridge.signal.register(phone, voice=voice, captcha=captcha)
  103. evt.sender.command_status = {
  104. "action": "Register",
  105. "room_id": evt.room_id,
  106. "next": enter_register_code,
  107. "username": username,
  108. }
  109. await evt.reply("Register SMS requested, please enter the code here.")
  110. async def enter_register_code(evt: CommandEvent) -> None:
  111. try:
  112. username = evt.sender.command_status["username"]
  113. account = await evt.bridge.signal.verify(username, code=evt.args[0])
  114. except UnexpectedResponse as e:
  115. if e.resp_type == "error":
  116. await evt.reply(e.data)
  117. else:
  118. raise
  119. else:
  120. await evt.sender.on_signin(account)
  121. await evt.reply(f"Successfully logged in as {pu.Puppet.fmt_phone(evt.sender.username)}."
  122. f"\n\n**N.B.** You must set a Signal profile name with `$cmdprefix+sp "
  123. f"set-profile-name <name>` before you can participate in new groups.")
  124. @command_handler(needs_auth=True, management_only=True, help_section=SECTION_AUTH,
  125. help_text="Remove all local data about your Signal link")
  126. async def logout(evt: CommandEvent) -> None:
  127. if not evt.sender.username:
  128. await evt.reply("You're not logged in")
  129. return
  130. await evt.sender.logout()
  131. await evt.reply("Successfully logged out")