dfindexeddb 20240501__tar.gz → 20240519__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dfindexeddb-20240501/dfindexeddb.egg-info → dfindexeddb-20240519}/PKG-INFO +34 -13
- {dfindexeddb-20240501 → dfindexeddb-20240519}/README.md +30 -12
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/blink.py +5 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/record.py +11 -6
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/cli.py +16 -6
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/safari/webkit.py +15 -7
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/cli.py +69 -6
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/log.py +9 -3
- dfindexeddb-20240519/dfindexeddb/leveldb/plugins/__init__.py +17 -0
- dfindexeddb-20240519/dfindexeddb/leveldb/plugins/chrome_notifications.py +135 -0
- dfindexeddb-20240519/dfindexeddb/leveldb/plugins/interface.py +36 -0
- dfindexeddb-20240519/dfindexeddb/leveldb/plugins/manager.py +75 -0
- dfindexeddb-20240519/dfindexeddb/leveldb/plugins/notification_database_data_pb2.py +38 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/record.py +33 -1
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/utils.py +34 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/version.py +1 -1
- {dfindexeddb-20240501 → dfindexeddb-20240519/dfindexeddb.egg-info}/PKG-INFO +34 -13
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/SOURCES.txt +6 -1
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/requires.txt +4 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/pyproject.toml +8 -4
- {dfindexeddb-20240501 → dfindexeddb-20240519}/AUTHORS +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/LICENSE +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/__init__.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/errors.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/__init__.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/__init__.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/definitions.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/v8.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/firefox/__init__.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/safari/__init__.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/safari/definitions.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/safari/record.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/utils.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/__init__.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/definitions.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/descriptor.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/ldb.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/utils.py +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/dependency_links.txt +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/entry_points.txt +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/top_level.txt +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/setup.cfg +0 -0
- {dfindexeddb-20240501 → dfindexeddb-20240519}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dfindexeddb
|
|
3
|
-
Version:
|
|
3
|
+
Version: 20240519
|
|
4
4
|
Summary: dfindexeddb is an experimental Python tool for performing digital forensic analysis of IndexedDB and leveldb files.
|
|
5
5
|
Author-email: Syd Pleno <sydp@google.com>
|
|
6
6
|
Maintainer-email: dfIndexeddb Developers <dfindexeddb-dev@googlegroups.com>
|
|
@@ -219,6 +219,9 @@ License-File: LICENSE
|
|
|
219
219
|
License-File: AUTHORS
|
|
220
220
|
Requires-Dist: python-snappy==0.6.1
|
|
221
221
|
Requires-Dist: zstd==1.5.5.1
|
|
222
|
+
Provides-Extra: plugins
|
|
223
|
+
Requires-Dist: protobuf; extra == "plugins"
|
|
224
|
+
Requires-Dist: dfdatetime; extra == "plugins"
|
|
222
225
|
|
|
223
226
|
# dfIndexeddb
|
|
224
227
|
|
|
@@ -255,6 +258,12 @@ include:
|
|
|
255
258
|
$ pip install dfindexeddb
|
|
256
259
|
```
|
|
257
260
|
|
|
261
|
+
To also install the dependencies for leveldb/indexeddb plugins, run
|
|
262
|
+
```
|
|
263
|
+
$ pip install 'dfindexeddb[plugins]'
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
|
|
258
267
|
## Installation from source
|
|
259
268
|
|
|
260
269
|
1. [Linux] Install the snappy compression development package
|
|
@@ -273,6 +282,11 @@ include:
|
|
|
273
282
|
$ pip install .
|
|
274
283
|
```
|
|
275
284
|
|
|
285
|
+
To also install the dependencies for leveldb/indexeddb plugins, run
|
|
286
|
+
```
|
|
287
|
+
$ pip install '.[plugins]'
|
|
288
|
+
```
|
|
289
|
+
|
|
276
290
|
## Usage
|
|
277
291
|
|
|
278
292
|
Two CLI tools for parsing IndexedDB/LevelDB files are available after
|
|
@@ -359,7 +373,15 @@ options:
|
|
|
359
373
|
To parse records from a LevelDB folder, use the following command:
|
|
360
374
|
|
|
361
375
|
```
|
|
362
|
-
|
|
376
|
+
dfleveldb db -s SOURCE
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
To parse records from a LevelDB folder, and use the sequence number to
|
|
380
|
+
determine recovered records and output as JSON, use the
|
|
381
|
+
following command:
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
dfleveldb db -s SOURCE --use_sequence_number
|
|
363
385
|
```
|
|
364
386
|
|
|
365
387
|
To parse blocks / physical records/ write batches / internal key records from a
|
|
@@ -383,15 +405,14 @@ following command:
|
|
|
383
405
|
|
|
384
406
|
```
|
|
385
407
|
$ dfleveldb descriptor -s SOURCE [-o {json,jsonl,repr}] [-t {blocks,physical_records,versionedit} | -v]
|
|
386
|
-
|
|
387
|
-
options:
|
|
388
|
-
-h, --help show this help message and exit
|
|
389
|
-
-s SOURCE, --source SOURCE
|
|
390
|
-
The source leveldb file
|
|
391
|
-
-o {json,jsonl,repr}, --output {json,jsonl,repr}
|
|
392
|
-
Output format. Default is json
|
|
393
|
-
-t {blocks,physical_records,versionedit}, --structure_type {blocks,physical_records,versionedit}
|
|
394
|
-
Parses the specified structure. Default is versionedit.
|
|
395
|
-
-v, --version_history
|
|
396
|
-
Parses the leveldb version history.
|
|
397
408
|
```
|
|
409
|
+
|
|
410
|
+
#### Plugins
|
|
411
|
+
|
|
412
|
+
To apply a plugin parser for a leveldb file/folder, add the
|
|
413
|
+
`--plugin [Plugin Name]` argument. Currently, there is support for the
|
|
414
|
+
following artifacts:
|
|
415
|
+
|
|
416
|
+
| Plugin Name | Artifact Name |
|
|
417
|
+
| -------- | ------- |
|
|
418
|
+
| `ChromeNotificationRecord` | Chrome/Chromium Notifications |
|
|
@@ -33,6 +33,12 @@ include:
|
|
|
33
33
|
$ pip install dfindexeddb
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
To also install the dependencies for leveldb/indexeddb plugins, run
|
|
37
|
+
```
|
|
38
|
+
$ pip install 'dfindexeddb[plugins]'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
|
|
36
42
|
## Installation from source
|
|
37
43
|
|
|
38
44
|
1. [Linux] Install the snappy compression development package
|
|
@@ -51,6 +57,11 @@ include:
|
|
|
51
57
|
$ pip install .
|
|
52
58
|
```
|
|
53
59
|
|
|
60
|
+
To also install the dependencies for leveldb/indexeddb plugins, run
|
|
61
|
+
```
|
|
62
|
+
$ pip install '.[plugins]'
|
|
63
|
+
```
|
|
64
|
+
|
|
54
65
|
## Usage
|
|
55
66
|
|
|
56
67
|
Two CLI tools for parsing IndexedDB/LevelDB files are available after
|
|
@@ -137,7 +148,15 @@ options:
|
|
|
137
148
|
To parse records from a LevelDB folder, use the following command:
|
|
138
149
|
|
|
139
150
|
```
|
|
140
|
-
|
|
151
|
+
dfleveldb db -s SOURCE
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
To parse records from a LevelDB folder, and use the sequence number to
|
|
155
|
+
determine recovered records and output as JSON, use the
|
|
156
|
+
following command:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
dfleveldb db -s SOURCE --use_sequence_number
|
|
141
160
|
```
|
|
142
161
|
|
|
143
162
|
To parse blocks / physical records/ write batches / internal key records from a
|
|
@@ -161,15 +180,14 @@ following command:
|
|
|
161
180
|
|
|
162
181
|
```
|
|
163
182
|
$ dfleveldb descriptor -s SOURCE [-o {json,jsonl,repr}] [-t {blocks,physical_records,versionedit} | -v]
|
|
164
|
-
|
|
165
|
-
options:
|
|
166
|
-
-h, --help show this help message and exit
|
|
167
|
-
-s SOURCE, --source SOURCE
|
|
168
|
-
The source leveldb file
|
|
169
|
-
-o {json,jsonl,repr}, --output {json,jsonl,repr}
|
|
170
|
-
Output format. Default is json
|
|
171
|
-
-t {blocks,physical_records,versionedit}, --structure_type {blocks,physical_records,versionedit}
|
|
172
|
-
Parses the specified structure. Default is versionedit.
|
|
173
|
-
-v, --version_history
|
|
174
|
-
Parses the leveldb version history.
|
|
175
183
|
```
|
|
184
|
+
|
|
185
|
+
#### Plugins
|
|
186
|
+
|
|
187
|
+
To apply a plugin parser for a leveldb file/folder, add the
|
|
188
|
+
`--plugin [Plugin Name]` argument. Currently, there is support for the
|
|
189
|
+
following artifacts:
|
|
190
|
+
|
|
191
|
+
| Plugin Name | Artifact Name |
|
|
192
|
+
| -------- | ------- |
|
|
193
|
+
| `ChromeNotificationRecord` | Chrome/Chromium Notifications |
|
|
@@ -780,6 +780,9 @@ class V8ScriptValueDecoder:
|
|
|
780
780
|
|
|
781
781
|
Returns:
|
|
782
782
|
A parsed CryptoKey.
|
|
783
|
+
|
|
784
|
+
Raises:
|
|
785
|
+
ParserError: if there is an unexpected CryptoKeySubTag.
|
|
783
786
|
"""
|
|
784
787
|
_, raw_key_byte = self.deserializer.decoder.DecodeUint8()
|
|
785
788
|
key_byte = definitions.CryptoKeySubTag(raw_key_byte)
|
|
@@ -795,6 +798,8 @@ class V8ScriptValueDecoder:
|
|
|
795
798
|
key_type, algorithm_parameters = self._ReadED25519Key()
|
|
796
799
|
elif key_byte == definitions.CryptoKeySubTag.NO_PARAMS_KEY:
|
|
797
800
|
key_type, algorithm_parameters = self.ReadNoParamsKey()
|
|
801
|
+
else:
|
|
802
|
+
raise errors.ParserError('Unexpected CryptoKeySubTag')
|
|
798
803
|
|
|
799
804
|
_, raw_usages = self.deserializer.decoder.DecodeUint32Varint()
|
|
800
805
|
usages = definitions.CryptoKeyUsage(raw_usages)
|
|
@@ -1275,14 +1275,17 @@ class ExternalObjectEntry(utils.FromDecoderMixin):
|
|
|
1275
1275
|
filename = None
|
|
1276
1276
|
last_modified = None
|
|
1277
1277
|
token = None
|
|
1278
|
-
|
|
1279
|
-
|
|
1278
|
+
else:
|
|
1279
|
+
if (object_type ==
|
|
1280
|
+
definitions.ExternalObjectType.FILE_SYSTEM_ACCESS_HANDLE):
|
|
1281
|
+
_, token = decoder.DecodeBlobWithLength()
|
|
1282
|
+
else:
|
|
1283
|
+
token = None
|
|
1280
1284
|
blob_number = None
|
|
1281
1285
|
mime_type = None
|
|
1282
1286
|
size = None
|
|
1283
1287
|
filename = None
|
|
1284
1288
|
last_modified = None
|
|
1285
|
-
_, token = decoder.DecodeBlobWithLength()
|
|
1286
1289
|
|
|
1287
1290
|
return cls(offset=base_offset + offset, object_type=object_type,
|
|
1288
1291
|
blob_number=blob_number, mime_type=mime_type, size=size,
|
|
@@ -1417,20 +1420,22 @@ class FolderReader:
|
|
|
1417
1420
|
|
|
1418
1421
|
def GetRecords(
|
|
1419
1422
|
self,
|
|
1420
|
-
use_manifest: bool = False
|
|
1423
|
+
use_manifest: bool = False,
|
|
1424
|
+
use_sequence_number: bool = False
|
|
1421
1425
|
) -> Generator[IndexedDBRecord, None, None]:
|
|
1422
1426
|
"""Yield LevelDBRecords.
|
|
1423
1427
|
|
|
1424
1428
|
Args:
|
|
1425
1429
|
use_manifest: True to use the current manifest in the folder as a means to
|
|
1426
1430
|
find the active file set.
|
|
1427
|
-
|
|
1431
|
+
use_sequence_number: True to use the sequence number to determine the
|
|
1428
1432
|
Yields:
|
|
1429
1433
|
IndexedDBRecord.
|
|
1430
1434
|
"""
|
|
1431
1435
|
leveldb_folder_reader = record.FolderReader(self.foldername)
|
|
1432
1436
|
for leveldb_record in leveldb_folder_reader.GetRecords(
|
|
1433
|
-
use_manifest=use_manifest
|
|
1437
|
+
use_manifest=use_manifest,
|
|
1438
|
+
use_sequence_number=use_sequence_number):
|
|
1434
1439
|
try:
|
|
1435
1440
|
yield IndexedDBRecord.FromLevelDBRecord(
|
|
1436
1441
|
leveldb_record)
|
|
@@ -20,6 +20,7 @@ from datetime import datetime
|
|
|
20
20
|
import json
|
|
21
21
|
import pathlib
|
|
22
22
|
|
|
23
|
+
from dfindexeddb import utils
|
|
23
24
|
from dfindexeddb import version
|
|
24
25
|
from dfindexeddb.indexeddb.chromium import blink
|
|
25
26
|
from dfindexeddb.indexeddb.chromium import record as chromium_record
|
|
@@ -36,7 +37,7 @@ class Encoder(json.JSONEncoder):
|
|
|
36
37
|
"""A JSON encoder class for dfindexeddb fields."""
|
|
37
38
|
def default(self, o):
|
|
38
39
|
if dataclasses.is_dataclass(o):
|
|
39
|
-
o_dict =
|
|
40
|
+
o_dict = utils.asdict(o)
|
|
40
41
|
return o_dict
|
|
41
42
|
if isinstance(o, bytes):
|
|
42
43
|
out = []
|
|
@@ -83,7 +84,9 @@ def DbCommand(args):
|
|
|
83
84
|
"""The CLI for processing a directory as IndexedDB."""
|
|
84
85
|
if args.format in ('chrome', 'chromium'):
|
|
85
86
|
for db_record in chromium_record.FolderReader(
|
|
86
|
-
args.source).GetRecords(
|
|
87
|
+
args.source).GetRecords(
|
|
88
|
+
use_manifest=args.use_manifest,
|
|
89
|
+
use_sequence_number=args.use_sequence_number):
|
|
87
90
|
_Output(db_record, output=args.output)
|
|
88
91
|
elif args.format == 'safari':
|
|
89
92
|
for db_record in safari_record.FileReader(args.source).Records():
|
|
@@ -139,15 +142,22 @@ def App():
|
|
|
139
142
|
help=(
|
|
140
143
|
'The source IndexedDB folder (for chrome/chromium) '
|
|
141
144
|
'or file (for safari).'))
|
|
145
|
+
recover_group = parser_db.add_mutually_exclusive_group()
|
|
146
|
+
recover_group.add_argument(
|
|
147
|
+
'--use_manifest',
|
|
148
|
+
action='store_true',
|
|
149
|
+
help='Use manifest file to determine active/deleted records.')
|
|
150
|
+
recover_group.add_argument(
|
|
151
|
+
'--use_sequence_number',
|
|
152
|
+
action='store_true',
|
|
153
|
+
help=(
|
|
154
|
+
'Use sequence number and file offset to determine active/deleted '
|
|
155
|
+
'records.'))
|
|
142
156
|
parser_db.add_argument(
|
|
143
157
|
'--format',
|
|
144
158
|
required=True,
|
|
145
159
|
choices=['chromium', 'chrome', 'safari'],
|
|
146
160
|
help='The type of IndexedDB to parse.')
|
|
147
|
-
parser_db.add_argument(
|
|
148
|
-
'--use_manifest',
|
|
149
|
-
action='store_true',
|
|
150
|
-
help='Use manifest file to determine active/deleted records.')
|
|
151
161
|
parser_db.add_argument(
|
|
152
162
|
'-o',
|
|
153
163
|
'--output',
|
|
@@ -290,6 +290,7 @@ class SerializedScriptValueDecoder():
|
|
|
290
290
|
"""
|
|
291
291
|
_, length = self.decoder.DecodeUint32()
|
|
292
292
|
array = JSArray()
|
|
293
|
+
self.object_pool.append(array)
|
|
293
294
|
for _ in range(length):
|
|
294
295
|
_, _ = self.decoder.DecodeUint32()
|
|
295
296
|
_, value = self.DecodeValue()
|
|
@@ -314,13 +315,13 @@ class SerializedScriptValueDecoder():
|
|
|
314
315
|
"""Decodes an Object value."""
|
|
315
316
|
tag = self.PeekTag()
|
|
316
317
|
js_object = {}
|
|
318
|
+
self.object_pool.append(js_object)
|
|
317
319
|
while tag != definitions.TerminatorTag:
|
|
318
320
|
name = self.DecodeStringData()
|
|
319
321
|
_, value = self.DecodeValue()
|
|
320
322
|
js_object[name] = value
|
|
321
323
|
tag = self.PeekTag()
|
|
322
324
|
_ = self.decoder.DecodeUint32()
|
|
323
|
-
self.object_pool.append(js_object)
|
|
324
325
|
return js_object
|
|
325
326
|
|
|
326
327
|
def DecodeStringData(self) -> str:
|
|
@@ -342,11 +343,11 @@ class SerializedScriptValueDecoder():
|
|
|
342
343
|
|
|
343
344
|
if peeked_tag == definitions.StringPoolTag:
|
|
344
345
|
_ = self.decoder.DecodeUint32()
|
|
345
|
-
if len(self.constant_pool)
|
|
346
|
+
if len(self.constant_pool) <= 0xff:
|
|
346
347
|
_, cp_index = self.decoder.DecodeUint8()
|
|
347
|
-
elif len(self.constant_pool)
|
|
348
|
+
elif len(self.constant_pool) <= 0xffff:
|
|
348
349
|
_, cp_index = self.decoder.DecodeUint16()
|
|
349
|
-
elif len(self.constant_pool)
|
|
350
|
+
elif len(self.constant_pool) <= 0xffffffff:
|
|
350
351
|
_, cp_index = self.decoder.DecodeUint32()
|
|
351
352
|
else:
|
|
352
353
|
raise errors.ParserError('Unexpected constant pool size value.')
|
|
@@ -450,6 +451,7 @@ class SerializedScriptValueDecoder():
|
|
|
450
451
|
"""Decodes a Map value."""
|
|
451
452
|
tag = self.PeekSerializationTag()
|
|
452
453
|
js_map = {} # TODO: make this into a JSMap (like JSArray/JSSet)
|
|
454
|
+
self.object_pool.append(js_map)
|
|
453
455
|
|
|
454
456
|
while tag != definitions.SerializationTag.NON_MAP_PROPERTIES:
|
|
455
457
|
_, key = self.DecodeValue()
|
|
@@ -468,13 +470,13 @@ class SerializedScriptValueDecoder():
|
|
|
468
470
|
pool_tag = self.PeekTag()
|
|
469
471
|
|
|
470
472
|
_, tag = self.decoder.DecodeUint32()
|
|
471
|
-
|
|
472
473
|
return js_map
|
|
473
474
|
|
|
474
475
|
def DecodeSetData(self) -> JSSet:
|
|
475
476
|
"""Decodes a SetData value."""
|
|
476
477
|
tag = self.PeekSerializationTag()
|
|
477
478
|
js_set = JSSet()
|
|
479
|
+
self.object_pool.append(js_set)
|
|
478
480
|
|
|
479
481
|
while tag != definitions.SerializationTag.NON_SET_PROPERTIES:
|
|
480
482
|
_, key = self.DecodeValue()
|
|
@@ -540,8 +542,13 @@ class SerializedScriptValueDecoder():
|
|
|
540
542
|
|
|
541
543
|
def DecodeObjectReference(self) -> Any:
|
|
542
544
|
"""Decodes an ObjectReference value."""
|
|
543
|
-
|
|
544
|
-
|
|
545
|
+
if len(self.object_pool) < 0xFF:
|
|
546
|
+
_, object_ref = self.decoder.DecodeUint8()
|
|
547
|
+
elif len(self.object_pool) < 0xFFFF:
|
|
548
|
+
_, object_ref = self.decoder.DecodeUint16()
|
|
549
|
+
else: # if len(self.object_pool) < 0xFFFFFFFF:
|
|
550
|
+
_, object_ref = self.decoder.DecodeUint32()
|
|
551
|
+
return self.object_pool[object_ref]
|
|
545
552
|
|
|
546
553
|
def DecodeArrayBufferView(self) -> ArrayBufferView:
|
|
547
554
|
"""Decodes an ArrayBufferView value.
|
|
@@ -641,6 +648,7 @@ class SerializedScriptValueDecoder():
|
|
|
641
648
|
value = self.DecodeArrayBuffer()
|
|
642
649
|
elif tag == definitions.SerializationTag.ARRAY_BUFFER_VIEW:
|
|
643
650
|
value = self.DecodeArrayBufferView()
|
|
651
|
+
self.object_pool.append(value)
|
|
644
652
|
elif tag == definitions.SerializationTag.ARRAY_BUFFER_TRANSFER:
|
|
645
653
|
value = self.DecodeArrayBufferTransfer()
|
|
646
654
|
elif tag == definitions.SerializationTag.TRUE_OBJECT:
|
|
@@ -19,11 +19,13 @@ from datetime import datetime
|
|
|
19
19
|
import json
|
|
20
20
|
import pathlib
|
|
21
21
|
|
|
22
|
+
from dfindexeddb import utils
|
|
22
23
|
from dfindexeddb import version
|
|
23
24
|
from dfindexeddb.leveldb import descriptor
|
|
24
25
|
from dfindexeddb.leveldb import ldb
|
|
25
26
|
from dfindexeddb.leveldb import log
|
|
26
27
|
from dfindexeddb.leveldb import record
|
|
28
|
+
from dfindexeddb.leveldb.plugins import manager
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
_VALID_PRINTABLE_CHARACTERS = (
|
|
@@ -37,7 +39,7 @@ class Encoder(json.JSONEncoder):
|
|
|
37
39
|
def default(self, o):
|
|
38
40
|
"""Returns a serializable object for o."""
|
|
39
41
|
if dataclasses.is_dataclass(o):
|
|
40
|
-
o_dict =
|
|
42
|
+
o_dict = utils.asdict(o)
|
|
41
43
|
return o_dict
|
|
42
44
|
if isinstance(o, bytes):
|
|
43
45
|
out = []
|
|
@@ -66,13 +68,39 @@ def _Output(structure, output):
|
|
|
66
68
|
|
|
67
69
|
def DbCommand(args):
|
|
68
70
|
"""The CLI for processing leveldb folders."""
|
|
71
|
+
if args.plugin and args.plugin == 'list':
|
|
72
|
+
for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
|
|
73
|
+
print(plugin)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if args.plugin:
|
|
77
|
+
plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
|
|
78
|
+
else:
|
|
79
|
+
plugin_class = None
|
|
80
|
+
|
|
69
81
|
for leveldb_record in record.FolderReader(
|
|
70
|
-
args.source).GetRecords(
|
|
71
|
-
|
|
82
|
+
args.source).GetRecords(
|
|
83
|
+
use_manifest=args.use_manifest,
|
|
84
|
+
use_sequence_number=args.use_sequence_number):
|
|
85
|
+
if plugin_class:
|
|
86
|
+
plugin_record = plugin_class.FromLevelDBRecord(leveldb_record)
|
|
87
|
+
_Output(plugin_record, output=args.output)
|
|
88
|
+
else:
|
|
89
|
+
_Output(leveldb_record, output=args.output)
|
|
72
90
|
|
|
73
91
|
|
|
74
92
|
def LdbCommand(args):
|
|
75
93
|
"""The CLI for processing ldb files."""
|
|
94
|
+
if args.plugin and args.plugin == 'list':
|
|
95
|
+
for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
|
|
96
|
+
print(plugin)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if args.plugin:
|
|
100
|
+
plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
|
|
101
|
+
else:
|
|
102
|
+
plugin_class = None
|
|
103
|
+
|
|
76
104
|
ldb_file = ldb.FileReader(args.source)
|
|
77
105
|
|
|
78
106
|
if args.structure_type == 'blocks':
|
|
@@ -83,7 +111,11 @@ def LdbCommand(args):
|
|
|
83
111
|
elif args.structure_type == 'records' or not args.structure_type:
|
|
84
112
|
# Prints key value record information.
|
|
85
113
|
for key_value_record in ldb_file.GetKeyValueRecords():
|
|
86
|
-
|
|
114
|
+
if plugin_class:
|
|
115
|
+
plugin_record = plugin_class.FromKeyValueRecord(key_value_record)
|
|
116
|
+
_Output(plugin_record, output=args.output)
|
|
117
|
+
else:
|
|
118
|
+
_Output(key_value_record, output=args.output)
|
|
87
119
|
|
|
88
120
|
else:
|
|
89
121
|
print(f'{args.structure_type} is not supported for ldb files.')
|
|
@@ -91,6 +123,16 @@ def LdbCommand(args):
|
|
|
91
123
|
|
|
92
124
|
def LogCommand(args):
|
|
93
125
|
"""The CLI for processing log files."""
|
|
126
|
+
if args.plugin and args.plugin == 'list':
|
|
127
|
+
for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
|
|
128
|
+
print(plugin)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if args.plugin:
|
|
132
|
+
plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
|
|
133
|
+
else:
|
|
134
|
+
plugin_class = None
|
|
135
|
+
|
|
94
136
|
log_file = log.FileReader(args.source)
|
|
95
137
|
|
|
96
138
|
if args.structure_type == 'blocks':
|
|
@@ -112,7 +154,11 @@ def LogCommand(args):
|
|
|
112
154
|
or not args.structure_type):
|
|
113
155
|
# Prints key value record information.
|
|
114
156
|
for internal_key_record in log_file.GetParsedInternalKeys():
|
|
115
|
-
|
|
157
|
+
if plugin_class:
|
|
158
|
+
plugin_record = plugin_class.FromKeyValueRecord(internal_key_record)
|
|
159
|
+
_Output(plugin_record, output=args.output)
|
|
160
|
+
else:
|
|
161
|
+
_Output(internal_key_record, output=args.output)
|
|
116
162
|
|
|
117
163
|
else:
|
|
118
164
|
print(f'{args.structure_type} is not supported for log files.')
|
|
@@ -144,6 +190,7 @@ def DescriptorCommand(args):
|
|
|
144
190
|
else:
|
|
145
191
|
print(f'{args.structure_type} is not supported for descriptor files.')
|
|
146
192
|
|
|
193
|
+
|
|
147
194
|
def App():
|
|
148
195
|
"""The CLI app entrypoint for parsing leveldb files."""
|
|
149
196
|
parser = argparse.ArgumentParser(
|
|
@@ -160,10 +207,17 @@ def App():
|
|
|
160
207
|
required=True,
|
|
161
208
|
type=pathlib.Path,
|
|
162
209
|
help='The source leveldb directory')
|
|
163
|
-
parser_db.
|
|
210
|
+
recover_group = parser_db.add_mutually_exclusive_group()
|
|
211
|
+
recover_group.add_argument(
|
|
164
212
|
'--use_manifest',
|
|
165
213
|
action='store_true',
|
|
166
214
|
help='Use manifest file to determine active/deleted records.')
|
|
215
|
+
recover_group.add_argument(
|
|
216
|
+
'--use_sequence_number',
|
|
217
|
+
action='store_true',
|
|
218
|
+
help=(
|
|
219
|
+
'Use sequence number and file offset to determine active/deleted '
|
|
220
|
+
'records.'))
|
|
167
221
|
parser_db.add_argument(
|
|
168
222
|
'-o',
|
|
169
223
|
'--output',
|
|
@@ -173,6 +227,9 @@ def App():
|
|
|
173
227
|
'repr'],
|
|
174
228
|
default='json',
|
|
175
229
|
help='Output format. Default is json')
|
|
230
|
+
parser_db.add_argument(
|
|
231
|
+
'--plugin',
|
|
232
|
+
help='Use plugin to parse records.')
|
|
176
233
|
parser_db.set_defaults(func=DbCommand)
|
|
177
234
|
|
|
178
235
|
parser_log = subparsers.add_parser(
|
|
@@ -191,6 +248,9 @@ def App():
|
|
|
191
248
|
'repr'],
|
|
192
249
|
default='json',
|
|
193
250
|
help='Output format. Default is json')
|
|
251
|
+
parser_log.add_argument(
|
|
252
|
+
'--plugin',
|
|
253
|
+
help='Use plugin to parse records.')
|
|
194
254
|
parser_log.add_argument(
|
|
195
255
|
'-t',
|
|
196
256
|
'--structure_type',
|
|
@@ -218,6 +278,9 @@ def App():
|
|
|
218
278
|
'repr'],
|
|
219
279
|
default='json',
|
|
220
280
|
help='Output format. Default is json')
|
|
281
|
+
parser_ldb.add_argument(
|
|
282
|
+
'--plugin',
|
|
283
|
+
help='Use plugin to parse records.')
|
|
221
284
|
parser_ldb.add_argument(
|
|
222
285
|
'-t',
|
|
223
286
|
'--structure_type',
|
|
@@ -152,7 +152,7 @@ class PhysicalRecord(utils.FromDecoderMixin):
|
|
|
152
152
|
@classmethod
|
|
153
153
|
def FromDecoder(
|
|
154
154
|
cls, decoder: utils.LevelDBDecoder, base_offset: int = 0
|
|
155
|
-
) -> PhysicalRecord:
|
|
155
|
+
) -> Optional[PhysicalRecord]:
|
|
156
156
|
"""Decodes a PhysicalRecord from the current position of a LevelDBDecoder.
|
|
157
157
|
|
|
158
158
|
Args:
|
|
@@ -161,11 +161,13 @@ class PhysicalRecord(utils.FromDecoderMixin):
|
|
|
161
161
|
read from.
|
|
162
162
|
|
|
163
163
|
Returns:
|
|
164
|
-
A PhysicalRecord.
|
|
164
|
+
A PhysicalRecord or None if the parsed header is 0.
|
|
165
165
|
"""
|
|
166
166
|
offset, checksum = decoder.DecodeUint32()
|
|
167
167
|
_, length = decoder.DecodeUint16()
|
|
168
168
|
_, record_type_byte = decoder.DecodeUint8()
|
|
169
|
+
if checksum == 0 or length == 0 or record_type_byte == 0:
|
|
170
|
+
return None
|
|
169
171
|
try:
|
|
170
172
|
record_type = definitions.LogFilePhysicalRecordType(record_type_byte)
|
|
171
173
|
except ValueError as error:
|
|
@@ -206,7 +208,11 @@ class Block:
|
|
|
206
208
|
buffer_length = len(self.data)
|
|
207
209
|
|
|
208
210
|
while buffer.tell() + PhysicalRecord.PHYSICAL_HEADER_LENGTH < buffer_length:
|
|
209
|
-
|
|
211
|
+
record = PhysicalRecord.FromStream(buffer, base_offset=self.offset)
|
|
212
|
+
if record:
|
|
213
|
+
yield record
|
|
214
|
+
else:
|
|
215
|
+
return
|
|
210
216
|
|
|
211
217
|
@classmethod
|
|
212
218
|
def FromStream(cls, stream: BinaryIO) -> Optional[Block]:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""Leveldb Plugin module."""
|
|
16
|
+
|
|
17
|
+
from dfindexeddb.leveldb.plugins import chrome_notifications
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""Parser plugin for Chrome Notifications."""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# pytype: disable=import-error
|
|
25
|
+
from dfdatetime import webkit_time
|
|
26
|
+
from dfindexeddb.leveldb.plugins import notification_database_data_pb2 as \
|
|
27
|
+
notification_pb2
|
|
28
|
+
# pytype: enable=import-error
|
|
29
|
+
_has_import_dependencies = True
|
|
30
|
+
except ImportError as err:
|
|
31
|
+
_has_import_dependencies = False
|
|
32
|
+
logging.warning((
|
|
33
|
+
'Could not import dependencies for '
|
|
34
|
+
'leveldb.plugins.chrome_notifications: %s'), err)
|
|
35
|
+
|
|
36
|
+
from dfindexeddb.indexeddb.chromium import blink
|
|
37
|
+
from dfindexeddb.leveldb.plugins import interface
|
|
38
|
+
from dfindexeddb.leveldb.plugins import manager
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclasses.dataclass
|
|
42
|
+
class ChromeNotificationRecord(interface.LeveldbPlugin):
|
|
43
|
+
"""Chrome notification record."""
|
|
44
|
+
src_file: Optional[str] = None
|
|
45
|
+
offset: Optional[int] = None
|
|
46
|
+
key: Optional[str] = None
|
|
47
|
+
sequence_number: Optional[int] = None
|
|
48
|
+
type: Optional[int] = None
|
|
49
|
+
origin: Optional[str] = None
|
|
50
|
+
service_worker_registration_id: Optional[int] = None
|
|
51
|
+
notification_title: Optional[str] = None
|
|
52
|
+
notification_direction: Optional[str] = None
|
|
53
|
+
notification_lang: Optional[str] = None
|
|
54
|
+
notification_body: Optional[str] = None
|
|
55
|
+
notification_tag: Optional[str] = None
|
|
56
|
+
notification_icon: Optional[str] = None
|
|
57
|
+
notification_silent: Optional[bool] = None
|
|
58
|
+
notification_data: Optional[str] = None
|
|
59
|
+
notification_require_interaction: Optional[bool] = None
|
|
60
|
+
notification_time: Optional[str] = None
|
|
61
|
+
notification_renotify: Optional[bool] = None
|
|
62
|
+
notification_badge: Optional[str] = None
|
|
63
|
+
notification_image: Optional[str] = None
|
|
64
|
+
notification_id: Optional[str] = None
|
|
65
|
+
replaced_existing_notification: Optional[bool] = None
|
|
66
|
+
num_clicks: Optional[int] = None
|
|
67
|
+
num_action_button_clicks: Optional[int] = None
|
|
68
|
+
creation_time: Optional[str] = None
|
|
69
|
+
closed_reason: Optional[str] = None
|
|
70
|
+
has_triggered: Optional[bool] = None
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def FromKeyValueRecord(
|
|
74
|
+
cls,
|
|
75
|
+
ldb_record
|
|
76
|
+
) -> ChromeNotificationRecord:
|
|
77
|
+
record = cls()
|
|
78
|
+
record.offset = ldb_record.offset
|
|
79
|
+
record.key = ldb_record.key.decode()
|
|
80
|
+
record.sequence_number = ldb_record.sequence_number
|
|
81
|
+
record.type = ldb_record.record_type
|
|
82
|
+
|
|
83
|
+
if not ldb_record.value:
|
|
84
|
+
return record
|
|
85
|
+
|
|
86
|
+
# pylint: disable-next=no-member,line-too-long
|
|
87
|
+
notification_proto = notification_pb2.NotificationDatabaseDataProto() # pytype: disable=module-attr
|
|
88
|
+
notification_proto.ParseFromString(ldb_record.value)
|
|
89
|
+
|
|
90
|
+
record.origin = notification_proto.origin
|
|
91
|
+
record.service_worker_registration_id = (
|
|
92
|
+
notification_proto.service_worker_registration_id)
|
|
93
|
+
record.notification_title = notification_proto.notification_data.title
|
|
94
|
+
record.notification_direction = (
|
|
95
|
+
notification_proto.notification_data.direction)
|
|
96
|
+
record.notification_lang = notification_proto.notification_data.lang
|
|
97
|
+
record.notification_body = notification_proto.notification_data.body
|
|
98
|
+
record.notification_tag = notification_proto.notification_data.tag
|
|
99
|
+
record.notification_icon = notification_proto.notification_data.icon
|
|
100
|
+
record.notification_silent = notification_proto.notification_data.silent
|
|
101
|
+
record.notification_data = notification_proto.notification_data.data
|
|
102
|
+
record.notification_require_interaction = (
|
|
103
|
+
notification_proto.notification_data.require_interaction)
|
|
104
|
+
record.notification_time = webkit_time.WebKitTime(
|
|
105
|
+
timestamp=notification_proto.notification_data.timestamp
|
|
106
|
+
).CopyToDateTimeString()
|
|
107
|
+
record.notification_renotify = notification_proto.notification_data.renotify
|
|
108
|
+
record.notification_badge = notification_proto.notification_data.badge
|
|
109
|
+
record.notification_image = notification_proto.notification_data.image
|
|
110
|
+
record.notification_id = notification_proto.notification_id
|
|
111
|
+
record.replaced_existing_notification = (
|
|
112
|
+
notification_proto.replaced_existing_notification)
|
|
113
|
+
record.num_clicks = notification_proto.num_clicks
|
|
114
|
+
record.num_action_button_clicks = (
|
|
115
|
+
notification_proto.num_action_button_clicks)
|
|
116
|
+
record.creation_time = webkit_time.WebKitTime(
|
|
117
|
+
timestamp=notification_proto.creation_time_millis
|
|
118
|
+
).CopyToDateTimeString()
|
|
119
|
+
record.closed_reason = notification_proto.closed_reason
|
|
120
|
+
record.has_triggered = notification_proto.has_triggered
|
|
121
|
+
|
|
122
|
+
if not notification_proto.notification_data.data:
|
|
123
|
+
return record
|
|
124
|
+
|
|
125
|
+
notification_data = blink.V8ScriptValueDecoder(
|
|
126
|
+
raw_data=notification_proto.notification_data.data).Deserialize()
|
|
127
|
+
record.notification_data = notification_data
|
|
128
|
+
|
|
129
|
+
return record
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# check if dependencies are in existence..
|
|
133
|
+
|
|
134
|
+
if _has_import_dependencies:
|
|
135
|
+
manager.PluginManager.RegisterPlugin(ChromeNotificationRecord)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""Interface for leveldb plugins."""
|
|
16
|
+
from typing import Any, Union
|
|
17
|
+
|
|
18
|
+
from dfindexeddb.leveldb import record
|
|
19
|
+
from dfindexeddb.leveldb import ldb
|
|
20
|
+
from dfindexeddb.leveldb import log
|
|
21
|
+
|
|
22
|
+
class LeveldbPlugin:
|
|
23
|
+
"""The base leveldb plugin class."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def FromLevelDBRecord(cls,
|
|
27
|
+
ldb_record: record.LevelDBRecord) -> Any:
|
|
28
|
+
"""Parses a leveldb record."""
|
|
29
|
+
parsed_record = cls.FromKeyValueRecord(ldb_record.record)
|
|
30
|
+
ldb_record.record = parsed_record
|
|
31
|
+
return ldb_record
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def FromKeyValueRecord(
|
|
35
|
+
cls, ldb_record: Union[ldb.KeyValueRecord, log.ParsedInternalKey]) -> Any:
|
|
36
|
+
"""Parses a leveldb key value record."""
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""Leveldb plugin manager."""
|
|
16
|
+
from typing import Type
|
|
17
|
+
|
|
18
|
+
from dfindexeddb.leveldb.plugins import interface
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LeveldbPluginManager:
|
|
22
|
+
"""The leveldb plugin manager."""
|
|
23
|
+
|
|
24
|
+
_class_registry = {}
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def GetPlugins(cls):
|
|
28
|
+
"""Retrieves the registered leveldb plugins.
|
|
29
|
+
|
|
30
|
+
Yields:
|
|
31
|
+
tuple: containing:
|
|
32
|
+
str: the name of the leveldb plugin.
|
|
33
|
+
class: the plugin class.
|
|
34
|
+
"""
|
|
35
|
+
yield from cls._class_registry.items()
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def GetPlugin(cls, plugin_name: str) -> interface.LeveldbPlugin:
|
|
39
|
+
"""Retrieves a class object of a specific plugin.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
plugin_name: name of the plugin.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
the LeveldbPlugin class.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
KeyError: if the plugin is not found/registered in the manager.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
return cls._class_registry[plugin_name]
|
|
52
|
+
except KeyError:
|
|
53
|
+
raise KeyError(f'Plugin not found: {plugin_name}')
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def RegisterPlugin(cls, plugin_class: Type[interface.LeveldbPlugin]):
|
|
57
|
+
"""Registers a leveldb plugin.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
plugin_class (class): the plugin class to register.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
KeyError: if class is already set for the corresponding name.
|
|
64
|
+
"""
|
|
65
|
+
plugin_name = plugin_class.__name__
|
|
66
|
+
if plugin_name in cls._class_registry:
|
|
67
|
+
raise KeyError(f'Plugin already registered {plugin_name}')
|
|
68
|
+
cls._class_registry[plugin_name] = plugin_class
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def ClearPlugins(cls):
|
|
72
|
+
"""Clears all plugin registrations."""
|
|
73
|
+
cls._class_registry = {}
|
|
74
|
+
|
|
75
|
+
PluginManager = LeveldbPluginManager()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: notification_database_data.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.1
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# pylint: skip-file
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n notification_database_data.proto\"\xff\t\n\x1dNotificationDatabaseDataProto\x12\"\n\x1apersistent_notification_id\x18\x01 \x01(\x03\x12\x17\n\x0fnotification_id\x18\x05 \x01(\t\x12\x0e\n\x06origin\x18\x02 \x01(\t\x12&\n\x1eservice_worker_registration_id\x18\x03 \x01(\x03\x12&\n\x1ereplaced_existing_notification\x18\x06 \x01(\x08\x12\x12\n\nnum_clicks\x18\x07 \x01(\x05\x12 \n\x18num_action_button_clicks\x18\x08 \x01(\x05\x12\x1c\n\x14\x63reation_time_millis\x18\t \x01(\x03\x12%\n\x1dtime_until_first_click_millis\x18\n \x01(\x03\x12$\n\x1ctime_until_last_click_millis\x18\x0b \x01(\x03\x12\x1f\n\x17time_until_close_millis\x18\x0c \x01(\x03\x12\x42\n\rclosed_reason\x18\r \x01(\x0e\x32+.NotificationDatabaseDataProto.ClosedReason\x12J\n\x11notification_data\x18\x04 \x01(\x0b\x32/.NotificationDatabaseDataProto.NotificationData\x12\x15\n\rhas_triggered\x18\x0e \x01(\x08\x1a\xba\x01\n\x12NotificationAction\x12\x0e\n\x06\x61\x63tion\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0c\n\x04icon\x18\x03 \x01(\t\x12\x44\n\x04type\x18\x04 \x01(\x0e\x32\x36.NotificationDatabaseDataProto.NotificationAction.Type\x12\x13\n\x0bplaceholder\x18\x05 \x01(\t\"\x1c\n\x04Type\x12\n\n\x06\x42UTTON\x10\x00\x12\x08\n\x04TEXT\x10\x01\x1a\xe4\x03\n\x10NotificationData\x12\r\n\x05title\x18\x01 \x01(\t\x12L\n\tdirection\x18\x02 \x01(\x0e\x32\x39.NotificationDatabaseDataProto.NotificationData.Direction\x12\x0c\n\x04lang\x18\x03 \x01(\t\x12\x0c\n\x04\x62ody\x18\x04 \x01(\t\x12\x0b\n\x03tag\x18\x05 \x01(\t\x12\r\n\x05image\x18\x0f \x01(\t\x12\x0c\n\x04icon\x18\x06 \x01(\t\x12\r\n\x05\x62\x61\x64ge\x18\x0e \x01(\t\x12\x1d\n\x11vibration_pattern\x18\t \x03(\x05\x42\x02\x10\x01\x12\x11\n\ttimestamp\x18\x0c \x01(\x03\x12\x10\n\x08renotify\x18\r \x01(\x08\x12\x0e\n\x06silent\x18\x07 \x01(\x08\x12\x1b\n\x13require_interaction\x18\x0b \x01(\x08\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\x12\x42\n\x07\x61\x63tions\x18\n \x03(\x0b\x32\x31.NotificationDatabaseDataProto.NotificationAction\x12\x1e\n\x16show_trigger_timestamp\x18\x10 \x01(\x03\";\n\tDirection\x12\x11\n\rLEFT_TO_RIGHT\x10\x00\x12\x11\n\rRIGHT_TO_LEFT\x10\x01\x12\x08\n\x04\x41UTO\x10\x02\"4\n\x0c\x43losedReason\x12\x08\n\x04USER\x10\x00\x12\r\n\tDEVELOPER\x10\x01\x12\x0b\n\x07UNKNOWN\x10\x02')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'notification_database_data_pb2', _globals)
|
|
22
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
|
+
DESCRIPTOR._options = None
|
|
24
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA'].fields_by_name['vibration_pattern']._options = None
|
|
25
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA'].fields_by_name['vibration_pattern']._serialized_options = b'\020\001'
|
|
26
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO']._serialized_start=37
|
|
27
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO']._serialized_end=1316
|
|
28
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION']._serialized_start=589
|
|
29
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION']._serialized_end=775
|
|
30
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION_TYPE']._serialized_start=747
|
|
31
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION_TYPE']._serialized_end=775
|
|
32
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA']._serialized_start=778
|
|
33
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA']._serialized_end=1262
|
|
34
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA_DIRECTION']._serialized_start=1203
|
|
35
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA_DIRECTION']._serialized_end=1262
|
|
36
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_CLOSEDREASON']._serialized_start=1264
|
|
37
|
+
_globals['_NOTIFICATIONDATABASEDATAPROTO_CLOSEDREASON']._serialized_end=1316
|
|
38
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""A module for records from LevelDB files."""
|
|
16
16
|
from __future__ import annotations
|
|
17
|
+
from collections import defaultdict
|
|
17
18
|
import dataclasses
|
|
18
19
|
import pathlib
|
|
19
20
|
import re
|
|
@@ -297,9 +298,38 @@ class FolderReader:
|
|
|
297
298
|
record.recovered = True
|
|
298
299
|
yield record
|
|
299
300
|
|
|
301
|
+
def _RecordsBySequenceNumber(self) -> Generator[LevelDBRecord, None, None]:
|
|
302
|
+
"""Yields LevelDBRecords using the sequence number and file offset.
|
|
303
|
+
|
|
304
|
+
Yields:
|
|
305
|
+
LevelDBRecords.
|
|
306
|
+
"""
|
|
307
|
+
unsorted_records = defaultdict(list)
|
|
308
|
+
|
|
309
|
+
for filename in self.foldername.iterdir():
|
|
310
|
+
for leveldb_record in LevelDBRecord.FromFile(filename):
|
|
311
|
+
if leveldb_record:
|
|
312
|
+
unsorted_records[leveldb_record.record.key].append(leveldb_record)
|
|
313
|
+
for _, unsorted_records in unsorted_records.items():
|
|
314
|
+
num_unsorted_records = len(unsorted_records)
|
|
315
|
+
if num_unsorted_records == 1:
|
|
316
|
+
unsorted_records[0].recovered = False
|
|
317
|
+
yield unsorted_records[0]
|
|
318
|
+
else:
|
|
319
|
+
for i, record in enumerate(sorted(
|
|
320
|
+
unsorted_records, key=lambda x: (
|
|
321
|
+
x.record.sequence_number, x.record.offset)),
|
|
322
|
+
start=1):
|
|
323
|
+
if i == num_unsorted_records:
|
|
324
|
+
record.recovered = False
|
|
325
|
+
else:
|
|
326
|
+
record.recovered = True
|
|
327
|
+
yield record
|
|
328
|
+
|
|
300
329
|
def GetRecords(
|
|
301
330
|
self,
|
|
302
|
-
use_manifest: bool = False
|
|
331
|
+
use_manifest: bool = False,
|
|
332
|
+
use_sequence_number: bool = False
|
|
303
333
|
) -> Generator[LevelDBRecord, None, None]:
|
|
304
334
|
"""Yield LevelDBRecords.
|
|
305
335
|
|
|
@@ -312,6 +342,8 @@ class FolderReader:
|
|
|
312
342
|
"""
|
|
313
343
|
if use_manifest:
|
|
314
344
|
yield from self._RecordsByManifest()
|
|
345
|
+
elif use_sequence_number:
|
|
346
|
+
yield from self._RecordsBySequenceNumber()
|
|
315
347
|
else:
|
|
316
348
|
for filename in self.foldername.iterdir():
|
|
317
349
|
yield from LevelDBRecord.FromFile(filename)
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Utilities for dfindexeddb."""
|
|
16
16
|
from __future__ import annotations
|
|
17
|
+
import copy
|
|
18
|
+
import dataclasses
|
|
17
19
|
import io
|
|
18
20
|
import os
|
|
19
21
|
import struct
|
|
@@ -259,3 +261,35 @@ class FromDecoderMixin:
|
|
|
259
261
|
"""
|
|
260
262
|
stream = io.BytesIO(raw_data)
|
|
261
263
|
return cls.FromStream(stream=stream, base_offset=base_offset)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def asdict(obj, *, dict_factory=dict): # pylint: disable=invalid-name
|
|
267
|
+
"""Custom implementation of the asdict dataclasses method to include the
|
|
268
|
+
class name under the __type__ attribute name.
|
|
269
|
+
"""
|
|
270
|
+
if not dataclasses.is_dataclass(obj):
|
|
271
|
+
raise TypeError("asdict() should be called on dataclass instances")
|
|
272
|
+
return _asdict_inner(obj, dict_factory)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _asdict_inner(obj, dict_factory):
|
|
276
|
+
"""Custom implementation of the _asdict_inner dataclasses method."""
|
|
277
|
+
if dataclasses.is_dataclass(obj):
|
|
278
|
+
result = [('__type__', obj.__class__.__name__)]
|
|
279
|
+
for f in dataclasses.fields(obj):
|
|
280
|
+
value = _asdict_inner(getattr(obj, f.name), dict_factory)
|
|
281
|
+
result.append((f.name, value))
|
|
282
|
+
return dict_factory(result)
|
|
283
|
+
|
|
284
|
+
if isinstance(obj, tuple) and hasattr(obj, '_fields'):
|
|
285
|
+
return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj])
|
|
286
|
+
|
|
287
|
+
if isinstance(obj, (list, tuple)):
|
|
288
|
+
return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
|
|
289
|
+
|
|
290
|
+
if isinstance(obj, dict):
|
|
291
|
+
return type(obj)((_asdict_inner(k, dict_factory),
|
|
292
|
+
_asdict_inner(v, dict_factory))
|
|
293
|
+
for k, v in obj.items())
|
|
294
|
+
|
|
295
|
+
return copy.deepcopy(obj)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dfindexeddb
|
|
3
|
-
Version:
|
|
3
|
+
Version: 20240519
|
|
4
4
|
Summary: dfindexeddb is an experimental Python tool for performing digital forensic analysis of IndexedDB and leveldb files.
|
|
5
5
|
Author-email: Syd Pleno <sydp@google.com>
|
|
6
6
|
Maintainer-email: dfIndexeddb Developers <dfindexeddb-dev@googlegroups.com>
|
|
@@ -219,6 +219,9 @@ License-File: LICENSE
|
|
|
219
219
|
License-File: AUTHORS
|
|
220
220
|
Requires-Dist: python-snappy==0.6.1
|
|
221
221
|
Requires-Dist: zstd==1.5.5.1
|
|
222
|
+
Provides-Extra: plugins
|
|
223
|
+
Requires-Dist: protobuf; extra == "plugins"
|
|
224
|
+
Requires-Dist: dfdatetime; extra == "plugins"
|
|
222
225
|
|
|
223
226
|
# dfIndexeddb
|
|
224
227
|
|
|
@@ -255,6 +258,12 @@ include:
|
|
|
255
258
|
$ pip install dfindexeddb
|
|
256
259
|
```
|
|
257
260
|
|
|
261
|
+
To also install the dependencies for leveldb/indexeddb plugins, run
|
|
262
|
+
```
|
|
263
|
+
$ pip install 'dfindexeddb[plugins]'
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
|
|
258
267
|
## Installation from source
|
|
259
268
|
|
|
260
269
|
1. [Linux] Install the snappy compression development package
|
|
@@ -273,6 +282,11 @@ include:
|
|
|
273
282
|
$ pip install .
|
|
274
283
|
```
|
|
275
284
|
|
|
285
|
+
To also install the dependencies for leveldb/indexeddb plugins, run
|
|
286
|
+
```
|
|
287
|
+
$ pip install '.[plugins]'
|
|
288
|
+
```
|
|
289
|
+
|
|
276
290
|
## Usage
|
|
277
291
|
|
|
278
292
|
Two CLI tools for parsing IndexedDB/LevelDB files are available after
|
|
@@ -359,7 +373,15 @@ options:
|
|
|
359
373
|
To parse records from a LevelDB folder, use the following command:
|
|
360
374
|
|
|
361
375
|
```
|
|
362
|
-
|
|
376
|
+
dfleveldb db -s SOURCE
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
To parse records from a LevelDB folder, and use the sequence number to
|
|
380
|
+
determine recovered records and output as JSON, use the
|
|
381
|
+
following command:
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
dfleveldb db -s SOURCE --use_sequence_number
|
|
363
385
|
```
|
|
364
386
|
|
|
365
387
|
To parse blocks / physical records/ write batches / internal key records from a
|
|
@@ -383,15 +405,14 @@ following command:
|
|
|
383
405
|
|
|
384
406
|
```
|
|
385
407
|
$ dfleveldb descriptor -s SOURCE [-o {json,jsonl,repr}] [-t {blocks,physical_records,versionedit} | -v]
|
|
386
|
-
|
|
387
|
-
options:
|
|
388
|
-
-h, --help show this help message and exit
|
|
389
|
-
-s SOURCE, --source SOURCE
|
|
390
|
-
The source leveldb file
|
|
391
|
-
-o {json,jsonl,repr}, --output {json,jsonl,repr}
|
|
392
|
-
Output format. Default is json
|
|
393
|
-
-t {blocks,physical_records,versionedit}, --structure_type {blocks,physical_records,versionedit}
|
|
394
|
-
Parses the specified structure. Default is versionedit.
|
|
395
|
-
-v, --version_history
|
|
396
|
-
Parses the leveldb version history.
|
|
397
408
|
```
|
|
409
|
+
|
|
410
|
+
#### Plugins
|
|
411
|
+
|
|
412
|
+
To apply a plugin parser for a leveldb file/folder, add the
|
|
413
|
+
`--plugin [Plugin Name]` argument. Currently, there is support for the
|
|
414
|
+
following artifacts:
|
|
415
|
+
|
|
416
|
+
| Plugin Name | Artifact Name |
|
|
417
|
+
| -------- | ------- |
|
|
418
|
+
| `ChromeNotificationRecord` | Chrome/Chromium Notifications |
|
|
@@ -33,4 +33,9 @@ dfindexeddb/leveldb/descriptor.py
|
|
|
33
33
|
dfindexeddb/leveldb/ldb.py
|
|
34
34
|
dfindexeddb/leveldb/log.py
|
|
35
35
|
dfindexeddb/leveldb/record.py
|
|
36
|
-
dfindexeddb/leveldb/utils.py
|
|
36
|
+
dfindexeddb/leveldb/utils.py
|
|
37
|
+
dfindexeddb/leveldb/plugins/__init__.py
|
|
38
|
+
dfindexeddb/leveldb/plugins/chrome_notifications.py
|
|
39
|
+
dfindexeddb/leveldb/plugins/interface.py
|
|
40
|
+
dfindexeddb/leveldb/plugins/manager.py
|
|
41
|
+
dfindexeddb/leveldb/plugins/notification_database_data_pb2.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dfindexeddb"
|
|
7
|
-
version = "
|
|
7
|
+
version = "20240519"
|
|
8
8
|
requires-python = ">=3.8"
|
|
9
9
|
description = "dfindexeddb is an experimental Python tool for performing digital forensic analysis of IndexedDB and leveldb files."
|
|
10
10
|
license = {file = "LICENSE"}
|
|
@@ -22,18 +22,22 @@ classifiers = [
|
|
|
22
22
|
'Programming Language :: Python',
|
|
23
23
|
]
|
|
24
24
|
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
plugins = ["protobuf", "dfdatetime"]
|
|
27
|
+
|
|
25
28
|
[project.scripts]
|
|
26
29
|
dfindexeddb = "dfindexeddb.indexeddb.cli:App"
|
|
27
30
|
dfleveldb = "dfindexeddb.leveldb.cli:App"
|
|
28
31
|
|
|
29
32
|
[tool.setuptools]
|
|
30
33
|
packages = [
|
|
31
|
-
"dfindexeddb",
|
|
32
|
-
"dfindexeddb.indexeddb",
|
|
34
|
+
"dfindexeddb",
|
|
35
|
+
"dfindexeddb.indexeddb",
|
|
33
36
|
"dfindexeddb.indexeddb.chromium",
|
|
34
37
|
"dfindexeddb.indexeddb.firefox",
|
|
35
|
-
"dfindexeddb.indexeddb.safari",
|
|
38
|
+
"dfindexeddb.indexeddb.safari",
|
|
36
39
|
"dfindexeddb.leveldb",
|
|
40
|
+
"dfindexeddb.leveldb.plugins",
|
|
37
41
|
]
|
|
38
42
|
|
|
39
43
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|