From 34656c990431e5204976afa834602f242d3e33a3 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Mon, 26 Feb 2024 00:08:38 +0800 Subject: [PATCH] Get Signal uuid and password from db directly --- AppImageBuilder-arm64.yml | 2 - AppImageBuilder-x86_64.yml | 2 - Dockerfile | 8 +- compile.py | 2 +- docs/guide_signal.md | 2 + requirements.txt | 2 +- src/sticker_convert/cli.py | 19 +- .../windows/signal_get_auth_window.py | 95 ++--- .../utils/auth/get_signal_auth.py | 384 ++++-------------- src/sticker_convert/utils/callback.py | 1 - 10 files changed, 133 insertions(+), 384 deletions(-) diff --git a/AppImageBuilder-arm64.yml b/AppImageBuilder-arm64.yml index beb5d85..8a24180 100644 --- a/AppImageBuilder-arm64.yml +++ b/AppImageBuilder-arm64.yml @@ -49,8 +49,6 @@ AppDir: - python3.11 - python3.11-tk - python3.11-distutils - - binutils - - psmisc exclude: [] runtime: diff --git a/AppImageBuilder-x86_64.yml b/AppImageBuilder-x86_64.yml index 12d5601..2c29837 100644 --- a/AppImageBuilder-x86_64.yml +++ b/AppImageBuilder-x86_64.yml @@ -46,8 +46,6 @@ AppDir: - python3.11 - python3.11-tk - python3.11-distutils - - binutils - - psmisc exclude: [] after_bundle: | diff --git a/Dockerfile b/Dockerfile index 6654460..558545a 100755 --- a/Dockerfile +++ b/Dockerfile @@ -67,13 +67,13 @@ RUN apt purge -y curl wget gpg git && \ rm -rf /var/lib/{apt,dpkg,cache,log}/ FROM base-gui AS full -# Install signal-desktop-beta +# Install signal-desktop RUN wget -O- https://updates.signal.org/desktop/apt/keys.asc | gpg --dearmor > signal-desktop-keyring.gpg && \ cat signal-desktop-keyring.gpg | tee -a /usr/share/keyrings/signal-desktop-keyring.gpg > /dev/null && \ echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/signal-desktop-keyring.gpg] https://updates.signal.org/desktop/apt xenial main' |\ tee -a /etc/apt/sources.list.d/signal-xenial.list && \ apt update -y && \ - apt install --no-install-recommends -y signal-desktop-beta + apt install --no-install-recommends -y signal-desktop RUN apt purge -y curl wget gpg git && \ apt clean autoclean && \ @@ -81,6 +81,4 @@ RUN apt purge -y curl wget gpg git && \ rm -rf /var/lib/{apt,dpkg,cache,log}/ RUN mkdir -p '/root/.config/Signal' && \ - mkdir -p '/root/.config/Signal Beta' && \ - chmod 777 '/root/.config/Signal' && \ - chmod 777 '/root/.config/Signal Beta' \ No newline at end of file + chmod 777 '/root/.config/Signal' \ No newline at end of file diff --git a/compile.py b/compile.py index 88b1f14..6cb006a 100755 --- a/compile.py +++ b/compile.py @@ -7,7 +7,7 @@ from pathlib import Path sys.path.append("./src") -from sticker_convert.version import __version__ +from sticker_convert import __version__ conan_archs = { "x86_64": ["amd64", "x86_64", "x64"], diff --git a/docs/guide_signal.md b/docs/guide_signal.md index 255e193..3fccf6e 100644 --- a/docs/guide_signal.md +++ b/docs/guide_signal.md @@ -4,6 +4,8 @@ You can get them easily with `Generate` button (In GUI) or `--signal-get-auth` (In CLI) +Since v2.7.0, you can get Signal uuid and password from non-beta version of Signal. + Alternatively, follow instructions below to get them manually: ![../imgs/signal-uuid-password.png](../imgs/signal-uuid-password.png) diff --git a/requirements.txt b/requirements.txt index ebcfe6f..3a88275 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,8 +10,8 @@ pyoxipng~=9.0.0 python-telegram-bot~=20.5 requests~=2.31.0 rlottie_python~=1.2.1 -selenium~=4.17.2 signalstickers-client~=3.3.0 +sqlcipher3-wheels~=0.5.2.post0 tqdm~=4.66.2 ttkbootstrap-fork-laggykiller~=1.5.1 webp~=0.3.0 \ No newline at end of file diff --git a/src/sticker_convert/cli.py b/src/sticker_convert/cli.py index 9549803..bb60b80 100755 --- a/src/sticker_convert/cli.py +++ b/src/sticker_convert/cli.py @@ -435,22 +435,15 @@ def get_opt_cred(self, args: Namespace) -> CredOption: self.cb.msg(f"Got auth_token successfully: {auth_token}") if args.signal_get_auth: - m = GetSignalAuth(cb_msg=self.cb.msg, cb_ask_str=self.cb.ask_str) - m.launch_signal_desktop() + m = GetSignalAuth() - uuid, password = None, None - while True: - uuid, password = m.get_cred() + uuid, password, msg = m.get_cred() - if uuid and password: - opt_cred.signal_uuid = uuid - opt_cred.signal_password = password + if uuid and password: + opt_cred.signal_uuid = uuid + opt_cred.signal_password = password - self.cb.msg( - f"Got uuid and password successfully: {uuid}, {password}" - ) - m.close() - break + self.cb.msg(msg) if args.line_get_auth: m = GetLineAuth() diff --git a/src/sticker_convert/gui_components/windows/signal_get_auth_window.py b/src/sticker_convert/gui_components/windows/signal_get_auth_window.py index a564dc3..19679fd 100644 --- a/src/sticker_convert/gui_components/windows/signal_get_auth_window.py +++ b/src/sticker_convert/gui_components/windows/signal_get_auth_window.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 from functools import partial -from threading import Thread +from subprocess import Popen from typing import Any -from ttkbootstrap import Button, Frame, Label, Toplevel # type: ignore +from ttkbootstrap import Button, Frame, Label # type: ignore from sticker_convert.gui_components.gui_utils import GUIUtils from sticker_convert.gui_components.windows.base_window import BaseWindow @@ -20,75 +20,64 @@ def __init__(self, *args: Any, **kwargs: Any): self.cb_ask_str_signal = partial(self.gui.cb_ask_str, parent=self) self.frame_info = Frame(self.scrollable_frame) - self.frame_start_btn = Frame(self.scrollable_frame) + self.frame_btns = Frame(self.scrollable_frame) self.frame_info.grid(column=0, row=0, sticky="news", padx=3, pady=3) - self.frame_start_btn.grid(column=0, row=1, sticky="news", padx=3, pady=3) + self.frame_btns.grid(column=0, row=1, sticky="news", padx=3, pady=3) # Info frame - self.explanation1_lbl = Label( + self.explanation_lbl = Label( self.frame_info, - text="Please install Signal Desktop BETA VERSION", - justify="left", - anchor="w", - ) - self.explanation2_lbl = Label( - self.frame_info, - text="After installation, you need to login to Signal Desktop", - justify="left", - anchor="w", - ) - self.explanation3_lbl = Label( - self.frame_info, - text="uuid and password will be automatically fetched", + text="Please install Signal Desktop and login first.", justify="left", anchor="w", ) - self.explanation1_lbl.grid( + self.explanation_lbl.grid( column=0, row=0, columnspan=3, sticky="w", padx=3, pady=3 ) - self.explanation2_lbl.grid( - column=0, row=1, columnspan=3, sticky="w", padx=3, pady=3 - ) - self.explanation3_lbl.grid( - column=0, row=2, columnspan=3, sticky="w", padx=3, pady=3 - ) # Start button frame + self.launch_btn = Button( + self.frame_btns, text="Launch Signal Desktop", command=self.cb_launch_signal + ) + self.login_btn = Button( - self.frame_start_btn, text="Get uuid and password", command=self.cb_login + self.frame_btns, text="Get uuid and password", command=self.cb_login ) + self.launch_btn.pack() self.login_btn.pack() GUIUtils.finalize_window(self) def cb_login(self): - Thread(target=self.cb_login_thread, daemon=True).start() - - def cb_login_thread(self, *args: Any): - m = GetSignalAuth(cb_msg=self.gui.cb_msg, cb_ask_str=self.cb_ask_str_signal) - m.launch_signal_desktop() - - uuid, password = None, None - while Toplevel.winfo_exists(self): - uuid, password = m.get_cred() - - if uuid and password: - if not self.gui.creds.get("signal"): - self.gui.creds["signal"] = {} - self.gui.creds["signal"]["uuid"] = uuid - self.gui.creds["signal"]["password"] = password - self.gui.signal_uuid_var.set(uuid) - self.gui.signal_password_var.set(password) - m.close() - - self.cb_msg_block_signal( - f"Got uuid and password successfully:\nuuid={uuid}\npassword={password}" - ) - self.gui.save_creds() - self.gui.highlight_fields() - return - - self.cb_msg_block_signal("Failed to get uuid and password") + m = GetSignalAuth() + uuid, password, msg = m.get_cred() + + if uuid and password: + if not self.gui.creds.get("signal"): + self.gui.creds["signal"] = {} + self.gui.creds["signal"]["uuid"] = uuid + self.gui.creds["signal"]["password"] = password + self.gui.signal_uuid_var.set(uuid) + self.gui.signal_password_var.set(password) + + self.gui.save_creds() + self.gui.highlight_fields() + + self.cb_msg_block_signal(msg) + + def cb_launch_signal(self): + m = GetSignalAuth() + signal_bin_path, signal_user_data_dir = m.get_signal_desktop() + if signal_bin_path: + Popen( + [ + signal_bin_path, + "--no-sandbox", + f"--user-data-dir={signal_user_data_dir}", + ] + ) + else: + self.cb_msg_block_signal("Error: Signal Desktop not installed.") diff --git a/src/sticker_convert/utils/auth/get_signal_auth.py b/src/sticker_convert/utils/auth/get_signal_auth.py index cfae088..884bef8 100755 --- a/src/sticker_convert/utils/auth/get_signal_auth.py +++ b/src/sticker_convert/utils/auth/get_signal_auth.py @@ -1,242 +1,15 @@ #!/usr/bin/env python3 -import io -import json import os +import json import platform import shutil -import stat -import string -import webbrowser -import zipfile +from sqlcipher3 import dbapi2 as sqlite3 from pathlib import Path -from typing import Generator, Optional, Callable - -import requests -from selenium import webdriver -from selenium.common.exceptions import JavascriptException -from selenium.webdriver.chrome.service import Service - -from sticker_convert.definitions import CONFIG_DIR -from sticker_convert.utils.files.run_bin import RunBin - - -# https://stackoverflow.com/a/17197027 -def strings(filename: str, min: int = 4) -> Generator[str, None, None]: - with open(filename, "r", errors="ignore") as f: - result = "" - for c in f.read(): - if c in string.printable: - result += c - continue - if len(result) >= min: - yield result - result = "" - if len(result) >= min: # catch result at EOF - yield result +from typing import Optional class GetSignalAuth: - def __init__( - self, - signal_bin_version: str = "beta", - cb_msg: Callable[..., None] = print, - cb_ask_str: Callable[..., str] = input, - ): - chromedriver_download_dir = CONFIG_DIR / "bin" - os.makedirs(chromedriver_download_dir, exist_ok=True) - - self.signal_bin_version = signal_bin_version - self.chromedriver_download_dir = chromedriver_download_dir - - self.cb_ask_str = cb_ask_str - self.cb_msg = cb_msg - - def download_signal_desktop(self, download_url: str, signal_bin_path: str): - webbrowser.open(download_url) - - self.cb_msg(download_url) - - prompt = "Signal Desktop not detected.\n" - prompt += "Download and install Signal Desktop BETA version\n" - prompt += "After installation, quit Signal Desktop before continuing" - while not (Path(signal_bin_path).is_file() or shutil.which(signal_bin_path)): - if self.cb_ask_str != input: - self.cb_ask_str( - prompt, initialvalue=download_url, cli_show_initialvalue=False - ) - else: - input(prompt) - - def get_signal_chromedriver_version(self, electron_bin_path: str) -> Optional[str]: - if RunBin.get_bin("strings", silent=True): - status, output_str = RunBin.run_cmd( - cmd_list=["strings", electron_bin_path], silence=True - ) - if status is False: - return None - ss = output_str.split("\n") - else: - ss = strings(electron_bin_path) - - for s in ss: - if "Chrome/" in s and " Electron/" in s: - major_version = s.replace("Chrome/", "").split(".", 1)[0] - if major_version.isnumeric(): - return major_version - - return None - - def get_local_chromedriver( - self, chromedriver_download_dir: Path - ) -> tuple[Optional[Path], Optional[str]]: - local_chromedriver_version = None - if platform.system() == "Windows": - chromedriver_name = "chromedriver.exe" - else: - chromedriver_name = "chromedriver" - chromedriver_path = Path(chromedriver_download_dir, chromedriver_name).resolve() - if not chromedriver_path.is_file(): - chromedriver_which = shutil.which("chromedriver") - if chromedriver_which: - chromedriver_path = Path(chromedriver_which) - else: - chromedriver_path = None - - if chromedriver_path: - status, output_str = RunBin.run_cmd( - cmd_list=[str(chromedriver_path), "-v"], silence=True - ) - if status is False: - local_chromedriver_version = None - local_chromedriver_version = output_str.split(" ")[1].split(".", 1)[0] - else: - local_chromedriver_version = None - - return chromedriver_path, local_chromedriver_version - - def download_chromedriver( - self, major_version: str, chromedriver_download_dir: Path - ) -> Optional[Path]: - if platform.system() == "Windows": - chromedriver_platform = "win32" - if "64" in platform.architecture()[0]: - chromedriver_platform_new = "win64" - else: - chromedriver_platform_new = "win32" - elif platform.system() == "Darwin": - if platform.processor().lower() == "arm64": - chromedriver_platform = "mac_arm64" - chromedriver_platform_new = "mac-arm64" - else: - chromedriver_platform = "mac64" - chromedriver_platform_new = "mac-x64" - else: - chromedriver_platform = "linux64" - chromedriver_platform_new = "linux64" - - chromedriver_url = None - chromedriver_version_url = f"https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{major_version}" - r = requests.get(chromedriver_version_url) - if r.ok: - new_chrome = False - chromedriver_version = r.text - chromedriver_url = f"https://chromedriver.storage.googleapis.com/{chromedriver_version}/chromedriver_{chromedriver_platform}.zip" - else: - new_chrome = True - r = requests.get( - "https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone-with-downloads.json" - ) - versions_dict = json.loads(r.text) - chromedriver_list = ( - versions_dict.get("milestones", {}) - .get(major_version, {}) - .get("downloads", {}) - .get("chromedriver", {}) - ) - - chromedriver_url = None - for i in chromedriver_list: - if i.get("platform") == chromedriver_platform_new: - chromedriver_url = i.get("url") - - if not chromedriver_url: - return None - - if platform.system() == "Windows": - chromedriver_name = "chromedriver.exe" - else: - chromedriver_name = "chromedriver" - - if new_chrome: - chromedriver_zip_path = ( - f"chromedriver-{chromedriver_platform_new}/{chromedriver_name}" - ) - else: - chromedriver_zip_path = chromedriver_name - - chromedriver_path = Path(chromedriver_download_dir, chromedriver_name).resolve() - - with io.BytesIO() as f: - f.write(requests.get(chromedriver_url).content) # type: ignore - with zipfile.ZipFile(f, "r") as z, open(chromedriver_path, "wb+") as g: - g.write(z.read(chromedriver_zip_path)) - - if platform.system() != "Windows": - st = os.stat(chromedriver_path) - os.chmod(chromedriver_path, st.st_mode | stat.S_IEXEC) - - return chromedriver_path - - def killall_signal(self): - if platform.system() == "Windows": - os.system('taskkill /F /im "Signal.exe"') - os.system('taskkill /F /im "Signal Beta.exe"') - else: - RunBin.run_cmd(cmd_list=["killall", "signal-desktop"], silence=True) - RunBin.run_cmd(cmd_list=["killall", "signal-desktop-beta"], silence=True) - - def launch_signal( - self, signal_bin_path: str, signal_user_data_dir: str, chromedriver_path: str - ): - options = webdriver.ChromeOptions() - options.binary_location = signal_bin_path - options.add_argument(f"user-data-dir={signal_user_data_dir}") # type: ignore - options.add_argument("no-sandbox") # type: ignore - service = Service(executable_path=chromedriver_path) - - self.driver = webdriver.Chrome(options=options, service=service) - - def get_cred(self) -> tuple[Optional[str], Optional[str]]: - # https://stackoverflow.com/a/73456344 - uuid: Optional[str] = None - password: Optional[str] = None - try: - if self.signal_bin_version == "prod": - uuid = self.driver.execute_script( # type: ignore - "return window.reduxStore.getState().items.uuid_id" - ) - password = self.driver.execute_script( # type: ignore - "return window.reduxStore.getState().items.password" - ) - else: - uuid = self.driver.execute_script( # type: ignore - "return window.SignalDebug.getReduxState().items.uuid_id" - ) - password = self.driver.execute_script( # type: ignore - "return window.SignalDebug.getReduxState().items.password" - ) - except JavascriptException: - pass - - assert uuid is None or isinstance(uuid, str) - assert password is None or isinstance(password, str) - return uuid, password - - def close(self): - self.cb_msg("Closing Signal Desktop") - self.driver.quit() - - def launch_signal_desktop(self) -> bool: + def get_signal_desktop(self) -> tuple[Optional[str], Optional[str]]: if platform.system() == "Windows": signal_bin_path_prod = os.path.expandvars( "%localappdata%/Programs/signal-desktop/Signal.exe" @@ -250,8 +23,6 @@ def launch_signal_desktop(self) -> bool: signal_user_data_dir_beta = os.path.abspath( os.path.expandvars("%appdata%/Signal Beta") ) - electron_bin_path_prod = signal_bin_path_prod - electron_bin_path_beta = signal_bin_path_beta elif platform.system() == "Darwin": signal_bin_path_prod = "/Applications/Signal.app/Contents/MacOS/Signal" signal_bin_path_beta = ( @@ -263,81 +34,82 @@ def launch_signal_desktop(self) -> bool: signal_user_data_dir_beta = os.path.expanduser( "~/Library/Application Support/Signal Beta" ) - electron_bin_path_prod = "/Applications/Signal.app/Contents/Frameworks/Electron Framework.framework/Electron Framework" - electron_bin_path_beta = "/Applications/Signal Beta.app/Contents/Frameworks/Electron Framework.framework/Electron Framework" else: - signal_bin_path_prod = "signal-desktop" - signal_bin_path_beta = "signal-desktop-beta" + if not (signal_bin_path_prod := shutil.which("signal-desktop")): # type: ignore + signal_bin_path_prod = "signal-desktop" + if not (signal_bin_path_beta := shutil.which("signal-desktop-beta")): # type: ignore + signal_bin_path_beta = "signal-desktop-beta" signal_user_data_dir_prod = os.path.expanduser("~/.config/Signal") signal_user_data_dir_beta = os.path.expanduser("~/.config/Signal Beta") - electron_bin_path_prod = signal_bin_path_prod - electron_bin_path_beta = signal_bin_path_beta - - if self.signal_bin_version == "prod": - signal_bin_path = signal_bin_path_prod - signal_user_data_dir = signal_user_data_dir_prod - electron_bin_path = electron_bin_path_prod - signal_download_url = "https://signal.org/en/download/" - else: - signal_bin_path = signal_bin_path_beta - signal_user_data_dir = signal_user_data_dir_beta - electron_bin_path = electron_bin_path_beta - signal_download_url = ( - "https://support.signal.org/hc/en-us/articles/360007318471-Signal-Beta" - ) - - if not (Path(signal_bin_path).is_file() or shutil.which(signal_bin_path)): - success = self.download_signal_desktop(signal_download_url, signal_bin_path) - - if not success: - return False - - electron_bin_path = ( - shutil.which(electron_bin_path) - if not Path(electron_bin_path).is_file() - else electron_bin_path - ) - if not electron_bin_path: - self.cb_msg("Cannot find Electron Framework inside Signal installation") - return False - signal_bin_path = ( - signal_bin_path - if not shutil.which(signal_bin_path) - else shutil.which(signal_bin_path) - ) - if not signal_bin_path: - self.cb_msg("Cannot find Signal installation") - return False - - major_version = self.get_signal_chromedriver_version(electron_bin_path) - if major_version: - self.cb_msg(f"Signal Desktop is using chrome version {major_version}") - else: - self.cb_msg("Unable to determine Signal Desktop chrome version") - return False - - chromedriver_path, local_chromedriver_version = self.get_local_chromedriver( - chromedriver_download_dir=self.chromedriver_download_dir - ) - if chromedriver_path and local_chromedriver_version == major_version: - self.cb_msg( - f"Found chromedriver version {local_chromedriver_version}, skip downloading" - ) + if Path(signal_bin_path_prod).is_file(): + return signal_bin_path_prod, signal_user_data_dir_prod + elif Path(signal_bin_path_beta).is_file(): + return signal_bin_path_beta, signal_user_data_dir_beta else: - chromedriver_path = self.download_chromedriver( - major_version, chromedriver_download_dir=self.chromedriver_download_dir - ) - if not chromedriver_path: - self.cb_msg("Unable to download suitable chromedriver") - return False - - self.cb_msg("Killing all Signal Desktop processes") - self.killall_signal() - - self.cb_msg("Starting Signal Desktop with Selenium") - self.launch_signal( - signal_bin_path, signal_user_data_dir, str(chromedriver_path) - ) - - return True + return None, None + + def get_cred(self) -> tuple[Optional[str], Optional[str], str]: + signal_bin_path, signal_user_data_dir = self.get_signal_desktop() + + if not (signal_bin_path and signal_user_data_dir): + msg = "Signal Desktop not detected.\n" + msg += "Download and install Signal Desktop,\n" + msg += "then login to Signal Desktop and try again." + + return None, None, msg + + signal_config = Path(signal_user_data_dir, "config.json") + + if not signal_config.is_file(): + msg = "Signal Desktop installed,\n" + msg += "but it's config file not found.\n" + msg += "Please login to Signal Desktop and try again.\n" + msg += "\n" + msg += f"{signal_bin_path=}\n" + msg += f"{signal_user_data_dir=}\n" + return None, None, msg + + with open(signal_config) as f: + config = json.load(f) + key = config.get("key") + db_key = f"x'{key}'" + + signal_database = Path(signal_user_data_dir, "sql/db.sqlite") + + if not signal_database.is_file(): + msg = "Signal Desktop installed,\n" + msg += "but database file not found.\n" + msg += "Please login to Signal Desktop and try again.\n" + msg += "\n" + msg += f"{signal_bin_path=}\n" + msg += f"{signal_user_data_dir=}\n" + return None, None, msg + + db_conn = sqlite3.connect(signal_database.as_posix()) # type: ignore + db_cursor = db_conn.cursor() + db_cursor.execute(f'PRAGMA key="{db_key}"') + db_cursor.execute("SELECT * FROM items") + result = db_cursor.fetchall() + db_conn.close() + + uuid_id = None + password = None + for r in result: + if "uuid_id" in r: + uuid_id = json.loads(r[1])["value"] + if "password" in r: + password = json.loads(r[1])["value"] + if uuid_id and password: + msg = "Got uuid and password successfully:\n" + msg += f"{uuid_id=}\n" + msg += f"{password=}" + return uuid_id, password, msg + + msg = "Signal Desktop installed and Database found,\n" + msg += "but uuid and password not found.\n" + msg += "Please login to Signal Desktop and try again.\n" + msg += "\n" + msg += f"{signal_bin_path=}\n" + msg += f"{signal_user_data_dir=}\n" + return None, None, msg diff --git a/src/sticker_convert/utils/callback.py b/src/sticker_convert/utils/callback.py index 8879b6d..61f2926 100644 --- a/src/sticker_convert/utils/callback.py +++ b/src/sticker_convert/utils/callback.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -from queue import Queue from multiprocessing import Queue, Event from typing import Callable, Optional, Union, Any