Add code to get the Signal SQL decryption key
This commit is contained in:
parent
969269a70f
commit
648d26092c
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.key
|
55
decrypt_api.py
Normal file
55
decrypt_api.py
Normal 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
17
decrypt_from_appdata.py
Normal 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
19
decrypt_from_keys.py
Normal 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
21
readme.md
Normal 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.
|
||||
|
Loading…
Reference in New Issue
Block a user