auth.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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, AuthorizationFailedException
  19. from mautrix.appservice import IntentAPI
  20. from mautrix.types import MediaMessageEventContent, MessageType, ImageInfo, EventID
  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. is_enabled_for=lambda evt: evt.config["signal.registration_enabled"],
  74. help_text="Sign into Signal as the primary device", help_args="<phone>")
  75. async def register(evt: CommandEvent) -> None:
  76. if len(evt.args) == 0:
  77. await evt.reply("**Usage**: $cmdprefix+sp register [--voice] [--captcha <token>] <phone>")
  78. return
  79. if await evt.sender.is_logged_in():
  80. await evt.reply("You're already logged in. "
  81. "If you want to re-register, log out with `$cmdprefix+sp logout` first.")
  82. return
  83. voice = False
  84. captcha = None
  85. while True:
  86. flag = evt.args[0].lower()
  87. if flag == "--voice" or flag == "-v":
  88. voice = True
  89. evt.args = evt.args[1:]
  90. elif flag == "--captcha" or flag == "-c":
  91. if "=" in evt.args[0]:
  92. captcha = evt.args[0].split("=", 1)[1]
  93. evt.args = evt.args[1:]
  94. else:
  95. captcha = evt.args[1]
  96. evt.args = evt.args[2:]
  97. else:
  98. break
  99. phone = evt.args[0].translate(remove_extra_chars)
  100. if not phone.startswith("+") or not phone[1:].isdecimal():
  101. await evt.reply(f"Please enter the phone number in international format (E.164)")
  102. return
  103. username = await evt.bridge.signal.register(phone, voice=voice, captcha=captcha)
  104. evt.sender.command_status = {
  105. "action": "Register",
  106. "room_id": evt.room_id,
  107. "next": enter_register_code,
  108. "username": username,
  109. }
  110. await evt.reply("Register SMS requested, please enter the code here.")
  111. async def enter_register_code(evt: CommandEvent) -> None:
  112. try:
  113. username = evt.sender.command_status["username"]
  114. account = await evt.bridge.signal.verify(username, code=evt.args[0])
  115. except UnexpectedResponse as e:
  116. if e.resp_type == "error":
  117. await evt.reply(e.data)
  118. else:
  119. raise
  120. else:
  121. await evt.sender.on_signin(account)
  122. await evt.reply(f"Successfully logged in as {pu.Puppet.fmt_phone(evt.sender.username)}."
  123. f"\n\n**N.B.** You must set a Signal profile name with `$cmdprefix+sp "
  124. f"set-profile-name <name>` before you can participate in new groups.")
  125. @command_handler(needs_auth=True, management_only=True, help_section=SECTION_AUTH,
  126. help_text="Remove all local data about your Signal link")
  127. async def logout(evt: CommandEvent) -> None:
  128. if not evt.sender.username:
  129. await evt.reply("You're not logged in")
  130. return
  131. await evt.sender.logout()
  132. await evt.reply("Successfully logged out")
  133. @command_handler(needs_auth=True, management_only=True, help_section=SECTION_AUTH,
  134. help_text="List devices linked to your Signal account")
  135. async def list_devices(evt: CommandEvent) -> None:
  136. devices = await evt.bridge.signal.get_linked_devices(evt.sender.username)
  137. await evt.reply("\n".join(f"* #{dev.id}: {dev.name_with_default} (created {dev.created_fmt}, "
  138. f"last seen {dev.last_seen_fmt})" for dev in devices))
  139. @command_handler(needs_auth=True, management_only=True, help_section=SECTION_AUTH,
  140. help_text="Remove a linked device")
  141. async def remove_linked_device(evt: CommandEvent) -> EventID:
  142. if len(evt.args) == 0:
  143. return await evt.reply("**Usage:** `$cmdprefix+sp remove-linked-device <device ID>`")
  144. device_id = int(evt.args[0])
  145. try:
  146. await evt.bridge.signal.remove_linked_device(evt.sender.username, device_id)
  147. except AuthorizationFailedException as e:
  148. return await evt.reply(f"{e} Only the primary device can remove linked devices.")
  149. return await evt.reply("Device removed")