diff --git a/pyproject.toml b/pyproject.toml index 2cd8f76..fea4bca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,5 +106,4 @@ reportMissingImports = true reportMissingParameterType = true reportUnnecessaryTypeIgnoreComment = true reportDeprecated = true -pythonVersion = "3.12" -pythonPlatform = "Linux" +pythonVersion = "3.9" diff --git a/sigexport/crypto.py b/sigexport/crypto.py index e5f3fd4..1f06a8e 100644 --- a/sigexport/crypto.py +++ b/sigexport/crypto.py @@ -5,37 +5,43 @@ import subprocess import sys from pathlib import Path +from typing import Optional from Crypto.Cipher import AES from Crypto.Hash import SHA1 from Crypto.Protocol.KDF import PBKDF2 from Crypto.Util.Padding import unpad -from typer import Exit, colors, secho +from typer import colors, secho -def get_key(file: Path) -> str: +def get_key(file: Path, password: Optional[str]) -> str: with open(file, encoding="utf-8") as f: data = json.loads(f.read()) if "key" in data: return data["key"] elif "encryptedKey" in data: - if sys.platform == "darwin": - try: - pw = get_password() - return decrypt(pw, data["encryptedKey"]) - except Exception: - secho("Failed to decrypt Signal password", fg=colors.RED) - raise Exit(1) - else: + encrypyed_key = data["encryptedKey"] + if sys.platform == "win32": secho( - "Your Signal data key is encrypted, and descrypting" - "it is currently only supported on macOS", + "Signal decryption isn't currently supported on Windows" + "If you know some Python and crypto, please contribute a PR!", fg=colors.RED, ) - raise Exit(1) - - secho("No Signal decryption key found", fg=colors.RED) - raise Exit(1) + if sys.platform == "darwin": + pw = get_password() + return decrypt(pw, encrypyed_key, b"v10", 1003) + else: + if password: + return decrypt(password, encrypyed_key, b"v11", 1) + else: + secho("Your Signal data key is encrypted, and requires a password.") + secho("On Gnome, you can try to get this with this command:") + secho("secret-tool lookup application Signal\n", fg=colors.BLUE) + secho("Then please rerun sigexport as follows:") + secho("sigexport --password=PASSWORD_FROM_COMMAND ...", fg=colors.BLUE) + else: + secho("No Signal decryption key found", fg=colors.RED) + raise def get_password() -> str: @@ -45,15 +51,16 @@ def get_password() -> str: return pw.strip() -def decrypt(password: str, encrypted_key: str) -> str: +def decrypt(password: str, encrypted_key: str, prefix: bytes, iterations: int) -> str: encrypted_key_b = bytes.fromhex(encrypted_key) - prefix = b"v10" if not encrypted_key_b.startswith(prefix): raise encrypted_key_b = encrypted_key_b[len(prefix) :] salt = b"saltysalt" - key = PBKDF2(password, salt=salt, dkLen=128 // 8, count=1003, hmac_hash_module=SHA1) + key = PBKDF2( + password, salt=salt, dkLen=128 // 8, count=iterations, hmac_hash_module=SHA1 + ) iv = b" " * 16 aes_decrypted = AES.new(key, AES.MODE_CBC, iv).decrypt(encrypted_key_b) decrypted_key = unpad(aes_decrypted, block_size=16).decode("ascii") diff --git a/sigexport/data.py b/sigexport/data.py index 54184e3..0d0b64e 100644 --- a/sigexport/data.py +++ b/sigexport/data.py @@ -2,8 +2,10 @@ import json from pathlib import Path +from typing import Optional from pysqlcipher3 import dbapi2 as sqlcipher +from typer import Exit, colors, secho from sigexport import crypto, models from sigexport.logging import log @@ -11,6 +13,7 @@ def fetch_data( source_dir: Path, + password: Optional[str], chats: str, include_empty: bool, ) -> tuple[models.Convos, models.Contacts]: @@ -18,9 +21,13 @@ def fetch_data( db_file = source_dir / "sql" / "db.sqlite" signal_config = source_dir / "config.json" - log(f"Fetching data from {db_file}\n") - key = crypto.get_key(signal_config) + try: + key = crypto.get_key(signal_config, password) + except Exception: + secho("Failed to decrypt Signal password", fg=colors.RED) + raise Exit(1) + log(f"Fetching data from {db_file}\n") contacts: models.Contacts = {} convos: models.Convos = {} chats_list = chats.split(",") if len(chats) > 0 else [] diff --git a/sigexport/main.py b/sigexport/main.py index 45efd50..5af2154 100755 --- a/sigexport/main.py +++ b/sigexport/main.py @@ -25,6 +25,7 @@ def main( dest: Path = Argument(None), source: OptionalPath = Option(None, help="Path to Signal source directory"), old: OptionalPath = Option(None, help="Path to previous export to merge"), + password: OptionalStr = Option(None, help="Linux-only. Password to decrypt DB key"), paginate: int = Option( 100, "--paginate", "-p", help="Messages per page in HTML; set to 0 for infinite" ), @@ -152,6 +153,7 @@ def main( convos, contacts = fetch_data( source_dir, + password=password, chats=chats, include_empty=include_empty, )