Add code to get the Signal SQL decryption key
This commit is contained in:
		
							parent
							
								
									969269a70f
								
							
						
					
					
						commit
						b4c32c506c
					
				
							
								
								
									
										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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										20
									
								
								readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								readme.md
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					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, 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