Signal-iOS/Scripts/sqlclient

202 lines
7.7 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_DBPATH_KEY = "dbPath"
SIGNAL_DEBUG_PAYLOAD_PASSPHRASE_KEY = "key"
SIGNAL_FALLBACK_DATABASE_PATH = "grdb/signal.sqlite"
quietMode=False
def failWithError(string):
print("Error: " + string, file=sys.stderr)
exit(1)
def printInfo(string = ""):
if quietMode == False:
print(string)
def runCommand(cmd):
result = subprocess.run(cmd.split(), text=True, capture_output=True)
if result.returncode != 0:
failWithError("Failed to run \"" + cmd + "\". 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)
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):
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:
printInfo("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] [--quiet]")
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.")
parser.add_argument("--quiet", action='store_true', help="Suppress non-failing output")
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 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)