-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
sqlclient
executable file
·242 lines (200 loc) · 9.58 KB
/
sqlclient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#!/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)