dissect.target 3.19.dev10__py3-none-any.whl → 3.19.dev12__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.
@@ -25,6 +25,7 @@ class ChromePlugin(ChromiumMixin, BrowserPlugin):
25
25
  DIRS = [
26
26
  # Windows
27
27
  "AppData/Local/Google/Chrome/User Data/Default",
28
+ "AppData/Local/Google/Chrome/User Data/Snapshots/*/Default",
28
29
  "AppData/Local/Google/Chrome/continuousUpdates/User Data/Default",
29
30
  "Local Settings/Application Data/Google/Chrome/User Data/Default",
30
31
  # Linux
@@ -79,11 +79,12 @@ class ChromiumMixin:
79
79
  users_dirs: list[tuple] = []
80
80
  for user_details in self.target.user_details.all_with_home():
81
81
  for d in hist_paths:
82
- cur_dir: TargetPath = user_details.home_path.joinpath(d)
83
- cur_dir = cur_dir.resolve()
84
- if not cur_dir.exists() or (user_details, cur_dir) in users_dirs:
85
- continue
86
- users_dirs.append((user_details, cur_dir))
82
+ home_dir: TargetPath = user_details.home_path
83
+ for cur_dir in home_dir.glob(d):
84
+ cur_dir = cur_dir.resolve()
85
+ if not cur_dir.exists() or (user_details.user, cur_dir) in users_dirs:
86
+ continue
87
+ users_dirs.append((user_details, cur_dir))
87
88
  return users_dirs
88
89
 
89
90
  def _iter_db(
@@ -28,6 +28,7 @@ class EdgePlugin(ChromiumMixin, BrowserPlugin):
28
28
  ".var/app/com.microsoft.Edge/config/microsoft-edge/Default",
29
29
  # Windows
30
30
  "AppData/Local/Microsoft/Edge/User Data/Default",
31
+ "AppData/Local/Microsoft/Edge/User Data/Snapshots/*/Default",
31
32
  # Macos
32
33
  "Library/Application Support/Microsoft Edge/Default",
33
34
  ]
@@ -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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.19.dev10
3
+ Version: 3.19.dev12
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
@@ -121,9 +120,9 @@ dissect/target/plugins/apps/av/trendmicro.py,sha256=8F4IWYCXG7HEniGridQ4ax82Mrx_
121
120
  dissect/target/plugins/apps/browser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
122
121
  dissect/target/plugins/apps/browser/brave.py,sha256=EW1ubL10swHeV9CscfpE-SrNZozul_Ewj48LNRaG5Kg,2865
123
122
  dissect/target/plugins/apps/browser/browser.py,sha256=rBIwcgdl73gm-8APwx2jEUAYXRniXkqcdMr2UYj_tS8,4118
124
- dissect/target/plugins/apps/browser/chrome.py,sha256=hxS8gqpBwoCrPaxNpllIa6K9DtsSGzn6XXcUaHyes6w,3048
125
- dissect/target/plugins/apps/browser/chromium.py,sha256=N9hS-a45iEv_GyKhLZQR_FSkEjWlMA0f22eURBuxF5Y,27999
126
- dissect/target/plugins/apps/browser/edge.py,sha256=woXzZtHPWmfcV8vbxGKHELKru5JRb32MAXs43_b4K4E,2883
123
+ dissect/target/plugins/apps/browser/chrome.py,sha256=DMONTYE95sI_jcmyQOapHwWQWwrezfYMllVCCPwhEP0,3117
124
+ dissect/target/plugins/apps/browser/chromium.py,sha256=QOeWSSXFM1IbUh3PMUB14oy6sqAS_v2B3xhthAMM_8k,28058
125
+ dissect/target/plugins/apps/browser/edge.py,sha256=tuuIbm4s8nNstA6nIOEfU0LG0jt20a8gf3rve2SXtdM,2953
127
126
  dissect/target/plugins/apps/browser/firefox.py,sha256=3Ucp85DXTDyCofW1_aEzjba_Pr0QyC4F5gX8NqY-uOg,30981
128
127
  dissect/target/plugins/apps/browser/iexplore.py,sha256=g_xw0toaiyjevxO8g9XPCOqc-CXZp39FVquRhPFGdTE,8801
129
128
  dissect/target/plugins/apps/container/__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
@@ -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.dev10.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
349
- dissect.target-3.19.dev10.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
350
- dissect.target-3.19.dev10.dist-info/METADATA,sha256=_IaES22tqwU15qttr5A5X3d0VUa43dSOESK8X8Ao0K0,12719
351
- dissect.target-3.19.dev10.dist-info/WHEEL,sha256=FZ75kcLy9M91ncbIgG8dnpCncbiKXSRGJ_PFILs6SFg,91
352
- dissect.target-3.19.dev10.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
353
- dissect.target-3.19.dev10.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
354
- dissect.target-3.19.dev10.dist-info/RECORD,,
347
+ dissect.target-3.19.dev12.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
348
+ dissect.target-3.19.dev12.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
349
+ dissect.target-3.19.dev12.dist-info/METADATA,sha256=UR_OR6Mke9csuzDPtguApwOU8mClzRRkkWmmiqNv364,12719
350
+ dissect.target-3.19.dev12.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
351
+ dissect.target-3.19.dev12.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
352
+ dissect.target-3.19.dev12.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
353
+ dissect.target-3.19.dev12.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.0.1)
2
+ Generator: setuptools (71.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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
- )