dissect.target 3.19.dev11__py3-none-any.whl → 3.19.dev13__py3-none-any.whl

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.
@@ -7,7 +7,6 @@ from typing import Iterator
7
7
  from dissect.target import Target
8
8
  from dissect.target.exceptions import UnsupportedPluginError
9
9
  from dissect.target.helpers.fsutil import TargetPath
10
- from dissect.target.helpers.ssh import SSHPrivateKey
11
10
  from dissect.target.plugin import export
12
11
  from dissect.target.plugins.apps.ssh.ssh import (
13
12
  AuthorizedKeysRecord,
@@ -15,6 +14,7 @@ from dissect.target.plugins.apps.ssh.ssh import (
15
14
  PrivateKeyRecord,
16
15
  PublicKeyRecord,
17
16
  SSHPlugin,
17
+ SSHPrivateKey,
18
18
  calculate_fingerprints,
19
19
  )
20
20
 
@@ -1,10 +1,55 @@
1
1
  import base64
2
+ import binascii
2
3
  from hashlib import md5, sha1, sha256
3
4
 
5
+ from dissect.cstruct import cstruct
6
+
4
7
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
5
8
  from dissect.target.helpers.record import create_extended_descriptor
6
9
  from dissect.target.plugin import NamespacePlugin
7
10
 
11
+ rfc4716_def = """
12
+ struct ssh_string {
13
+ uint32 length;
14
+ char value[length];
15
+ }
16
+
17
+ struct ssh_private_key {
18
+ char magic[15];
19
+
20
+ ssh_string cipher;
21
+ ssh_string kdf_name;
22
+ ssh_string kdf_options;
23
+
24
+ uint32 number_of_keys;
25
+
26
+ ssh_string public;
27
+ ssh_string private;
28
+ }
29
+ """
30
+
31
+ c_rfc4716 = cstruct(endian=">").load(rfc4716_def)
32
+
33
+ RFC4716_MARKER_START = b"-----BEGIN OPENSSH PRIVATE KEY-----"
34
+ RFC4716_MARKER_END = b"-----END OPENSSH PRIVATE KEY-----"
35
+ RFC4716_MAGIC = b"openssh-key-v1\x00"
36
+ RFC4716_PADDING = b"\x01\x02\x03\x04\x05\x06\x07"
37
+ RFC4716_NONE = b"none"
38
+
39
+ PKCS8_MARKER_START = b"-----BEGIN PRIVATE KEY-----"
40
+ PKCS8_MARKER_END = b"-----END PRIVATE KEY-----"
41
+ PKCS8_MARKER_START_ENCRYPTED = b"-----BEGIN ENCRYPTED PRIVATE KEY-----"
42
+ PKCS8_MARKER_END_ENCRYPTED = b"-----END ENCRYPTED PRIVATE KEY-----"
43
+
44
+ PEM_MARKER_START_RSA = b"-----BEGIN RSA PRIVATE KEY-----"
45
+ PEM_MARKER_END_RSA = b"-----END RSA PRIVATE KEY-----"
46
+ PEM_MARKER_START_DSA = b"-----BEGIN DSA PRIVATE KEY-----"
47
+ PEM_MARKER_END_DSA = b"-----END DSA PRIVATE KEY-----"
48
+ PEM_MARKER_START_EC = b"-----BEGIN EC PRIVATE KEY-----"
49
+ PEM_MARKER_END_EC = b"-----END EC PRIVATE KEY-----"
50
+ PEM_ENCRYPTED = b"ENCRYPTED"
51
+
52
+
8
53
  OpenSSHUserRecordDescriptor = create_extended_descriptor([UserRecordDescriptorExtension])
9
54
 
10
55
  COMMON_ELLEMENTS = [
@@ -96,3 +141,135 @@ def calculate_fingerprints(public_key_decoded: bytes, ssh_keygen_format: bool =
96
141
  fingerprint_sha256 = digest_sha256.hex()
97
142
 
98
143
  return digest_md5.hex(), fingerprint_sha1, fingerprint_sha256
144
+
145
+
146
+ def is_rfc4716(data: bytes) -> bool:
147
+ """Validate data is a valid looking SSH private key in the OpenSSH format."""
148
+ return data.startswith(RFC4716_MARKER_START) and data.endswith(RFC4716_MARKER_END)
149
+
150
+
151
+ def decode_rfc4716(data: bytes) -> bytes:
152
+ """Base64 decode the private key data."""
153
+ encoded_key_data = data.removeprefix(RFC4716_MARKER_START).removesuffix(RFC4716_MARKER_END)
154
+ try:
155
+ return base64.b64decode(encoded_key_data)
156
+ except binascii.Error:
157
+ raise ValueError("Error decoding RFC4716 key data")
158
+
159
+
160
+ def is_pkcs8(data: bytes) -> bool:
161
+ """Validate data is a valid looking PKCS8 SSH private key."""
162
+ return (data.startswith(PKCS8_MARKER_START) and data.endswith(PKCS8_MARKER_END)) or (
163
+ data.startswith(PKCS8_MARKER_START_ENCRYPTED) and data.endswith(PKCS8_MARKER_END_ENCRYPTED)
164
+ )
165
+
166
+
167
+ def is_pem(data: bytes) -> bool:
168
+ """Validate data is a valid looking PEM SSH private key."""
169
+ return (
170
+ (data.startswith(PEM_MARKER_START_RSA) and data.endswith(PEM_MARKER_END_RSA))
171
+ or (data.startswith(PEM_MARKER_START_DSA) and data.endswith(PEM_MARKER_END_DSA))
172
+ or (data.startswith(PEM_MARKER_START_EC) and data.endswith(PEM_MARKER_END_EC))
173
+ )
174
+
175
+
176
+ class SSHPrivateKey:
177
+ """A class to parse (OpenSSH-supported) SSH private keys.
178
+
179
+ OpenSSH supports three types of keys:
180
+ * RFC4716 (default)
181
+ * PKCS8
182
+ * PEM
183
+ """
184
+
185
+ def __init__(self, data: bytes):
186
+ self.key_type = None
187
+ self.public_key = None
188
+ self.comment = ""
189
+
190
+ if is_rfc4716(data):
191
+ self.format = "RFC4716"
192
+ self._parse_rfc4716(data)
193
+
194
+ elif is_pkcs8(data):
195
+ self.format = "PKCS8"
196
+ self.is_encrypted = data.startswith(PKCS8_MARKER_START_ENCRYPTED)
197
+
198
+ elif is_pem(data):
199
+ self.format = "PEM"
200
+ self._parse_pem(data)
201
+
202
+ else:
203
+ raise ValueError("Unsupported private key format")
204
+
205
+ def _parse_rfc4716(self, data: bytes) -> None:
206
+ """Parse OpenSSH format SSH private keys.
207
+
208
+ The format:
209
+ "openssh-key-v1"0x00 # NULL-terminated "Auth Magic" string
210
+ 32-bit length, "none" # ciphername length and string
211
+ 32-bit length, "none" # kdfname length and string
212
+ 32-bit length, nil # kdf (0 length, no kdf)
213
+ 32-bit 0x01 # number of keys, hard-coded to 1 (no length)
214
+ 32-bit length, sshpub # public key in ssh format
215
+ 32-bit length, keytype
216
+ 32-bit length, pub0
217
+ 32-bit length, pub1
218
+ 32-bit length for rnd+prv+comment+pad
219
+ 64-bit dummy checksum? # a random 32-bit int, repeated
220
+ 32-bit length, keytype # the private key (including public)
221
+ 32-bit length, pub0 # Public Key parts
222
+ 32-bit length, pub1
223
+ 32-bit length, prv0 # Private Key parts
224
+ ... # (number varies by type)
225
+ 32-bit length, comment # comment string
226
+ padding bytes 0x010203 # pad to blocksize (see notes below)
227
+
228
+ Source: https://coolaj86.com/articles/the-openssh-private-key-format/
229
+ """
230
+
231
+ key_data = decode_rfc4716(data)
232
+ private_key = c_rfc4716.ssh_private_key(key_data)
233
+
234
+ # RFC4716 only supports 1 key at the moment.
235
+ if private_key.magic != RFC4716_MAGIC or private_key.number_of_keys != 1:
236
+ raise ValueError("Unexpected number of keys for RFC4716 format private key")
237
+
238
+ self.is_encrypted = private_key.cipher.value != RFC4716_NONE
239
+
240
+ self.public_key = base64.b64encode(private_key.public.value)
241
+ public_key_type = c_rfc4716.ssh_string(private_key.public.value)
242
+ self.key_type = public_key_type.value
243
+
244
+ if not self.is_encrypted:
245
+ private_key_data = private_key.private.value.rstrip(RFC4716_PADDING)
246
+
247
+ # We skip the two dummy uint32s at the start.
248
+ private_key_index = 8
249
+
250
+ private_key_type = c_rfc4716.ssh_string(private_key_data[private_key_index:])
251
+ private_key_index += 4 + private_key_type.length
252
+ self.key_type = private_key_type.value
253
+
254
+ private_key_fields = []
255
+ while private_key_index < len(private_key_data):
256
+ field = c_rfc4716.ssh_string(private_key_data[private_key_index:])
257
+ private_key_index += 4 + field.length
258
+ private_key_fields.append(field)
259
+
260
+ # There is always a comment present (with a length field of 0 for empty comments).
261
+ self.comment = private_key_fields[-1].value
262
+
263
+ def _parse_pem(self, data: bytes) -> None:
264
+ """Detect key type and encryption of PEM keys."""
265
+ self.is_encrypted = PEM_ENCRYPTED in data
266
+
267
+ if data.startswith(PEM_MARKER_START_RSA):
268
+ self.key_type = "ssh-rsa"
269
+
270
+ elif data.startswith(PEM_MARKER_START_DSA):
271
+ self.key_type = "ssh-dss"
272
+
273
+ # This is not a valid SSH key type, but we currently do not detect the specific ecdsa variant.
274
+ else:
275
+ self.key_type = "ecdsa"
@@ -40,12 +40,18 @@ class UnixPlugin(OSPlugin):
40
40
  @export(record=UnixUserRecord)
41
41
  @arg("--sessions", action="store_true", help="Parse syslog for recent user sessions")
42
42
  def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
43
- """Recover users from /etc/passwd, /etc/master.passwd or /var/log/syslog session logins."""
43
+ """Yield unix user records from passwd files or syslog session logins.
44
+
45
+ Resources:
46
+ - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
47
+ """
48
+
49
+ PASSWD_FILES = ["/etc/passwd", "/etc/passwd-", "/etc/master.passwd"]
44
50
 
45
51
  seen_users = set()
46
52
 
47
53
  # Yield users found in passwd files.
48
- for passwd_file in ["/etc/passwd", "/etc/master.passwd"]:
54
+ for passwd_file in PASSWD_FILES:
49
55
  if (path := self.target.fs.path(passwd_file)).exists():
50
56
  for line in path.open("rt"):
51
57
  line = line.strip()
@@ -53,7 +59,12 @@ class UnixPlugin(OSPlugin):
53
59
  continue
54
60
 
55
61
  pwent = dict(enumerate(line.split(":")))
56
- seen_users.add((pwent.get(0), pwent.get(5), pwent.get(6)))
62
+
63
+ current_user = (pwent.get(0), pwent.get(5), pwent.get(6))
64
+ if current_user in seen_users:
65
+ continue
66
+
67
+ seen_users.add(current_user)
57
68
  yield UnixUserRecord(
58
69
  name=pwent.get(0),
59
70
  passwd=pwent.get(1),
@@ -29,39 +29,55 @@ class ShadowPlugin(Plugin):
29
29
  if not self.target.fs.path("/etc/shadow").exists():
30
30
  raise UnsupportedPluginError("No shadow file found")
31
31
 
32
+ SHADOW_FILES = ["/etc/shadow", "/etc/shadow-"]
33
+
32
34
  @export(record=UnixShadowRecord)
33
35
  def passwords(self) -> Iterator[UnixShadowRecord]:
34
- """Recover shadow records from /etc/shadow files."""
35
-
36
- if (path := self.target.fs.path("/etc/shadow")).exists():
37
- for line in path.open("rt"):
38
- line = line.strip()
39
- if line == "" or line.startswith("#"):
40
- continue
41
-
42
- shent = dict(enumerate(line.split(":")))
43
- crypt = extract_crypt_details(shent)
44
-
45
- # do not return a shadow record if we have no hash
46
- if crypt.get("hash") is None or crypt.get("hash") == "":
47
- continue
48
-
49
- yield UnixShadowRecord(
50
- name=shent.get(0),
51
- crypt=shent.get(1),
52
- algorithm=crypt.get("algo"),
53
- crypt_param=crypt.get("param"),
54
- salt=crypt.get("salt"),
55
- hash=crypt.get("hash"),
56
- last_change=shent.get(2),
57
- min_age=shent.get(3),
58
- max_age=shent.get(4),
59
- warning_period=shent.get(5),
60
- inactivity_period=shent.get(6),
61
- expiration_date=shent.get(7),
62
- unused_field=shent.get(8),
63
- _target=self.target,
64
- )
36
+ """Yield shadow records from /etc/shadow files.
37
+
38
+ Resources:
39
+ - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html#file:/etc/shadow
40
+ """
41
+
42
+ seen_hashes = set()
43
+
44
+ for shadow_file in self.SHADOW_FILES:
45
+ if (path := self.target.fs.path(shadow_file)).exists():
46
+ for line in path.open("rt"):
47
+ line = line.strip()
48
+ if line == "" or line.startswith("#"):
49
+ continue
50
+
51
+ shent = dict(enumerate(line.split(":")))
52
+ crypt = extract_crypt_details(shent)
53
+
54
+ # do not return a shadow record if we have no hash
55
+ if crypt.get("hash") is None or crypt.get("hash") == "":
56
+ continue
57
+
58
+ # prevent duplicate user hashes
59
+ current_hash = (shent.get(0), crypt.get("hash"))
60
+ if current_hash in seen_hashes:
61
+ continue
62
+
63
+ seen_hashes.add(current_hash)
64
+
65
+ yield UnixShadowRecord(
66
+ name=shent.get(0),
67
+ crypt=shent.get(1),
68
+ algorithm=crypt.get("algo"),
69
+ crypt_param=crypt.get("param"),
70
+ salt=crypt.get("salt"),
71
+ hash=crypt.get("hash"),
72
+ last_change=shent.get(2),
73
+ min_age=shent.get(3),
74
+ max_age=shent.get(4),
75
+ warning_period=shent.get(5),
76
+ inactivity_period=shent.get(6),
77
+ expiration_date=shent.get(7),
78
+ unused_field=shent.get(8),
79
+ _target=self.target,
80
+ )
65
81
 
66
82
 
67
83
  def extract_crypt_details(shent: dict) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.19.dev11
3
+ Version: 3.19.dev13
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -65,7 +65,6 @@ dissect/target/helpers/record.py,sha256=lWl7k2Mp9Axllm0tXzPGJx2zj2zONsyY_p5g424T
65
65
  dissect/target/helpers/record_modifier.py,sha256=3I_rC5jqvl0TsW3V8OQ6Dltz_D8J4PU1uhhzbJGKm9c,3245
66
66
  dissect/target/helpers/regutil.py,sha256=kX-sSZbW8Qkg29Dn_9zYbaQrwLumrr4Y8zJ1EhHXIAM,27337
67
67
  dissect/target/helpers/shell_folder_ids.py,sha256=Behhb8oh0kMxrEk6YYKYigCDZe8Hw5QS6iK_d2hTs2Y,24978
68
- dissect/target/helpers/ssh.py,sha256=obB7sqUH0IoUo78NAmHM8TX0pgA_4GHICZ3TA3TW_0E,6324
69
68
  dissect/target/helpers/targetd.py,sha256=ELhUulzQ4OgXgHsWhsLgM14vut8Wm6btr7qTynlwKaE,1812
70
69
  dissect/target/helpers/utils.py,sha256=r36Bn0UL0E6Z8ajmQrHzC6RyUxTRdwJ1PNsd904Lmzs,4027
71
70
  dissect/target/helpers/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -135,10 +134,10 @@ dissect/target/plugins/apps/remoteaccess/teamviewer.py,sha256=SiEH36HM2NvdPuCjfL
135
134
  dissect/target/plugins/apps/shell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
135
  dissect/target/plugins/apps/shell/powershell.py,sha256=biPSMRWxPI6kRqP0-75yMtrw0Ti2Bzfl_xI3xbmmF48,2641
137
136
  dissect/target/plugins/apps/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
- dissect/target/plugins/apps/ssh/openssh.py,sha256=yt3bX93Q9wfF25_vG9APMwfZWUUqCPyLlVJdhu20syI,7250
137
+ dissect/target/plugins/apps/ssh/openssh.py,sha256=oaJeKmTvVMo4aePo4Ep7t0ludJPNuuokGEW07w4gAvQ,7216
139
138
  dissect/target/plugins/apps/ssh/opensshd.py,sha256=DaXKdgGF3GYHHA4buEvphcm6FF4C8YFjgD96Dv6rRnM,5510
140
139
  dissect/target/plugins/apps/ssh/putty.py,sha256=EmsXr2NbOB13-EWS5AkpEPMUhOkVl6FAy8JGUiaDhxk,10133
141
- dissect/target/plugins/apps/ssh/ssh.py,sha256=tTA87u0B8yY1yVCPV0VJdRUct6ggkir_pIziP-eKnVo,3009
140
+ dissect/target/plugins/apps/ssh/ssh.py,sha256=d3U8PJbtMvOV3K0wV_9KzGt2oRs-mfNQz1_Xd6SNx0Y,9320
142
141
  dissect/target/plugins/apps/vpn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
142
  dissect/target/plugins/apps/vpn/openvpn.py,sha256=d-DGINTIHP_bvv3T09ZwbezHXGctvCyAhJ482m2_-a0,7654
144
143
  dissect/target/plugins/apps/vpn/wireguard.py,sha256=SoAMED_bwWJQ3nci5qEY-qV4wJKSSDZQ8K7DoJRYq0k,6521
@@ -184,7 +183,7 @@ dissect/target/plugins/general/scrape.py,sha256=Fz7BNXflvuxlnVulyyDhLpyU8D_hJdH6
184
183
  dissect/target/plugins/general/users.py,sha256=cQXPQ2XbkPjckCPHYTUW4JEhYN0_CT8JI8hJPZn3qSs,3030
185
184
  dissect/target/plugins/os/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
186
185
  dissect/target/plugins/os/unix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
187
- dissect/target/plugins/os/unix/_os.py,sha256=VQHx8PDhJ0NHzNuo3RIN3DGXkLqpGXbQe8M5ZbtI5nM,14277
186
+ dissect/target/plugins/os/unix/_os.py,sha256=GcbP8HbK1XtwYFGbl8x0BdfoLAC2ROv9xieeFGI5dWM,14557
188
187
  dissect/target/plugins/os/unix/cronjobs.py,sha256=2ssj97UVJueyATVl7NMJmqd9uHflQ2tXUqdOCFIEje8,3182
189
188
  dissect/target/plugins/os/unix/datetime.py,sha256=gKfBdPyUirt3qmVYfOJ1oZXRPn8wRzssbZxR_ARrtk8,1518
190
189
  dissect/target/plugins/os/unix/etc.py,sha256=HoPEC1hxqurSnAXQAK-jf_HxdBIDe-1z_qSw_n-ViI4,258
@@ -192,7 +191,7 @@ dissect/target/plugins/os/unix/generic.py,sha256=6_MJrV1LbIxNQJwAZR0HEQljoxwF5BP
192
191
  dissect/target/plugins/os/unix/history.py,sha256=ptNGHkHOLJ5bE4r1PqtkQFcQHqzS6-qe5ms1tTGOJp8,6620
193
192
  dissect/target/plugins/os/unix/locale.py,sha256=V3R7mEyrH3f-h7SGAucByaYYDA2SIil9Qb-s3dPmDEA,3961
194
193
  dissect/target/plugins/os/unix/packagemanager.py,sha256=Wm2AAJOD_B3FAcZNXgWtSm_YwbvrHBYOP8bPmOXNjG4,2427
195
- dissect/target/plugins/os/unix/shadow.py,sha256=TvN04uzFnUttNMZAa6_1XdXSP-8V6ztbZNoetDvfD0w,3535
194
+ dissect/target/plugins/os/unix/shadow.py,sha256=W6W6rMru7IVnuBc6sl5wsRWTOrJdS1s7_2_q7QRf7Is,4148
196
195
  dissect/target/plugins/os/unix/bsd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
197
196
  dissect/target/plugins/os/unix/bsd/_os.py,sha256=e5rttTOFOmd7e2HqP9ZZFMEiPLBr-8rfH0XH1IIeroQ,1372
198
197
  dissect/target/plugins/os/unix/bsd/citrix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -345,10 +344,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
345
344
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
346
345
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
347
346
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
348
- dissect.target-3.19.dev11.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
349
- dissect.target-3.19.dev11.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
350
- dissect.target-3.19.dev11.dist-info/METADATA,sha256=K__-QEn5j-2bVryeNdtzhb6et_uhqklZ7KSWkBT33pM,12719
351
- dissect.target-3.19.dev11.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
352
- dissect.target-3.19.dev11.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
353
- dissect.target-3.19.dev11.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
354
- dissect.target-3.19.dev11.dist-info/RECORD,,
347
+ dissect.target-3.19.dev13.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
348
+ dissect.target-3.19.dev13.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
349
+ dissect.target-3.19.dev13.dist-info/METADATA,sha256=oFZiiry3QZEqrgYijsGOlPjZn1DfUM3GBMdf8WZaIFc,12719
350
+ dissect.target-3.19.dev13.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
351
+ dissect.target-3.19.dev13.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
352
+ dissect.target-3.19.dev13.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
353
+ dissect.target-3.19.dev13.dist-info/RECORD,,
@@ -1,177 +0,0 @@
1
- import base64
2
- import binascii
3
-
4
- from dissect.cstruct import cstruct
5
-
6
- rfc4716_def = """
7
- struct ssh_string {
8
- uint32 length;
9
- char value[length];
10
- }
11
-
12
- struct ssh_private_key {
13
- char magic[15];
14
-
15
- ssh_string cipher;
16
- ssh_string kdf_name;
17
- ssh_string kdf_options;
18
-
19
- uint32 number_of_keys;
20
-
21
- ssh_string public;
22
- ssh_string private;
23
- }
24
- """
25
-
26
- c_rfc4716 = cstruct(endian=">").load(rfc4716_def)
27
-
28
- RFC4716_MARKER_START = b"-----BEGIN OPENSSH PRIVATE KEY-----"
29
- RFC4716_MARKER_END = b"-----END OPENSSH PRIVATE KEY-----"
30
- RFC4716_MAGIC = b"openssh-key-v1\x00"
31
- RFC4716_PADDING = b"\x01\x02\x03\x04\x05\x06\x07"
32
- RFC4716_NONE = b"none"
33
-
34
- PKCS8_MARKER_START = b"-----BEGIN PRIVATE KEY-----"
35
- PKCS8_MARKER_END = b"-----END PRIVATE KEY-----"
36
- PKCS8_MARKER_START_ENCRYPTED = b"-----BEGIN ENCRYPTED PRIVATE KEY-----"
37
- PKCS8_MARKER_END_ENCRYPTED = b"-----END ENCRYPTED PRIVATE KEY-----"
38
-
39
- PEM_MARKER_START_RSA = b"-----BEGIN RSA PRIVATE KEY-----"
40
- PEM_MARKER_END_RSA = b"-----END RSA PRIVATE KEY-----"
41
- PEM_MARKER_START_DSA = b"-----BEGIN DSA PRIVATE KEY-----"
42
- PEM_MARKER_END_DSA = b"-----END DSA PRIVATE KEY-----"
43
- PEM_MARKER_START_EC = b"-----BEGIN EC PRIVATE KEY-----"
44
- PEM_MARKER_END_EC = b"-----END EC PRIVATE KEY-----"
45
- PEM_ENCRYPTED = b"ENCRYPTED"
46
-
47
-
48
- class SSHPrivateKey:
49
- """A class to parse (OpenSSH-supported) SSH private keys.
50
-
51
- OpenSSH supports three types of keys:
52
- * RFC4716 (default)
53
- * PKCS8
54
- * PEM
55
- """
56
-
57
- def __init__(self, data: bytes):
58
- self.key_type = None
59
- self.public_key = None
60
- self.comment = ""
61
-
62
- if is_rfc4716(data):
63
- self.format = "RFC4716"
64
- self._parse_rfc4716(data)
65
-
66
- elif is_pkcs8(data):
67
- self.format = "PKCS8"
68
- self.is_encrypted = data.startswith(PKCS8_MARKER_START_ENCRYPTED)
69
-
70
- elif is_pem(data):
71
- self.format = "PEM"
72
- self._parse_pem(data)
73
-
74
- else:
75
- raise ValueError("Unsupported private key format")
76
-
77
- def _parse_rfc4716(self, data: bytes) -> None:
78
- """Parse OpenSSH format SSH private keys.
79
-
80
- The format:
81
- "openssh-key-v1"0x00 # NULL-terminated "Auth Magic" string
82
- 32-bit length, "none" # ciphername length and string
83
- 32-bit length, "none" # kdfname length and string
84
- 32-bit length, nil # kdf (0 length, no kdf)
85
- 32-bit 0x01 # number of keys, hard-coded to 1 (no length)
86
- 32-bit length, sshpub # public key in ssh format
87
- 32-bit length, keytype
88
- 32-bit length, pub0
89
- 32-bit length, pub1
90
- 32-bit length for rnd+prv+comment+pad
91
- 64-bit dummy checksum? # a random 32-bit int, repeated
92
- 32-bit length, keytype # the private key (including public)
93
- 32-bit length, pub0 # Public Key parts
94
- 32-bit length, pub1
95
- 32-bit length, prv0 # Private Key parts
96
- ... # (number varies by type)
97
- 32-bit length, comment # comment string
98
- padding bytes 0x010203 # pad to blocksize (see notes below)
99
-
100
- Source: https://coolaj86.com/articles/the-openssh-private-key-format/
101
- """
102
-
103
- key_data = decode_rfc4716(data)
104
- private_key = c_rfc4716.ssh_private_key(key_data)
105
-
106
- # RFC4716 only supports 1 key at the moment.
107
- if private_key.magic != RFC4716_MAGIC or private_key.number_of_keys != 1:
108
- raise ValueError("Unexpected number of keys for RFC4716 format private key")
109
-
110
- self.is_encrypted = private_key.cipher.value != RFC4716_NONE
111
-
112
- self.public_key = base64.b64encode(private_key.public.value)
113
- public_key_type = c_rfc4716.ssh_string(private_key.public.value)
114
- self.key_type = public_key_type.value
115
-
116
- if not self.is_encrypted:
117
- private_key_data = private_key.private.value.rstrip(RFC4716_PADDING)
118
-
119
- # We skip the two dummy uint32s at the start.
120
- private_key_index = 8
121
-
122
- private_key_type = c_rfc4716.ssh_string(private_key_data[private_key_index:])
123
- private_key_index += 4 + private_key_type.length
124
- self.key_type = private_key_type.value
125
-
126
- private_key_fields = []
127
- while private_key_index < len(private_key_data):
128
- field = c_rfc4716.ssh_string(private_key_data[private_key_index:])
129
- private_key_index += 4 + field.length
130
- private_key_fields.append(field)
131
-
132
- # There is always a comment present (with a length field of 0 for empty comments).
133
- self.comment = private_key_fields[-1].value
134
-
135
- def _parse_pem(self, data: bytes) -> None:
136
- """Detect key type and encryption of PEM keys."""
137
- self.is_encrypted = PEM_ENCRYPTED in data
138
-
139
- if data.startswith(PEM_MARKER_START_RSA):
140
- self.key_type = "ssh-rsa"
141
-
142
- elif data.startswith(PEM_MARKER_START_DSA):
143
- self.key_type = "ssh-dss"
144
-
145
- # This is not a valid SSH key type, but we currently do not detect the specific ecdsa variant.
146
- else:
147
- self.key_type = "ecdsa"
148
-
149
-
150
- def is_rfc4716(data: bytes) -> bool:
151
- """Validate data is a valid looking SSH private key in the OpenSSH format."""
152
- return data.startswith(RFC4716_MARKER_START) and data.endswith(RFC4716_MARKER_END)
153
-
154
-
155
- def decode_rfc4716(data: bytes) -> bytes:
156
- """Base64 decode the private key data."""
157
- encoded_key_data = data.removeprefix(RFC4716_MARKER_START).removesuffix(RFC4716_MARKER_END)
158
- try:
159
- return base64.b64decode(encoded_key_data)
160
- except binascii.Error:
161
- raise ValueError("Error decoding RFC4716 key data")
162
-
163
-
164
- def is_pkcs8(data: bytes) -> bool:
165
- """Validate data is a valid looking PKCS8 SSH private key."""
166
- return (data.startswith(PKCS8_MARKER_START) and data.endswith(PKCS8_MARKER_END)) or (
167
- data.startswith(PKCS8_MARKER_START_ENCRYPTED) and data.endswith(PKCS8_MARKER_END_ENCRYPTED)
168
- )
169
-
170
-
171
- def is_pem(data: bytes) -> bool:
172
- """Validate data is a valid looking PEM SSH private key."""
173
- return (
174
- (data.startswith(PEM_MARKER_START_RSA) and data.endswith(PEM_MARKER_END_RSA))
175
- or (data.startswith(PEM_MARKER_START_DSA) and data.endswith(PEM_MARKER_END_DSA))
176
- or (data.startswith(PEM_MARKER_START_EC) and data.endswith(PEM_MARKER_END_EC))
177
- )