This change makes the debug the sqlcipher database with command line much easier. Leverages simctl to make simulator discovery and introspection super straightforward
195 lines
7.3 KiB
Python
Executable File
195 lines
7.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import getpass
|
|
import json
|
|
import os
|
|
import subprocess
|
|
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_DBPATH_KEY = "dbPath"
|
|
SIGNAL_DEBUG_PAYLOAD_PASSPHRASE_KEY = "key"
|
|
SIGNAL_FALLBACK_DATABASE_PATH = "grdb/signal.sqlite"
|
|
|
|
def runCommand(cmd):
|
|
result = subprocess.run(cmd.split(), text=True, capture_output=True)
|
|
if result.returncode != 0:
|
|
print("Failed to run \"" + cmd + "\". Status: " + str(result.returncode))
|
|
if len(result.stderr) > 0:
|
|
print("Error: " + result.stderr)
|
|
exit(1)
|
|
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)
|
|
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:
|
|
print("Error: Could not find a \"" + searchString + "\" simulator")
|
|
exit(1)
|
|
elif len(candidates) == 1:
|
|
selectedCandidate = candidates[0]
|
|
else:
|
|
for idx, candidate in enumerate(candidates):
|
|
print("{}:\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)
|
|
print("Selected simulator: " + selectedCandidate["name"] + " (" + selectedCandidate["udid"] + ")")
|
|
print("Using groupID: " + self.groupID)
|
|
print()
|
|
|
|
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):
|
|
debugPayload = self.parseDebugPayload()
|
|
|
|
if debugPayload and SIGNAL_DEBUG_PAYLOAD_DBPATH_KEY in debugPayload:
|
|
payloadPath = debugPayload[SIGNAL_DEBUG_PAYLOAD_DBPATH_KEY]
|
|
if os.path.isfile(payloadPath):
|
|
return payloadPath
|
|
else:
|
|
print("Debug payload " + payloadPath[-50:] + " not found. Falling back the standard path.")
|
|
|
|
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)
|
|
return result.rstrip()
|
|
|
|
def preparePassphrase(passphrase):
|
|
if len(passphrase) > 0 and passphrase[0] == 'x':
|
|
return passphrase
|
|
else:
|
|
return "x'" + passphrase + "'"
|
|
|
|
|
|
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.
|
|
'''),
|
|
usage="%(prog)s [--simulator simID [--staging] | --path dbPath] [--passphrase passphrase]")
|
|
|
|
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="An sqlcipher path")
|
|
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.")
|
|
args = parser.parse_args()
|
|
|
|
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:
|
|
print("Error: No valid database path")
|
|
exit(1)
|
|
elif os.path.isfile(dbPath) == False:
|
|
print("Error: Not valid path " + dbPath)
|
|
exit(1)
|
|
|
|
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 passphrase == None or len(passphrase) == 0:
|
|
print("Error: No valid sqlcipher passphrase")
|
|
exit(1)
|
|
|
|
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)
|
|
|