def pan_decrypt_event(self, event_dict, room_id=None, ignore_failures=True): # type: (Dict[Any, Any], Optional[str], bool) -> (bool) event = Event.parse_encrypted_event(event_dict) if not isinstance(event, MegolmEvent): logger.warn( "Encrypted event is not a megolm event:" "\n{}".format(pformat(event_dict)) ) return False if not event.room_id: event.room_id = room_id try: decrypted_event = self.decrypt_event(event) logger.info("Decrypted event: {}".format(decrypted_event)) event_dict.update(decrypted_event.source) event_dict["decrypted"] = True event_dict["verified"] = decrypted_event.verified return True except EncryptionError as error: logger.warn(error) if ignore_failures: event_dict.update(self.unable_to_decrypt) else: raise return False
async def message_router(receive_queue, send_queue, proxies): """Find the recipient of a message and forward it to the right proxy.""" def find_proxy_by_user(user): # type: (str) -> Optional[ProxyDaemon] for proxy in proxies: if user in proxy.pan_clients: return proxy return None async def send_info(message_id, pan_user, code, string): message = DaemonResponse(message_id, pan_user, code, string) await send_queue.put(message) while True: message = await receive_queue.get() logger.debug(f"Router got message {message}") proxy = find_proxy_by_user(message.pan_user) if not proxy: msg = f"No pan client found for {message.pan_user}." logger.warn(msg) await send_info(message.message_id, message.pan_user, "m.unknown_client", msg) await proxy.receive_message(message)
def decrypt_sync_body(self, body): # type: (Dict[Any, Any]) -> Dict[Any, Any] """Go through a json sync response and decrypt megolm encrypted events. Args: body (Dict[Any, Any]): The dictionary of a Sync response. Returns the json response with decrypted events. """ for room_id, room_dict in body["rooms"]["join"].items(): try: if not self.rooms[room_id].encrypted: logger.info("Room {} is not encrypted skipping...".format( self.rooms[room_id].display_name)) continue except KeyError: logger.info("Unknown room {} skipping...".format(room_id)) continue for event in room_dict["timeline"]["events"]: if event["type"] != "m.room.encrypted": logger.info("Event is not encrypted: " "\n{}".format(pformat(event))) continue parsed_event = RoomEncryptedEvent.parse_event(event) parsed_event.room_id = room_id if not isinstance(parsed_event, MegolmEvent): logger.warn("Encrypted event is not a megolm event:" "\n{}".format(pformat(event))) continue try: decrypted_event = self.decrypt_event(parsed_event) logger.info("Decrypted event: {}".format(decrypted_event)) event["type"] = "m.room.message" # TODO support other event types # This should be best done in nio, modify events so they # keep the dictionary from which they are built in a source # attribute. event["content"] = { "msgtype": "m.text", "body": decrypted_event.body } if decrypted_event.formatted_body: event["content"]["formatted_body"] = ( decrypted_event.formatted_body) event["content"]["format"] = decrypted_event.format event["decrypted"] = True event["verified"] = decrypted_event.verified except EncryptionError as error: logger.warn(error) continue return body
async def download(self, request): server_name = request.match_info["server_name"] media_id = request.match_info["media_id"] file_name = request.match_info.get("file_name") try: media_info = self.media_info[(server_name, media_id)] except KeyError: media_info = self.store.load_media(self.name, server_name, media_id) if not media_info: logger.info( f"No media info found for {server_name}/{media_id}") return await self.forward_to_web(request) self.media_info[(server_name, media_id)] = media_info try: key = media_info.key["k"] hash = media_info.hashes["sha256"] except KeyError: logger.warn( f"Media info for {server_name}/{media_id} doesn't contain a key or hash." ) return await self.forward_to_web(request) if not self.pan_clients: return await self.forward_to_web(request) client = next(iter(self.pan_clients.values())) try: response = await client.download(server_name, media_id, file_name) except ClientConnectionError as e: return web.Response(status=500, text=str(e)) if not isinstance(response, DownloadResponse): return web.Response( status=response.transport_response.status, content_type=response.transport_response.content_type, headers=CORS_HEADERS, body=await response.transport_response.read(), ) logger.info(f"Decrypting media {server_name}/{media_id}") loop = asyncio.get_running_loop() with concurrent.futures.ProcessPoolExecutor() as pool: decrypted_file = await loop.run_in_executor( pool, decrypt_attachment, response.body, key, hash, media_info.iv) return web.Response( status=response.transport_response.status, content_type=response.transport_response.content_type, headers=CORS_HEADERS, body=decrypted_file, )
def __attrs_post_init__(self): loop = asyncio.get_event_loop() self.homeserver_url = self.homeserver.geturl() self.hostname = self.homeserver.hostname self.store = PanStore(self.data_dir) accounts = self.store.load_users(self.name) self.media_info = self.store.load_media(self.name) for user_id, device_id in accounts: if self.conf.keyring: try: token = keyring.get_password( "pantalaimon", f"{user_id}-{device_id}-token" ) except RuntimeError as e: logger.error(e) else: token = self.store.load_access_token(user_id, device_id) if not token: logger.warn( f"Not restoring client for {user_id} {device_id}, " f"missing access token." ) continue logger.info(f"Restoring client for {user_id} {device_id}") pan_client = PanClient( self.name, self.store, self.conf, self.homeserver_url, self.send_queue, user_id, device_id, store_path=self.data_dir, ssl=self.ssl, proxy=self.proxy, store_class=self.client_store_class, media_info=self.media_info, ) pan_client.user_id = user_id pan_client.access_token = token pan_client.load_store() self.pan_clients[user_id] = pan_client loop.create_task( self.send_ui_message( UpdateUsersMessage(self.name, user_id, pan_client.device_id) ) ) loop.create_task(pan_client.send_update_devices(pan_client.device_store)) pan_client.start_loop()
def pan_decrypt_event(self, event_dict, room_id=None, ignore_failures=True): # type: (Dict[Any, Any], Optional[str], bool) -> (bool) event = Event.parse_encrypted_event(event_dict) if not isinstance(event, MegolmEvent): logger.warn("Encrypted event is not a megolm event:" "\n{}".format(pformat(event_dict))) return False if not event.room_id: event.room_id = room_id try: decrypted_event = self.decrypt_event(event) logger.debug("Decrypted event: {}".format(decrypted_event)) logger.info("Decrypted event from {} in {}, event id: {}".format( decrypted_event.sender, decrypted_event.room_id, decrypted_event.event_id, )) if isinstance(decrypted_event, RoomEncryptedMedia): self.store_event_media(decrypted_event) decrypted_event.source["content"]["url"] = decrypted_event.url if decrypted_event.thumbnail_url: decrypted_event.source["content"]["info"][ "thumbnail_url"] = decrypted_event.thumbnail_url event_dict.update(decrypted_event.source) event_dict["decrypted"] = True event_dict["verified"] = decrypted_event.verified return True except EncryptionError as error: logger.warn(error) if ignore_failures: event_dict.update(self.unable_to_decrypt) else: raise return False
async def _find_client(self, access_token): client_info = self.client_info.get(access_token, None) if not client_info: async with aiohttp.ClientSession() as session: try: method, path = Api.whoami(access_token) resp = await session.request( method, self.homeserver_url + path, proxy=self.proxy, ssl=self.ssl, ) except ClientConnectionError: return None if resp.status != 200: return None try: body = await resp.json() except (JSONDecodeError, ContentTypeError): return None try: user_id = body["user_id"] except KeyError: return None if user_id not in self.pan_clients: logger.warn( f"User {user_id} doesn't have a matching pan " f"client." ) return None logger.info( f"Homeserver confirmed valid access token " f"for user {user_id}, caching info." ) client_info = ClientInfo(user_id, access_token) self.client_info[access_token] = client_info client = self.pan_clients.get(client_info.user_id, None) return client
async def _load_decrypted_file(self, server_name, media_id, file_name): try: media_info = self.media_info[(server_name, media_id)] except KeyError: media_info = self.store.load_media(self.name, server_name, media_id) if not media_info: logger.info( f"No media info found for {server_name}/{media_id}") return None, None self.media_info[(server_name, media_id)] = media_info try: key = media_info.key["k"] hash = media_info.hashes["sha256"] except KeyError as e: logger.warn( f"Media info for {server_name}/{media_id} doesn't contain a key or hash." ) raise e if not self.pan_clients: return None, None client = next(iter(self.pan_clients.values())) try: response = await client.download(server_name, media_id, file_name) except ClientConnectionError as e: raise e if not isinstance(response, DownloadResponse): return response, None logger.info(f"Decrypting media {server_name}/{media_id}") loop = asyncio.get_running_loop() with concurrent.futures.ProcessPoolExecutor() as pool: decrypted_file = await loop.run_in_executor( pool, decrypt_attachment, response.body, key, hash, media_info.iv) return response, decrypted_file
async def start_pan_client(self, access_token, user, user_id, password, device_id=None): client = ClientInfo(user_id, access_token) self.client_info[access_token] = client self.store.save_server_user(self.name, user_id) if user_id in self.pan_clients: logger.info(f"Background sync client already exists for {user_id}," f" not starting new one") return pan_client = PanClient( self.name, self.store, self.conf, self.homeserver_url, self.send_queue, user_id, store_path=self.data_dir, ssl=self.ssl, proxy=self.proxy, store_class=self.client_store_class, media_info=self.media_info, ) if password == "": if device_id is None: logger.warn( "Empty password provided and device_id was also None, not " "starting background sync client ") return # If password is blank, we cannot login normally and must # fall back to using the provided device_id. pan_client.restore_login(user_id, device_id, access_token) else: response = await pan_client.login(password, "pantalaimon") if not isinstance(response, LoginResponse): await pan_client.close() return logger.info(f"Succesfully started new background sync client for " f"{user_id}") await self.send_ui_message( UpdateUsersMessage(self.name, user_id, pan_client.device_id)) self.pan_clients[user_id] = pan_client if self.conf.keyring: try: keyring.set_password( "pantalaimon", f"{user_id}-{pan_client.device_id}-token", pan_client.access_token, ) except RuntimeError as e: logger.error(e) else: self.store.save_access_token(user_id, pan_client.device_id, pan_client.access_token) pan_client.start_loop()