dissect.hypervisor 3.17.dev1__tar.gz → 3.17.dev3__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.
- dissect_hypervisor-3.17.dev3/.git-blame-ignore-revs +6 -0
- {dissect_hypervisor-3.17.dev1/dissect.hypervisor.egg-info → dissect_hypervisor-3.17.dev3}/PKG-INFO +2 -2
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/c_hyperv.py +2 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/hyperv.py +13 -15
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/ovf.py +9 -5
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/pvs.py +8 -3
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/vbox.py +8 -3
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/vmx.py +57 -47
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_hdd.py +1 -1
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_qcow2.py +4 -1
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_vdi.py +2 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_vhd.py +2 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_vhdx.py +2 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_vmdk.py +2 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/hdd.py +13 -13
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/qcow2.py +82 -72
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/vdi.py +5 -2
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/vhd.py +16 -13
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/vhdx.py +23 -17
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/vmdk.py +30 -30
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/tools/envelope.py +5 -1
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/util/envelope.py +12 -8
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/util/vmtar.py +15 -8
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3/dissect.hypervisor.egg-info}/PKG-INFO +2 -2
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/SOURCES.txt +1 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/pyproject.toml +48 -5
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/conftest.py +16 -11
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_envelope.py +6 -3
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_hdd.py +3 -1
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_hyperv.py +6 -2
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_ovf.py +3 -1
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_pvs.py +3 -1
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vbox.py +2 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vhd.py +8 -4
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vhdx.py +18 -15
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vmdk.py +8 -4
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vmtar.py +5 -1
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/test_vmx.py +6 -2
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tox.ini +4 -17
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/COPYRIGHT +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/LICENSE +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/MANIFEST.in +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/README.md +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/__init__.py +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/__init__.py +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/__init__.py +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/exceptions.py +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/tools/__init__.py +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/util/__init__.py +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/dependency_links.txt +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/entry_points.txt +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/requires.txt +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect.hypervisor.egg-info/top_level.txt +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/setup.cfg +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/__init__.py +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/differencing.avhdx.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/dynamic.vhd.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/dynamic.vhdx.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/encrypted.vmx +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/encryption.info +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/expanding.hdd/DiskDescriptor.xml +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/expanding.hdd/expanding.hdd +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/expanding.hdd/expanding.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/fixed.vhd.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/fixed.vhdx.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/local.tgz.ve +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/plain.hdd/DiskDescriptor.xml +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/plain.hdd/plain.hdd +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/plain.hdd/plain.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/sesparse.vmdk.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/DiskDescriptor.xml +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.1.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.2.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.3.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.4.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/split.hdd/split.hdd.5.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/test.VMRS +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/test.vgz +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/data/test.vmcx +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/docs/Makefile +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/docs/conf.py +0 -0
- {dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/tests/docs/index.rst +0 -0
{dissect_hypervisor-3.17.dev1/dissect.hypervisor.egg-info → dissect_hypervisor-3.17.dev3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: dissect.hypervisor
|
|
3
|
-
Version: 3.17.
|
|
3
|
+
Version: 3.17.dev3
|
|
4
4
|
Summary: A Dissect module implementing parsers for various hypervisor disk, backup and configuration files
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License: Affero General Public License v3
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import struct
|
|
6
|
-
from
|
|
7
|
-
from typing import BinaryIO, Optional, Union
|
|
6
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
8
7
|
|
|
9
8
|
from dissect.util.stream import RangeStream
|
|
10
9
|
|
|
@@ -16,6 +15,9 @@ from dissect.hypervisor.descriptor.c_hyperv import (
|
|
|
16
15
|
)
|
|
17
16
|
from dissect.hypervisor.exceptions import InvalidSignature
|
|
18
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import ItemsView, KeysView, ValuesView
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class HyperVFile:
|
|
21
23
|
"""HyperVFile implementation.
|
|
@@ -278,7 +280,7 @@ class HyperVStorageKeyTableEntry:
|
|
|
278
280
|
return f"<HyperVStorageKeyTableEntry type={self.type} size={self.size}>"
|
|
279
281
|
|
|
280
282
|
@property
|
|
281
|
-
def parent(self) ->
|
|
283
|
+
def parent(self) -> HyperVStorageKeyTableEntry | None:
|
|
282
284
|
"""Return the entry parent, if there is any.
|
|
283
285
|
|
|
284
286
|
Requires that all key tables are loaded.
|
|
@@ -333,8 +335,8 @@ class HyperVStorageKeyTableEntry:
|
|
|
333
335
|
file_object = self.get_file_object()
|
|
334
336
|
# This memoryview has no purpose, only do it so the return value type is consistent
|
|
335
337
|
return memoryview(file_object.read(size))
|
|
336
|
-
|
|
337
|
-
|
|
338
|
+
|
|
339
|
+
return self.raw[self.header.data_offset :]
|
|
338
340
|
|
|
339
341
|
@property
|
|
340
342
|
def key(self) -> str:
|
|
@@ -343,7 +345,7 @@ class HyperVStorageKeyTableEntry:
|
|
|
343
345
|
return self.raw.tobytes()[: self.header.data_offset - 1].decode("utf-8")
|
|
344
346
|
|
|
345
347
|
@property
|
|
346
|
-
def value(self) ->
|
|
348
|
+
def value(self) -> int | bytes | str:
|
|
347
349
|
"""Return a Python native value for this entry."""
|
|
348
350
|
data = self.data
|
|
349
351
|
|
|
@@ -369,6 +371,8 @@ class HyperVStorageKeyTableEntry:
|
|
|
369
371
|
if self.type == KeyDataType.Bool:
|
|
370
372
|
return struct.unpack("<I", data[:4])[0] != 0
|
|
371
373
|
|
|
374
|
+
raise TypeError(f"Unknown data type: {self.type}")
|
|
375
|
+
|
|
372
376
|
@property
|
|
373
377
|
def data_size(self) -> int:
|
|
374
378
|
"""Return the total amount of data bytes, including the key name.
|
|
@@ -427,10 +431,7 @@ class HyperVStorageKeyTableEntry:
|
|
|
427
431
|
|
|
428
432
|
obj = {}
|
|
429
433
|
for key, child in self.children.items():
|
|
430
|
-
if child.type == KeyDataType.Node
|
|
431
|
-
value = child.as_dict()
|
|
432
|
-
else:
|
|
433
|
-
value = child.value
|
|
434
|
+
value = child.as_dict() if child.type == KeyDataType.Node else child.value
|
|
434
435
|
|
|
435
436
|
obj[key] = value
|
|
436
437
|
|
|
@@ -466,13 +467,10 @@ class HyperVStorageFileObject:
|
|
|
466
467
|
if n is not None and n < -1:
|
|
467
468
|
raise ValueError("invalid number of bytes to read")
|
|
468
469
|
|
|
469
|
-
if n == -1
|
|
470
|
-
read_length = self.size
|
|
471
|
-
else:
|
|
472
|
-
read_length = min(n, self.size)
|
|
470
|
+
read_length = self.size if n == -1 else min(n, self.size)
|
|
473
471
|
|
|
474
472
|
self.file.fh.seek(self.offset)
|
|
475
473
|
return self.file.fh.read(read_length)
|
|
476
474
|
|
|
477
|
-
def open(self, size:
|
|
475
|
+
def open(self, size: int | None = None) -> BinaryIO:
|
|
478
476
|
return RangeStream(self.file.fh, self.offset, size or self.size)
|
{dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/ovf.py
RENAMED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Final, TextIO
|
|
3
4
|
|
|
4
5
|
from defusedxml import ElementTree
|
|
5
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from xml.etree.ElementTree import Element
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
class OVF:
|
|
8
|
-
NS = {
|
|
13
|
+
NS: Final[dict[str, str]] = {
|
|
9
14
|
"ovf": "http://schemas.dmtf.org/ovf/envelope/1",
|
|
10
15
|
"rasd": "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData",
|
|
11
16
|
}
|
|
@@ -34,8 +39,7 @@ class OVF:
|
|
|
34
39
|
for disk in self.xml.findall(self.DISK_DRIVE_XPATH, self.NS):
|
|
35
40
|
resource = disk.find("{{{rasd}}}HostResource".format(**self.NS))
|
|
36
41
|
xpath = resource.text
|
|
37
|
-
|
|
38
|
-
xpath = xpath[4:]
|
|
42
|
+
xpath = xpath.removeprefix("ovf:")
|
|
39
43
|
|
|
40
44
|
if xpath.startswith("/disk/"):
|
|
41
45
|
disk_ref = xpath.split("/")[-1]
|
{dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/pvs.py
RENAMED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, TextIO
|
|
3
4
|
|
|
4
5
|
from defusedxml import ElementTree
|
|
5
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from xml.etree.ElementTree import Element
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
class PVS:
|
|
8
13
|
"""Parallels VM settings file.
|
|
@@ -11,7 +16,7 @@ class PVS:
|
|
|
11
16
|
fh: The file-like object to a PVS file.
|
|
12
17
|
"""
|
|
13
18
|
|
|
14
|
-
def __init__(self, fh:
|
|
19
|
+
def __init__(self, fh: TextIO):
|
|
15
20
|
self._xml: Element = ElementTree.fromstring(fh.read())
|
|
16
21
|
|
|
17
22
|
def disks(self) -> Iterator[str]:
|
{dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/vbox.py
RENAMED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, TextIO
|
|
3
4
|
|
|
4
5
|
from defusedxml import ElementTree
|
|
5
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from xml.etree.ElementTree import Element
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
class VBox:
|
|
8
13
|
VBOX_XML_NAMESPACE = "{http://www.virtualbox.org/}"
|
|
9
14
|
|
|
10
|
-
def __init__(self, fh:
|
|
15
|
+
def __init__(self, fh: TextIO):
|
|
11
16
|
self._xml: Element = ElementTree.fromstring(fh.read())
|
|
12
17
|
|
|
13
18
|
def disks(self) -> Iterator[str]:
|
{dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/descriptor/vmx.py
RENAMED
|
@@ -7,11 +7,10 @@ import base64
|
|
|
7
7
|
import hashlib
|
|
8
8
|
import hmac
|
|
9
9
|
import re
|
|
10
|
-
from typing import Dict, List
|
|
11
10
|
from urllib.parse import unquote
|
|
12
11
|
|
|
13
12
|
try:
|
|
14
|
-
import _pystandalone
|
|
13
|
+
import _pystandalone # type: ignore
|
|
15
14
|
|
|
16
15
|
HAS_PYSTANDALONE = True
|
|
17
16
|
except ImportError:
|
|
@@ -44,8 +43,8 @@ PASS2KEY_MAP = {
|
|
|
44
43
|
|
|
45
44
|
|
|
46
45
|
class VMX:
|
|
47
|
-
def __init__(self,
|
|
48
|
-
self.attr =
|
|
46
|
+
def __init__(self, attr: dict[str, str]):
|
|
47
|
+
self.attr = attr
|
|
49
48
|
|
|
50
49
|
@classmethod
|
|
51
50
|
def parse(cls, string: str) -> VMX:
|
|
@@ -56,19 +55,28 @@ class VMX:
|
|
|
56
55
|
def encrypted(self) -> bool:
|
|
57
56
|
"""Return whether this VMX is encrypted.
|
|
58
57
|
|
|
59
|
-
Encrypted VMXs will have both a
|
|
60
|
-
The
|
|
58
|
+
Encrypted VMXs will have both a ``encryption.keySafe`` and ``encryption.data`` value.
|
|
59
|
+
The ``encryption.keySafe`` is a string encoded ``KeySafe``, which is made up of key locators.
|
|
61
60
|
|
|
62
61
|
For example:
|
|
62
|
+
|
|
63
|
+
.. code-block:: none
|
|
64
|
+
|
|
63
65
|
vmware:key/list/(pair/(phrase/phrase_id/phrase_content,hmac,data),pair/(.../...,...,...))
|
|
64
66
|
|
|
65
|
-
A KeySafe must be a list of Pairs
|
|
67
|
+
A ``KeySafe`` must be a list of ``Pairs``. Each ``Pair`` has a wrapped key, an HMAC type and encrypted data.
|
|
66
68
|
It's implementation specific how to unwrap a key. E.g. a phrase is just PBKDF2. The unwrapped key
|
|
67
|
-
can be used to unlock the encrypted Pair data. This will contain the final encryption key to decrypt
|
|
68
|
-
the data in
|
|
69
|
+
can be used to unlock the encrypted ``Pair`` data. This will contain the final encryption key to decrypt
|
|
70
|
+
the data in ``encryption.data``.
|
|
71
|
+
|
|
72
|
+
So, in summary, to unseal a ``KeySafe``:
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
- Parse ``KeySafe``
|
|
75
|
+
- Iterate pairs
|
|
76
|
+
- Unlock ``Pair``
|
|
77
|
+
- Unwrap key (e.g. ``Phrase``)
|
|
78
|
+
- Decrypt ``Pair`` data
|
|
79
|
+
- Parse dictionary
|
|
72
80
|
|
|
73
81
|
The terms for unwrapping, unlocking and unsealing are taken from VMware.
|
|
74
82
|
"""
|
|
@@ -77,7 +85,7 @@ class VMX:
|
|
|
77
85
|
def unlock_with_phrase(self, passphrase: str) -> None:
|
|
78
86
|
"""Unlock this VMX in-place with a passphrase if it's encrypted.
|
|
79
87
|
|
|
80
|
-
This will load the KeySafe from the current dictionary and attempt to recover the encryption key
|
|
88
|
+
This will load the ``KeySafe`` from the current dictionary and attempt to recover the encryption key
|
|
81
89
|
from it using the given passphrase. This key is used to decrypt the encrypted VMX data.
|
|
82
90
|
|
|
83
91
|
The dictionary is updated in-place with the encrypted VMX data.
|
|
@@ -92,7 +100,7 @@ class VMX:
|
|
|
92
100
|
decrypted = _decrypt_hmac(key, encrypted, mac)
|
|
93
101
|
self.attr.update(**_parse_dictionary(decrypted.decode()))
|
|
94
102
|
|
|
95
|
-
def disks(self) ->
|
|
103
|
+
def disks(self) -> list[str]:
|
|
96
104
|
"""Return a list of paths to disk files"""
|
|
97
105
|
dev_classes = ("scsi", "sata", "ide", "nvme")
|
|
98
106
|
devices = {}
|
|
@@ -129,7 +137,7 @@ class VMX:
|
|
|
129
137
|
return sorted(disk_files)
|
|
130
138
|
|
|
131
139
|
|
|
132
|
-
def _parse_dictionary(string: str) ->
|
|
140
|
+
def _parse_dictionary(string: str) -> dict[str, str]:
|
|
133
141
|
"""Parse a VMX dictionary."""
|
|
134
142
|
dictionary = {}
|
|
135
143
|
|
|
@@ -148,11 +156,11 @@ def _parse_dictionary(string: str) -> Dict[str, str]:
|
|
|
148
156
|
|
|
149
157
|
|
|
150
158
|
class KeySafe:
|
|
151
|
-
def __init__(self, locators:
|
|
159
|
+
def __init__(self, locators: list[Pair]):
|
|
152
160
|
self.locators = locators
|
|
153
161
|
|
|
154
162
|
def unseal_with_phrase(self, passphrase: str) -> bytes:
|
|
155
|
-
"""Unseal this KeySafe with a passphrase and return the decrypted key."""
|
|
163
|
+
"""Unseal this ``KeySafe`` with a passphrase and return the decrypted key."""
|
|
156
164
|
for locator in self.locators:
|
|
157
165
|
if not locator.has_phrase():
|
|
158
166
|
continue
|
|
@@ -170,7 +178,7 @@ class KeySafe:
|
|
|
170
178
|
|
|
171
179
|
@classmethod
|
|
172
180
|
def from_text(cls, text: str) -> KeySafe:
|
|
173
|
-
"""Parse a KeySafe from a string."""
|
|
181
|
+
"""Parse a ``KeySafe`` from a string."""
|
|
174
182
|
|
|
175
183
|
# Key safes are a list of key locators. It's a key locator string with a specific prefix
|
|
176
184
|
identifier, _, remainder = text.partition("/")
|
|
@@ -179,41 +187,41 @@ class KeySafe:
|
|
|
179
187
|
|
|
180
188
|
# First part must be a list of pairs
|
|
181
189
|
locators = _parse_key_locator(remainder)
|
|
182
|
-
if not isinstance(locators, list) and not all(
|
|
190
|
+
if not isinstance(locators, list) and not all(isinstance(member, Pair) for member in locators):
|
|
183
191
|
raise ValueError("Invalid KeySafe string, not a list of pairs")
|
|
184
192
|
|
|
185
193
|
return KeySafe(locators)
|
|
186
194
|
|
|
187
195
|
|
|
188
196
|
class Pair:
|
|
189
|
-
def __init__(self, wrapped_key, mac: str, data: bytes):
|
|
197
|
+
def __init__(self, wrapped_key: Phrase, mac: str, data: bytes):
|
|
190
198
|
self.wrapped_key = wrapped_key
|
|
191
199
|
self.mac = mac
|
|
192
200
|
self.data = data
|
|
193
201
|
|
|
194
|
-
def __repr__(self):
|
|
202
|
+
def __repr__(self) -> str:
|
|
195
203
|
return f"<Pair wrapped_key={self.wrapped_key} mac={self.mac}>"
|
|
196
204
|
|
|
197
205
|
def has_phrase(self) -> bool:
|
|
198
|
-
"""Return whether this Pair is a Phrase pair."""
|
|
206
|
+
"""Return whether this ``Pair`` is a ``Phrase`` pair."""
|
|
199
207
|
return isinstance(self.wrapped_key, Phrase)
|
|
200
208
|
|
|
201
209
|
def _unlock(self, key: bytes) -> bytes:
|
|
202
|
-
"""Decrypt the data in this Pair
|
|
210
|
+
"""Decrypt the data in this ``Pair``."""
|
|
203
211
|
return _decrypt_hmac(key, self.data, self.mac)
|
|
204
212
|
|
|
205
213
|
def unlock(self, *args, **kwargs) -> bytes:
|
|
206
|
-
"""Helper method to unlock this Pair for various wrapped keys.
|
|
214
|
+
"""Helper method to unlock this ``Pair`` for various wrapped keys.
|
|
207
215
|
|
|
208
216
|
Currently only supports `Phrase`.
|
|
209
217
|
"""
|
|
210
218
|
if self.has_phrase():
|
|
211
219
|
return self.unlock_with_phrase(*args, **kwargs)
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
|
|
221
|
+
raise TypeError(f"Unable to unlock {self.wrapped_key}")
|
|
214
222
|
|
|
215
223
|
def unlock_with_phrase(self, passphrase: str) -> bytes:
|
|
216
|
-
"""Unlock this Pair with a passphrase and return the decrypted data."""
|
|
224
|
+
"""Unlock this ``Pair`` with a passphrase and return the decrypted data."""
|
|
217
225
|
if not self.has_phrase():
|
|
218
226
|
raise TypeError("Pair doesn't have a phrase protected key")
|
|
219
227
|
|
|
@@ -229,13 +237,13 @@ class Phrase:
|
|
|
229
237
|
self.rounds = rounds
|
|
230
238
|
self.salt = salt
|
|
231
239
|
|
|
232
|
-
def __repr__(self):
|
|
240
|
+
def __repr__(self) -> str:
|
|
233
241
|
return f"<Phrase id={self.id} pass2key={self.pass2key} cipher={self.cipher} rounds={self.rounds}>"
|
|
234
242
|
|
|
235
243
|
def unwrap(self, passphrase: str) -> bytes:
|
|
236
244
|
"""Unwrap/generate the encryption key for a given passphrase.
|
|
237
245
|
|
|
238
|
-
VMware calls this unwrapping, but really it's a KDF with the properties of this Phrase
|
|
246
|
+
VMware calls this unwrapping, but really it's a KDF with the properties of this ``Phrase``.
|
|
239
247
|
"""
|
|
240
248
|
return hashlib.pbkdf2_hmac(
|
|
241
249
|
PASS2KEY_MAP[self.pass2key],
|
|
@@ -246,13 +254,13 @@ class Phrase:
|
|
|
246
254
|
)
|
|
247
255
|
|
|
248
256
|
|
|
249
|
-
def _parse_key_locator(locator_string: str):
|
|
257
|
+
def _parse_key_locator(locator_string: str) -> Pair | Phrase | list[Pair | Phrase]:
|
|
250
258
|
"""Parse a key locator from a string.
|
|
251
259
|
|
|
252
|
-
Key locators are string formatted data structures with a forward slash (
|
|
253
|
-
prefixed with a type, followed by that types' specific data. Values between separators are
|
|
260
|
+
Key locators are string formatted data structures with a forward slash (``/``) separator. Each component is
|
|
261
|
+
prefixed with a type, followed by that types' specific data. Values between separators are URL encoded.
|
|
254
262
|
|
|
255
|
-
Interally called
|
|
263
|
+
Interally called ``KeyLocator``.
|
|
256
264
|
"""
|
|
257
265
|
|
|
258
266
|
identifier, _, remainder = locator_string.partition("/")
|
|
@@ -261,7 +269,8 @@ def _parse_key_locator(locator_string: str):
|
|
|
261
269
|
# Comma separated list in between braces
|
|
262
270
|
# list/(member,member)
|
|
263
271
|
return [_parse_key_locator(member) for member in _split_list(remainder)]
|
|
264
|
-
|
|
272
|
+
|
|
273
|
+
if identifier == "pair":
|
|
265
274
|
# Comma separated tuple with 3 members
|
|
266
275
|
# pair/(key data,mac type,encrypted data)
|
|
267
276
|
members = _split_list(remainder)
|
|
@@ -270,7 +279,8 @@ def _parse_key_locator(locator_string: str):
|
|
|
270
279
|
unquote(members[1]),
|
|
271
280
|
base64.b64decode(unquote(members[2])),
|
|
272
281
|
)
|
|
273
|
-
|
|
282
|
+
|
|
283
|
+
if identifier == "phrase":
|
|
274
284
|
# Serialized crypto dict, prefixed with an identifier
|
|
275
285
|
# phrase/encoded id/encoded dict
|
|
276
286
|
phrase_id, _, phrase_data = remainder.partition("/")
|
|
@@ -282,20 +292,19 @@ def _parse_key_locator(locator_string: str):
|
|
|
282
292
|
int(crypto_dict["rounds"]),
|
|
283
293
|
base64.b64decode(crypto_dict["salt"]),
|
|
284
294
|
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
|
|
296
|
+
# rawkey, ldap, script, role, fqid
|
|
297
|
+
raise NotImplementedError(f"Not implemented keysafe identifier: {identifier}")
|
|
288
298
|
|
|
289
299
|
|
|
290
|
-
def _split_list(
|
|
300
|
+
def _split_list(value: str) -> list[str]:
|
|
291
301
|
"""Parse a key locator list from a string.
|
|
292
302
|
|
|
293
303
|
Lists are wrapped by braces and separated by comma. They can contain nested lists/pairs,
|
|
294
304
|
so we need to separate at the correct nest level.
|
|
295
305
|
"""
|
|
296
306
|
|
|
297
|
-
match
|
|
298
|
-
if not match:
|
|
307
|
+
if not (match := re.match(r"\((.+)\)", value)):
|
|
299
308
|
raise ValueError("Invalid list string")
|
|
300
309
|
|
|
301
310
|
contents = match.group(1)
|
|
@@ -321,12 +330,12 @@ def _split_list(list_string: str) -> List[str]:
|
|
|
321
330
|
return members
|
|
322
331
|
|
|
323
332
|
|
|
324
|
-
def _parse_crypto_dict(dict_string: str) ->
|
|
333
|
+
def _parse_crypto_dict(dict_string: str) -> dict[str, str]:
|
|
325
334
|
"""Parse a crypto dict from a string.
|
|
326
335
|
|
|
327
|
-
Crypto dicts are encoded as
|
|
336
|
+
Crypto dicts are encoded as ``key=encoded_value:key=encoded_value``.
|
|
328
337
|
|
|
329
|
-
Internally called
|
|
338
|
+
Internally called ``CryptoDict``.
|
|
330
339
|
"""
|
|
331
340
|
|
|
332
341
|
crypto_dict = {}
|
|
@@ -360,7 +369,7 @@ def _decrypt_hmac(key: bytes, data: bytes, digest: str) -> bytes:
|
|
|
360
369
|
return decrypted
|
|
361
370
|
|
|
362
371
|
|
|
363
|
-
def _create_cipher(key: bytes, iv: bytes):
|
|
372
|
+
def _create_cipher(key: bytes, iv: bytes) -> AES.CbcMode:
|
|
364
373
|
"""Create a cipher object.
|
|
365
374
|
|
|
366
375
|
Dynamic based on the available crypto module.
|
|
@@ -377,7 +386,8 @@ def _create_cipher(key: bytes, iv: bytes):
|
|
|
377
386
|
raise ValueError(f"Invalid key size: {len(key)}")
|
|
378
387
|
|
|
379
388
|
return _pystandalone.cipher(cipher, key, iv)
|
|
380
|
-
|
|
389
|
+
|
|
390
|
+
if HAS_PYCRYPTODOME:
|
|
381
391
|
return AES.new(key, AES.MODE_CBC, iv=iv)
|
|
382
|
-
|
|
383
|
-
|
|
392
|
+
|
|
393
|
+
raise RuntimeError("No crypto module available")
|
{dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/c_qcow2.py
RENAMED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from dissect.cstruct import cstruct
|
|
2
4
|
|
|
3
5
|
qcow2_def = """
|
|
@@ -169,8 +171,9 @@ UNALLOCATED_SUBCLUSTER_TYPES = (
|
|
|
169
171
|
)
|
|
170
172
|
|
|
171
173
|
|
|
172
|
-
def ctz(value, size=32):
|
|
174
|
+
def ctz(value: int, size: int = 32) -> int:
|
|
173
175
|
"""Count the number of zero bits in an integer of a given size."""
|
|
174
176
|
for i in range(size):
|
|
175
177
|
if value & (1 << i):
|
|
176
178
|
return i
|
|
179
|
+
return 0
|
{dissect_hypervisor-3.17.dev1 → dissect_hypervisor-3.17.dev3}/dissect/hypervisor/disk/hdd.py
RENAMED
|
@@ -4,9 +4,8 @@ from bisect import bisect_right
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from functools import cached_property
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
8
8
|
from uuid import UUID
|
|
9
|
-
from xml.etree.ElementTree import Element
|
|
10
9
|
|
|
11
10
|
from defusedxml import ElementTree
|
|
12
11
|
from dissect.util.stream import AlignedStream
|
|
@@ -14,6 +13,10 @@ from dissect.util.stream import AlignedStream
|
|
|
14
13
|
from dissect.hypervisor.disk.c_hdd import SECTOR_SIZE, c_hdd
|
|
15
14
|
from dissect.hypervisor.exceptions import InvalidHeaderError
|
|
16
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Iterator
|
|
18
|
+
from xml.etree.ElementTree import Element
|
|
19
|
+
|
|
17
20
|
DEFAULT_TOP_GUID = UUID("{5fbaabe3-6958-40ff-92a7-860e329aab41}")
|
|
18
21
|
NULL_GUID = UUID("00000000-0000-0000-0000-000000000000")
|
|
19
22
|
|
|
@@ -76,7 +79,7 @@ class HDD:
|
|
|
76
79
|
# If the path is relative, it's always relative to the HDD root
|
|
77
80
|
return (root / path).open("rb")
|
|
78
81
|
|
|
79
|
-
def open(self, guid:
|
|
82
|
+
def open(self, guid: str | UUID | None = None) -> BinaryIO:
|
|
80
83
|
"""Open a stream for this HDD, optionally for a specific snapshot.
|
|
81
84
|
|
|
82
85
|
If no snapshot GUID is provided, the "top" snapshot will be used.
|
|
@@ -154,7 +157,7 @@ class XMLEntry:
|
|
|
154
157
|
|
|
155
158
|
@classmethod
|
|
156
159
|
def _from_xml(cls, element: Element) -> XMLEntry:
|
|
157
|
-
raise NotImplementedError
|
|
160
|
+
raise NotImplementedError
|
|
158
161
|
|
|
159
162
|
|
|
160
163
|
@dataclass
|
|
@@ -213,7 +216,7 @@ class Image(XMLEntry):
|
|
|
213
216
|
|
|
214
217
|
@dataclass
|
|
215
218
|
class Snapshots(XMLEntry):
|
|
216
|
-
top_guid:
|
|
219
|
+
top_guid: UUID | None
|
|
217
220
|
shots: list[Shot]
|
|
218
221
|
|
|
219
222
|
@classmethod
|
|
@@ -307,7 +310,7 @@ class HDS(AlignedStream):
|
|
|
307
310
|
parent: Optional file-like object for the parent HDS file.
|
|
308
311
|
"""
|
|
309
312
|
|
|
310
|
-
def __init__(self, fh: BinaryIO, parent:
|
|
313
|
+
def __init__(self, fh: BinaryIO, parent: BinaryIO | None = None):
|
|
311
314
|
self.fh = fh
|
|
312
315
|
self.parent = parent
|
|
313
316
|
|
|
@@ -357,7 +360,7 @@ class HDS(AlignedStream):
|
|
|
357
360
|
|
|
358
361
|
return b"".join(result)
|
|
359
362
|
|
|
360
|
-
def _iter_runs(self, offset: int, length: int) -> Iterator[
|
|
363
|
+
def _iter_runs(self, offset: int, length: int) -> Iterator[tuple[int, int]]:
|
|
361
364
|
"""Iterate optimized read runs for a given offset and read length.
|
|
362
365
|
|
|
363
366
|
Args:
|
|
@@ -374,12 +377,9 @@ class HDS(AlignedStream):
|
|
|
374
377
|
read_size = min(self.cluster_size - offset_in_cluster, length)
|
|
375
378
|
|
|
376
379
|
bat_entry = bat[cluster_idx]
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
read_offset = 0
|
|
381
|
-
else:
|
|
382
|
-
read_offset = (bat_entry * self._bat_multiplier * SECTOR_SIZE) + offset_in_cluster
|
|
380
|
+
# BAT entry of 0 means either a sparse or a parent read
|
|
381
|
+
# Use 0 to denote a sparse run for now to make calculations easier
|
|
382
|
+
read_offset = 0 if bat_entry == 0 else bat_entry * self._bat_multiplier * SECTOR_SIZE + offset_in_cluster
|
|
383
383
|
|
|
384
384
|
if run_offset is None:
|
|
385
385
|
# First iteration
|