Skip to content

Instantly share code, notes, and snippets.

@neuhaus
Last active May 23, 2026 18:47
Show Gist options
  • Select an option

  • Save neuhaus/6f2c91199adf26e6f66ec161cd2bfed4 to your computer and use it in GitHub Desktop.

Select an option

Save neuhaus/6f2c91199adf26e6f66ec161cd2bfed4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
Signal Decryption Tool for macOS
Decrypts Signal's encrypted keys stored in the macOS Keychain.
Ported from Rust to Python.
References:
- https://github.com/fjh658/signal-decryption-tool
- https://github.com/electron/electron/blob/41b8fdca5c53a41eabdad9a6a75b45bda4a6f37b/shell/browser/api/electron_api_safe_storage.cc
- https://chromium.googlesource.com/chromium/src/+/refs/tags/130.0.6686.2/components/os_crypt/sync/os_crypt_mac.mm
"""
import argparse
import json
import subprocess
import sys
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes, padding
# ── Constants ────────────────────────────────────────────────────────────────
SALT = b"saltysalt"
ITERATIONS = 1003
KEY_LENGTH = 16 # AES-128
ENCRYPTION_VERSION_PREFIX = b"v10"
DEFAULT_CONFIG_PATH = f"{__import__('os').path.expanduser('~/Library/Application Support/Signal/config.json')}"
SERVICE_NAME = "Signal Safe Storage"
# ── Key derivation (PBKDF2-HMAC-SHA1) ────────────────────────────────────────
def derive_key(password: str) -> bytes:
"""Derive a 16-byte AES key from the password using PBKDF2-HMAC-SHA1."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA1(),
length=KEY_LENGTH,
salt=SALT,
iterations=ITERATIONS,
)
return kdf.derive(password.encode("utf-8"))
# ── AES-128-CBC with fixed IV (16 spaces) ───────────────────────────────────
def encrypt_aes(plaintext: str, aes_key: bytes) -> bytes:
"""Encrypt plaintext with AES-128-CBC and fixed IV of 16 space characters."""
iv = b" " * KEY_LENGTH
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder()
padded = padder.update(plaintext.encode("utf-8")) + padder.finalize()
return encryptor.update(padded) + encryptor.finalize()
def decrypt_aes(encrypted_data: bytes, aes_key: bytes) -> str:
"""Decrypt AES-128-CBC ciphertext with fixed IV of 16 space characters."""
iv = b" " * KEY_LENGTH
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
decryptor = cipher.decryptor()
padded = decryptor.update(encrypted_data) + decryptor.finalize()
try:
unpadder = padding.PKCS7(128).unpadder()
plaintext = unpadder.update(padded) + unpadder.finalize()
return plaintext.decode("utf-8")
except (ValueError, UnicodeDecodeError):
print("Error: Decryption failed. The secure storage key might be incorrect, or the data is corrupted.", file=sys.stderr)
sys.exit(1)
# ── Keychain access ──────────────────────────────────────────────────────────
def get_keychain_password(service: str, account: str) -> str | None:
"""Retrieve a password from macOS Keychain using the `security` CLI."""
try:
result = subprocess.run(
[
"security", "find-generic-password",
"-s", service,
"-a", account,
"-w",
],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
return result.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
return None
def get_secure_password() -> str:
"""Retrieve the Signal secure storage password, trying 'Signal Key' then 'Signal'."""
password = get_keychain_password(SERVICE_NAME, "Signal Key")
if password is None:
password = get_keychain_password(SERVICE_NAME, "Signal")
if password is None:
print("Error: Could not find secure storage password in Keychain.", file=sys.stderr)
sys.exit(1)
return password
# ── Config loading ───────────────────────────────────────────────────────────
def load_encrypted_key(config_path: str) -> str:
"""Load the encrypted key from Signal's config.json."""
try:
with open(config_path, "r") as f:
config = json.load(f)
except FileNotFoundError:
print(f"Error: Config file not found at '{config_path}'", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError:
print(f"Error: Malformed JSON in config file at '{config_path}'", file=sys.stderr)
sys.exit(1)
encrypted_key = config.get("encryptedKey")
if not encrypted_key:
print("Error: 'encryptedKey' not found in config.json.", file=sys.stderr)
sys.exit(1)
return encrypted_key
# ── Main decryption ──────────────────────────────────────────────────────────
def decrypt_signal_key(encrypted_hex: str, aes_key: bytes) -> str:
"""Decode hex, strip v10 prefix, and decrypt."""
try:
data = bytes.fromhex(encrypted_hex)
except ValueError:
print("Error: The encrypted key is not a valid hex string.", file=sys.stderr)
sys.exit(1)
if not data.startswith(ENCRYPTION_VERSION_PREFIX):
print(f"Error: Invalid encryption version prefix (expected 'v10').", file=sys.stderr)
sys.exit(1)
ciphertext = data[len(ENCRYPTION_VERSION_PREFIX):]
return decrypt_aes(ciphertext, aes_key)
def main():
parser = argparse.ArgumentParser(
description="Decrypt Signal's encrypted keys on macOS.",
)
parser.add_argument(
"-c", "--config",
default=None,
help="Path to config.json (default: ~/Library/Application Support/Signal/config.json)",
)
parser.add_argument(
"-k", "--key",
default=None,
help="Provide an encrypted key directly (hex string)",
)
parser.add_argument(
"-p", "--print-key",
action="store_true",
help="Print the secure storage key (use with caution)",
)
parser.add_argument(
"--version",
action="version",
version="SignalDecryption 1.0.0 (Python, macOS)",
)
args = parser.parse_args()
# 1. Get the secure password from Keychain
secure_password = get_secure_password()
if args.print_key:
print(f"Secure password retrieved: {secure_password}")
# 2. Derive the AES key
aes_key = derive_key(secure_password)
# 3. Get the encrypted key (from CLI or config file)
if args.key:
encrypted_key_str = args.key
print("Using directly provided encrypted key.")
else:
config_path = args.config or DEFAULT_CONFIG_PATH
print(f"Using config path: {config_path}")
encrypted_key_str = load_encrypted_key(config_path)
# 4. Decrypt
print(f"Encrypted key: {encrypted_key_str}")
decrypted = decrypt_signal_key(encrypted_key_str, aes_key)
print(f"Decrypted key: {decrypted}")
if __name__ == "__main__":
main()
@neuhaus

neuhaus commented May 22, 2026

Copy link
Copy Markdown
Author

This was oneshotted with qwen 3.6 27b and hermes agent, it required only a simple fix.

This is for use with Signal desktop on macOS.

How to use:

  1. run the script to get your key
  2. make a copy of Library/Application Support/Signal/sql/db.sqlite
  3. Install sqlcipher with brew install sqlcipher
  4. sqlcipher db.sqlite
    sqlite> PRAGMA key = "x'hexkeyhere'";
    sqlite> select * from messages where body like 'I love you'
    

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment