TM-SGNL-iOS/Scripts/sqlclient
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

242 lines
9.6 KiB
Python
Executable file

#!/usr/bin/env python3
import argparse
import getpass
import json
import os
import subprocess
import sys
import textwrap
SIGNAL_BUNDLEID = "org.whispersystems.signal"
SIGNAL_APPGROUP = "group.org.whispersystems.signal.group"
SIGNAL_APPGROUP_STAGING = "group.org.whispersystems.signal.group.staging"
SIGNAL_DEBUG_PAYLOAD_NAME = "dbPayload.txt"
SIGNAL_DEBUG_PAYLOAD_PASSPHRASE_KEY = "key"
SIGNAL_FALLBACK_DATABASE_PATH = "grdb/signal.sqlite"
DB_BROWSER_FOR_SQLITE_BUNDLEID = "net.sourceforge.sqlitebrowser"
quietMode=False
def failWithError(string):
print("Error: " + string, file=sys.stderr)
exit(1)
def printInfo(string = ""):
if quietMode == False:
print(string)
def runCommand(cmdList):
result = subprocess.run(cmdList, text=True, capture_output=True)
if result.returncode != 0:
failWithError("Failed to run \"" + " ".join(cmdList) + "\". Status: " + str(result.returncode) + "\n" + result.stderr)
return result.stdout
class Simulator:
def __init__(self, searchString, useStaging):
# Get JSON list of simulators matching searchString
cmd = "xcrun simctl list -j devices " + searchString
resultString = runCommand(cmd.split())
simDict = json.loads(resultString)
devicesByRuntime = simDict["devices"]
# Parse all candidates
candidates = []
for runtime, devices in devicesByRuntime.items():
os = self.parseOSFromRuntime(runtime)
for device in devices:
udid = device.get("udid")
rawDevice = device.get("deviceTypeIdentifier")
name = device.get("name")
if udid != None:
deviceType = self.parseDeviceTypeFromRaw(rawDevice)
candidates.append({"os": os, "type": deviceType, "udid": udid, "name": name})
# Select a candidate
selectedCandidate = None
if len(candidates) == 0:
failWithError("Could not find a \"" + searchString + "\" simulator")
elif len(candidates) == 1:
selectedCandidate = candidates[0]
else:
if quietMode:
failWithError("Multiple simulator candidates. Interactive selection not supported in quiet mode")
for idx, candidate in enumerate(candidates):
printInfo("{}:\t{:40}\t{} {} ({})".format(idx, candidate["name"], candidate["type"], candidate["os"], candidate["udid"]))
while selectedCandidate == None:
try:
idx = int(input("Select a simulator: "))
selectedCandidate = candidates[idx]
except (ValueError, IndexError):
pass
self.udid = selectedCandidate["udid"]
self.groupID = SIGNAL_APPGROUP_STAGING if useStaging else SIGNAL_APPGROUP
self.groupContainer = self.fetchGroupContainer(self.udid, self.groupID)
printInfo("Selected simulator: " + selectedCandidate["name"] + " (" + selectedCandidate["udid"] + ")")
printInfo("Using groupID: " + self.groupID)
printInfo()
def parseDebugPayload(self):
path = self.groupContainer + "/" + SIGNAL_DEBUG_PAYLOAD_NAME
try:
fd = open(path, 'r')
data = fd.read()
payload = json.loads(data)
return payload
except IOError:
return None
def databasePath(self):
return (self.groupContainer + "/" + SIGNAL_FALLBACK_DATABASE_PATH)
def passphraseIfAvailable(self):
debugPayload = self.parseDebugPayload()
if debugPayload and SIGNAL_DEBUG_PAYLOAD_PASSPHRASE_KEY in debugPayload:
return debugPayload[SIGNAL_DEBUG_PAYLOAD_PASSPHRASE_KEY]
else:
return None
@staticmethod
def parseOSFromRuntime(runtime):
lastPeriodIdx = runtime.rfind('.')
hypenatedOS = runtime[lastPeriodIdx+1:]
return hypenatedOS.replace("-", ".")
@staticmethod
def parseDeviceTypeFromRaw(rawDevice):
lastPeriodIdx = rawDevice.rfind('.')
hypenatedOS = rawDevice[lastPeriodIdx+1:]
return hypenatedOS.replace("-", " ")
@staticmethod
def fetchGroupContainer(udid, groupID):
cmd = "xcrun simctl get_app_container {} {} {}".format(udid, SIGNAL_BUNDLEID, groupID)
result = runCommand(cmd.split())
return result.rstrip()
def preparePassphrase(passphrase):
if len(passphrase) > 0 and passphrase[0] == 'x':
return passphrase
else:
return "x'" + passphrase + "'"
def writeGuiEnvFile(passphrase, dbPath):
dbName = os.path.basename(dbPath)
envFilePath = os.path.join(os.path.dirname(dbPath), ".env")
with open(envFilePath, "w", encoding="utf-8") as envFile:
envFile.write(dbName + " = " + passphrase + "\n")
envFile.write(dbName + "_plaintextHeaderSize = 32\n")
return envFilePath
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent('''\
SQLCipher Command Line Interface
If providing a simulatorID (or accepting the default "Booted" simulator), passphrase retrieval
can be simplified by navigating to Signal Settings > Debug UI > Misc > Save plaintext database key.
If a database key could not be found and one was not provided through an argument, you'll be prompted
to enter one.
Alternatively, you can provide a sqlcipher path directly via command line arguments. In this case,
you'll be required to provide a database key through an argument or stdin.
If --use-gui is specified, this script will attempt to open the database using the "DB Browser for
SQLite" (DBBfS) application.
If --gui-auto-decrypt-with-plaintext-key is passed alongside --use-gui, the script will place the
passphrase in a file next to the database file such that DBBfS is able to automatically decrypt and
open the databse. Note that this file is in plaintext, and *ONLY USE* with databases containing
test data.
'''),
usage="%(prog)s [--simulator simID [--staging] | --path dbPath] [--passphrase passphrase] [--quiet] [--use-gui [--gui-auto-decrypt-with-plaintext-key]]")
group = parser.add_mutually_exclusive_group()
group.add_argument("--simulator", metavar="SIM", help="A string identifiying a simulator instance. (default: %(default)s).", default="booted")
group.add_argument("--path", help="Path to a sqlcipher DB")
parser.add_argument("--passphrase", metavar="PASS", help="The passphrase encrypting the database")
parser.add_argument("--staging", action='store_true', help="If a simulator is being targeted, specifies that the staging database should be used")
parser.add_argument("remainder", nargs=argparse.REMAINDER, metavar="--", help="All subsequent args will be interpreted as SQL. You probably want quotes here. Be careful with \"*\" since your shell will probably replace it. Ignored if using GUI")
parser.add_argument("--quiet", action='store_true', help="Suppress non-failing output")
parser.add_argument(
"--use-gui",
action='store_true',
help="Tells the script to try and open DB Browser for SQLite"
)
parser.add_argument(
"--gui-auto-decrypt-with-plaintext-key",
action='store_true',
help=(
"Tells the script to try and have DB Browser for SQLite auto-decrypt the database by "
"placing the key in plaintext next to the DB file. ONLY USE with DBs guaranteed to "
"only contain test data"
)
)
args = parser.parse_args()
quietMode=args.quiet
dbPath = None
passphrase = None
if args.path:
dbPath = args.path
elif args.simulator:
target = Simulator(args.simulator, args.staging)
dbPath = target.databasePath()
passphrase = target.passphraseIfAvailable()
if dbPath == None:
failWithError("No valid database path")
elif os.path.isfile(dbPath) == False:
failWithError("Not valid path " + dbPath)
if args.passphrase:
passphrase = args.passphrase
if passphrase == None:
passphrase = getpass.getpass("Please enter the passphrase. Alternatively, set up a plaintext database key in Debug UI > Misc > Save plaintext database key. Then, rerun the command. ")
if args.use_gui:
if args.gui_auto_decrypt_with_plaintext_key:
if passphrase == None or len(passphrase) == 0:
failWithError("Missing sqlcipher passphrase for auto-decryption")
passphrase = preparePassphrase(passphrase)
envFilePath = writeGuiEnvFile(passphrase, dbPath)
printInfo("Warning: saved passphrase to " + envFilePath + " for auto-decryption.")
else:
printInfo(textwrap.dedent('''\
When prompted for the passphrase, select the SQLCipher 4 default settings.
Then, select "Custom" and set the "Plaintext Header Size" to 32 from 0.
Finally, select "Raw Key" instead of "Passphrase", manually enter "0x", and paste the key.
'''))
runCommand(["open", "-b", DB_BROWSER_FOR_SQLITE_BUNDLEID, dbPath])
else:
if passphrase == None or len(passphrase) == 0:
failWithError("No valid sqlcipher passphrase")
passphrase = preparePassphrase(passphrase)
sqlArgs = args.remainder
if len(sqlArgs) > 0 and sqlArgs[0] == "--":
sqlArgs.pop(0)
sqlArgString = " ".join(sqlArgs)
allArgs = [
"sqlcipher",
"-cmd", "PRAGMA key = \"" + passphrase + "\";",
"-cmd", "PRAGMA cipher_plaintext_header_size = 32;",
dbPath
]
if len(sqlArgString) > 0:
allArgs.append(sqlArgString)
os.execvp("sqlcipher", allArgs)