Add code to get the Signal SQL decryption key

This commit is contained in:
Michael Campagnaro 2025-03-19 16:57:03 -04:00
parent 969269a70f
commit 648d26092c
5 changed files with 113 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.key

55
decrypt_api.py Normal file
View File

@ -0,0 +1,55 @@
import base64, win32crypt
from Crypto.Cipher import AES
def decrypt_and_print_cipher_key(os_key_b64, config_key_hex):
print("GOT OS KEY:", os_key_b64)
print("GOT CONFIG KEY:", config_key_hex)
encrypted_os_key = base64.b64decode(os_key_b64)[5:] # Remove DPAPI prefix at the end.
master_key = win32crypt.CryptUnprotectData(encrypted_os_key, None, None, None, 0)[1] # This is the AES-GCM key.
assert len(master_key) == 32
print(f"OS Decoded length: {len(encrypted_os_key)} bytes")
print(f"OS Decoded data (hex): {encrypted_os_key.hex()}")
print(f"OS master key (hex): {master_key.hex()}")
# Determined the Signal config encryption by reading the source code and seeing that it's using
# Electron's crypt API. Tracing that, they're using Chromium's crypt API. Reading that, we can
# see exactly what's being done.
#
# Encrypted config key is AES-GCM encrypted. First 3 bytes is the version,
# next 12 bytes is the nonce (IV), last 16 bytes is the AES authentication tag,
# middle bytes is the ciphertext.
encrypted_key = bytes.fromhex(config_key_hex)
assert encrypted_key[:3] == b"v10"
print(f"Config Decoded length: {len(encrypted_key)} bytes")
print(f"Config Decoded data (hex): {encrypted_key.hex()}")
nonce = encrypted_key[3:15] # Skip the version.
ciphertext = encrypted_key[15:-16]
tag = encrypted_key[-16:]
print(f"Config nonce: {nonce.hex()}")
print(f"Config cipher: {ciphertext.hex()}")
print(f"Config auth tag: {tag.hex()}")
assert len(nonce) == 12
assert len(tag) == 16
cipher = AES.new(master_key, AES.MODE_GCM, nonce)
decrypted_cipher_key = cipher.decrypt_and_verify(ciphertext, tag)
cipher_key_len = len(decrypted_cipher_key)
print(f"Decrypted SQLCipher key: {decrypted_cipher_key.hex()}")
print(f"Key length: {cipher_key_len} bytes")
raw_binary_key = decrypted_cipher_key
if cipher_key_len == 64: # Likely hex-encoded
print("Likely hex-encoded. Final key should be 32 bytes.")
raw_binary_key = bytes.fromhex(decrypted_cipher_key.decode("utf-8").strip())
assert len(raw_binary_key) == 32
print(f"\n------------------------------------------------------------\nBinary key to use in SQLCipher (DB Browser for SQLite), using SQLCipher 4 defaults (4096 page size, raw key password):\n0x{raw_binary_key.hex()}")

17
decrypt_from_appdata.py Normal file
View File

@ -0,0 +1,17 @@
import os, json
from decrypt_api import *
if __name__ == "__main__":
appdata_path = os.getenv('APPDATA')
with open(f"{appdata_path}\Signal\Local State", "r", encoding="utf-8") as f:
local_state = json.load(f)
with open(f"{appdata_path}\Signal\config.json", "r", encoding="utf-8") as f:
config = json.load(f)
os_key_b64 = local_state["os_crypt"]["encrypted_key"]
config_key_hex = config["encryptedKey"]
decrypt_and_print_cipher_key(os_key_b64, config_key_hex)

19
decrypt_from_keys.py Normal file
View File

@ -0,0 +1,19 @@
import sys
from decrypt_api import *
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Pass the filename of the OS master key value (key is in the Signal local state file) and the filename of the config encrypted key value (config.json); see the appdata version if confused")
exit(1)
os_key_filename = sys.argv[1]
config_key_filename = sys.argv[2]
with open(os_key_filename, "r") as f:
os_key_b64 = f.read().strip()
with open(config_key_filename, "r") as f:
config_key_hex = f.read().strip()
decrypt_and_print_cipher_key(os_key_b64, config_key_hex)

21
readme.md Normal file
View File

@ -0,0 +1,21 @@
Outputs the key used to decrypt the Signal Desktop sqlite DB, as of Signal v7.46.1 (2025-03-19). This only works on Windows but I'm sure it's not hard to adapt it for other platforms.
You will need some Python libs, like win32crypt and maybe one other, I forget. Can install with pip.
The encryption key can change, so the easiest way to use this is to run `decrypt_from_appdata.py`.
If you're making a backup of the encrypted DB, then close Signal, grab a copy
the SQL file (or maybe it's better to copy the entire Signal folder in
`appdata/roaming`?), and create two keyfiles containing the encrypted OS master
key string from the Signal local_state file and the encrypted config.json key
string. You can then use the `decrypt_from_keys.py` version in the future with
those two key files, but you'll need to do it on the same Windows user account
that encrypted the DB. If you're concerned that you won't have access then just
get the decryption key from the script and save it somewhere for future use. If
any of this confuses you then look at the appdata python code to see where the
key values are stored.
---
For Michael's PC: Need to run this from Windows CMD because I can't get the win32crypt lib to install in msys.