Pixseal 1.0.0__pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
- Pixseal/__init__.py +34 -0
- Pixseal/imageSigner.py +342 -0
- Pixseal/imageValidator.py +399 -0
- Pixseal/keyInput.py +91 -0
- Pixseal/simpleImage.py +35 -0
- Pixseal/simpleImage_ext.pypy39-pp73-x86_64-linux-gnu.so +0 -0
- Pixseal/simpleImage_py.py +770 -0
- pixseal-1.0.0.dist-info/METADATA +285 -0
- pixseal-1.0.0.dist-info/RECORD +11 -0
- pixseal-1.0.0.dist-info/WHEEL +6 -0
- pixseal-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
from typing import TYPE_CHECKING, Sequence
|
|
4
|
+
import base64
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
|
7
|
+
from cryptography.exceptions import InvalidSignature
|
|
8
|
+
from cryptography.hazmat.primitives import hashes
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
10
|
+
|
|
11
|
+
from .imageSigner import (
|
|
12
|
+
BinaryProvider,
|
|
13
|
+
addHiddenBit,
|
|
14
|
+
_build_payload_json,
|
|
15
|
+
make_channel_key,
|
|
16
|
+
_choose_channel,
|
|
17
|
+
)
|
|
18
|
+
from .keyInput import PublicKeyInput, resolve_public_key
|
|
19
|
+
|
|
20
|
+
# profiler check
|
|
21
|
+
try:
|
|
22
|
+
from line_profiler import profile
|
|
23
|
+
except ImportError:
|
|
24
|
+
|
|
25
|
+
def profile(func):
|
|
26
|
+
return func
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Dynamic typing
|
|
30
|
+
from .simpleImage import (
|
|
31
|
+
ImageInput as _RuntimeImageInput,
|
|
32
|
+
SimpleImage as _RuntimeSimpleImage,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from .simpleImage_py import ImageInput, SimpleImage
|
|
37
|
+
else:
|
|
38
|
+
ImageInput = _RuntimeImageInput
|
|
39
|
+
SimpleImage = _RuntimeSimpleImage
|
|
40
|
+
|
|
41
|
+
# JSON field names
|
|
42
|
+
PAYLOAD_FIELD: str = "payload"
|
|
43
|
+
PAYLOAD_SIG_FIELD: str = "payloadSig"
|
|
44
|
+
IMAGE_HASH_FIELD: str = "imageHash"
|
|
45
|
+
IMAGE_HASH_SIG_FIELD: str = "imageHashSig"
|
|
46
|
+
|
|
47
|
+
# Sentinel
|
|
48
|
+
START_SENTINEL: str = "START-VALIDATION"
|
|
49
|
+
END_SENTINEL: str = "END-VALIDATION"
|
|
50
|
+
|
|
51
|
+
# Report print option
|
|
52
|
+
TAIL_HEAD_LEN = 20
|
|
53
|
+
TAIL_SUFFIX_LEN = 10
|
|
54
|
+
FULL_TAIL_EXTRA_LEN = 20
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_json_like(value: str) -> bool:
|
|
58
|
+
return value.lstrip().startswith("{")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _extract_payload_json(
|
|
62
|
+
deduplicated: list[str],
|
|
63
|
+
) -> dict:
|
|
64
|
+
for value in deduplicated:
|
|
65
|
+
if not _is_json_like(value):
|
|
66
|
+
continue
|
|
67
|
+
if not value.rstrip().endswith("}"):
|
|
68
|
+
continue
|
|
69
|
+
try:
|
|
70
|
+
payload_obj = json.loads(value)
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
continue
|
|
73
|
+
if not isinstance(payload_obj, dict):
|
|
74
|
+
continue
|
|
75
|
+
if not all(
|
|
76
|
+
key in payload_obj
|
|
77
|
+
for key in (
|
|
78
|
+
PAYLOAD_FIELD,
|
|
79
|
+
PAYLOAD_SIG_FIELD,
|
|
80
|
+
IMAGE_HASH_FIELD,
|
|
81
|
+
IMAGE_HASH_SIG_FIELD,
|
|
82
|
+
)
|
|
83
|
+
):
|
|
84
|
+
continue
|
|
85
|
+
return payload_obj
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def binaryToString(binaryCode):
|
|
90
|
+
string = []
|
|
91
|
+
for i in range(0, len(binaryCode), 8):
|
|
92
|
+
byte = binaryCode[i : i + 8]
|
|
93
|
+
decimal = int(byte, 2)
|
|
94
|
+
character = chr(decimal)
|
|
95
|
+
string.append(character)
|
|
96
|
+
return "".join(string)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@profile
|
|
100
|
+
def readHiddenBit(imageInput: ImageInput, channel_key: bytes | None = None):
|
|
101
|
+
img = (
|
|
102
|
+
imageInput
|
|
103
|
+
if isinstance(imageInput, SimpleImage)
|
|
104
|
+
else SimpleImage.open(imageInput)
|
|
105
|
+
)
|
|
106
|
+
width, height = img.size
|
|
107
|
+
pixels = img._pixels # direct buffer access for performance
|
|
108
|
+
total = width * height
|
|
109
|
+
bits = []
|
|
110
|
+
append_bit = bits.append
|
|
111
|
+
|
|
112
|
+
if channel_key is None:
|
|
113
|
+
for idx in range(total):
|
|
114
|
+
# Progress Check
|
|
115
|
+
# print("readHiddenBit Current : ", idx, "/", total)
|
|
116
|
+
|
|
117
|
+
base = idx * 3
|
|
118
|
+
r = pixels[base]
|
|
119
|
+
g = pixels[base + 1]
|
|
120
|
+
b = pixels[base + 2]
|
|
121
|
+
|
|
122
|
+
diffR = r - 127
|
|
123
|
+
if diffR < 0:
|
|
124
|
+
diffR = -diffR
|
|
125
|
+
diffG = g - 127
|
|
126
|
+
if diffG < 0:
|
|
127
|
+
diffG = -diffG
|
|
128
|
+
diffB = b - 127
|
|
129
|
+
if diffB < 0:
|
|
130
|
+
diffB = -diffB
|
|
131
|
+
|
|
132
|
+
maxDiff = diffR
|
|
133
|
+
if diffG > maxDiff:
|
|
134
|
+
maxDiff = diffG
|
|
135
|
+
if diffB > maxDiff:
|
|
136
|
+
maxDiff = diffB
|
|
137
|
+
|
|
138
|
+
bit = "1" if maxDiff % 2 == 0 else "0"
|
|
139
|
+
append_bit(bit)
|
|
140
|
+
else:
|
|
141
|
+
for idx in range(total):
|
|
142
|
+
base = idx * 3
|
|
143
|
+
channel = _choose_channel(idx, channel_key)
|
|
144
|
+
bit = pixels[base + channel] & 1
|
|
145
|
+
append_bit("1" if bit else "0")
|
|
146
|
+
|
|
147
|
+
return "".join(bits)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def deduplicate(arr: Sequence[str]) -> tuple[list[str], str]:
|
|
151
|
+
deduplicated = []
|
|
152
|
+
freq = {}
|
|
153
|
+
most_common = ""
|
|
154
|
+
most_count = 0
|
|
155
|
+
|
|
156
|
+
for i, value in enumerate(arr):
|
|
157
|
+
freq[value] = freq.get(value, 0) + 1
|
|
158
|
+
if freq[value] > most_count:
|
|
159
|
+
most_count = freq[value]
|
|
160
|
+
most_common = value
|
|
161
|
+
|
|
162
|
+
if i == 0 or value != arr[i - 1]:
|
|
163
|
+
deduplicated.append(value)
|
|
164
|
+
|
|
165
|
+
return deduplicated, most_common
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Check functions
|
|
169
|
+
def lengthCheck(arr: list[str]):
|
|
170
|
+
return len(arr) in (3, 4)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def tailCheck(arr: list[str]):
|
|
174
|
+
if len(arr) != 4:
|
|
175
|
+
return None # Not required
|
|
176
|
+
|
|
177
|
+
full_cipher = arr[1] # complete ciphertext
|
|
178
|
+
truncated_cipher = arr[2] # incomplete ciphertext
|
|
179
|
+
|
|
180
|
+
return full_cipher.startswith(truncated_cipher)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def verifySigniture(original: str, sig: str, publicKey: RSAPublicKey) -> bool:
|
|
184
|
+
try:
|
|
185
|
+
publicKey.verify(
|
|
186
|
+
data=original.encode("utf-8"),
|
|
187
|
+
signature=base64.b64decode(sig, validate=True),
|
|
188
|
+
padding=padding.PSS(
|
|
189
|
+
mgf=padding.MGF1(hashes.SHA256()),
|
|
190
|
+
salt_length=padding.PSS.MAX_LENGTH,
|
|
191
|
+
),
|
|
192
|
+
algorithm=hashes.SHA256(),
|
|
193
|
+
)
|
|
194
|
+
except InvalidSignature:
|
|
195
|
+
return False
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# def buildValidationReport(
|
|
200
|
+
# decrypted, tailCheck: bool, skipPlain: bool = False, hashCheck=None
|
|
201
|
+
# ):
|
|
202
|
+
# # Length after deduplication/decryption
|
|
203
|
+
# arrayLength = len(decrypted)
|
|
204
|
+
|
|
205
|
+
# # 1. Check that the deduplicated sequence length is valid
|
|
206
|
+
# lengthCheck = arrayLength in (3, 4)
|
|
207
|
+
|
|
208
|
+
# # 2. Validate start/end markers
|
|
209
|
+
# startCheck = decrypted[0] == "START-VALIDATION" if decrypted else False
|
|
210
|
+
# endCheck = decrypted[-1] == "END-VALIDATION" if decrypted else False
|
|
211
|
+
|
|
212
|
+
# # 4. Determine whether payload was successfully decrypted
|
|
213
|
+
# decryptedPayload = decrypted[1] if len(decrypted) > 1 else ""
|
|
214
|
+
# isDecrypted = bool(decryptedPayload) and not decryptedPayload.endswith("==")
|
|
215
|
+
|
|
216
|
+
# checkList = [lengthCheck, startCheck, endCheck, isDecrypted]
|
|
217
|
+
# # 5. Parse tailCheck result
|
|
218
|
+
# if tailCheck is None:
|
|
219
|
+
# tailCheckResult = "Not Required"
|
|
220
|
+
# else:
|
|
221
|
+
# tailCheckResult = tailCheck
|
|
222
|
+
# checkList.append(tailCheckResult)
|
|
223
|
+
|
|
224
|
+
# if hashCheck is None:
|
|
225
|
+
# hashCheckResult = "Not Checked"
|
|
226
|
+
# else:
|
|
227
|
+
# hashCheckResult = hashCheck
|
|
228
|
+
# checkList.append(hashCheckResult)
|
|
229
|
+
|
|
230
|
+
# # Overall verdict requires every check to pass
|
|
231
|
+
# verdict = all(checkList)
|
|
232
|
+
|
|
233
|
+
# result = {
|
|
234
|
+
# "arrayLength": arrayLength,
|
|
235
|
+
# "lengthCheck": lengthCheck,
|
|
236
|
+
# "startCheck": startCheck,
|
|
237
|
+
# "endCheck": endCheck,
|
|
238
|
+
# "isDecrypted": isDecrypted,
|
|
239
|
+
# "tailCheckResult": tailCheckResult,
|
|
240
|
+
# "hashCheckResult": hashCheckResult,
|
|
241
|
+
# "verdict": verdict,
|
|
242
|
+
# }
|
|
243
|
+
|
|
244
|
+
# if skipPlain:
|
|
245
|
+
# result["decryptSkipMessage"] = (
|
|
246
|
+
# "Skip decrypt: payload was plain or corrupted text despite decrypt request."
|
|
247
|
+
# )
|
|
248
|
+
|
|
249
|
+
# return result
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# main
|
|
253
|
+
def validateImage(imageInput: ImageInput, publicKey: PublicKeyInput):
|
|
254
|
+
"""
|
|
255
|
+
Extract the embedded payload from an image and optionally decrypt it.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
imageInput: File path, bytes, or file-like object accepted by SimpleImage.
|
|
259
|
+
publicKey: RSA public key or certificate (object, bytes, or path).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Dict with the most common extracted string, decrypted sequence, and
|
|
263
|
+
a validation report describing the sentinel checks and verdict.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
publicKey = resolve_public_key(publicKey)
|
|
267
|
+
channel_key = make_channel_key(publicKey)
|
|
268
|
+
resultBinary = readHiddenBit(imageInput, channel_key=channel_key)
|
|
269
|
+
resultString = binaryToString(resultBinary)
|
|
270
|
+
splitted = resultString.split("\n")
|
|
271
|
+
|
|
272
|
+
deduplicated, most_common = deduplicate(splitted)
|
|
273
|
+
if not deduplicated or most_common == "":
|
|
274
|
+
print("deduplicate : \n", deduplicated)
|
|
275
|
+
print("most_common : \n", most_common)
|
|
276
|
+
raise ValueError("deduplication failed!")
|
|
277
|
+
|
|
278
|
+
lengthCheckResult = lengthCheck(deduplicated)
|
|
279
|
+
tailCheckResult = tailCheck(deduplicated)
|
|
280
|
+
|
|
281
|
+
payload_obj = _extract_payload_json(deduplicated)
|
|
282
|
+
if not payload_obj:
|
|
283
|
+
raise ValueError("json extraction from payload failed!")
|
|
284
|
+
|
|
285
|
+
payload_text = payload_obj[PAYLOAD_FIELD]
|
|
286
|
+
payload_sig = payload_obj[PAYLOAD_SIG_FIELD]
|
|
287
|
+
image_hash = payload_obj[IMAGE_HASH_FIELD]
|
|
288
|
+
image_hash_sig = payload_obj[IMAGE_HASH_SIG_FIELD]
|
|
289
|
+
if (
|
|
290
|
+
not isinstance(payload_text, str)
|
|
291
|
+
or not isinstance(payload_sig, str)
|
|
292
|
+
or not isinstance(image_hash, str)
|
|
293
|
+
or not isinstance(image_hash_sig, str)
|
|
294
|
+
):
|
|
295
|
+
raise TypeError("Essenstial value missing!")
|
|
296
|
+
imageHashVerifyResult = verifySigniture(
|
|
297
|
+
original=image_hash, sig=image_hash_sig, publicKey=publicKey
|
|
298
|
+
)
|
|
299
|
+
payloadVerifyResult = verifySigniture(
|
|
300
|
+
original=payload_text, sig=payload_sig, publicKey=publicKey
|
|
301
|
+
)
|
|
302
|
+
start_sig = deduplicated[0]
|
|
303
|
+
end_sig = deduplicated[-1]
|
|
304
|
+
startVerifyResult = verifySigniture(
|
|
305
|
+
original=START_SENTINEL, sig=start_sig, publicKey=publicKey
|
|
306
|
+
)
|
|
307
|
+
endVerifyResult = verifySigniture(
|
|
308
|
+
original=END_SENTINEL, sig=end_sig, publicKey=publicKey
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
image_hash_placeholder = "0" * len(image_hash)
|
|
312
|
+
image_hash_sig_placeholder = "0" * len(image_hash_sig)
|
|
313
|
+
|
|
314
|
+
payload_placeholder = _build_payload_json(
|
|
315
|
+
payload_text,
|
|
316
|
+
payload_sig,
|
|
317
|
+
image_hash_placeholder,
|
|
318
|
+
image_hash_sig_placeholder,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
hiddenBinary = BinaryProvider(
|
|
322
|
+
payload=payload_placeholder + "\n",
|
|
323
|
+
startString=start_sig + "\n",
|
|
324
|
+
endString="\n" + end_sig,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
placeholder_image = addHiddenBit(
|
|
328
|
+
imageInput,
|
|
329
|
+
hiddenBinary,
|
|
330
|
+
channel_key=channel_key,
|
|
331
|
+
)
|
|
332
|
+
computed_hash = hashlib.sha256(placeholder_image._pixels).hexdigest()
|
|
333
|
+
imageHashCompareCheckResult = image_hash == computed_hash
|
|
334
|
+
|
|
335
|
+
verdict = all(
|
|
336
|
+
[
|
|
337
|
+
lengthCheckResult,
|
|
338
|
+
tailCheckResult,
|
|
339
|
+
startVerifyResult,
|
|
340
|
+
endVerifyResult,
|
|
341
|
+
payloadVerifyResult,
|
|
342
|
+
imageHashVerifyResult,
|
|
343
|
+
imageHashCompareCheckResult,
|
|
344
|
+
]
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
length_report = {
|
|
348
|
+
"length": len(deduplicated),
|
|
349
|
+
"result": lengthCheckResult,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if len(deduplicated) == 4:
|
|
353
|
+
full_value = deduplicated[1]
|
|
354
|
+
tail_value = deduplicated[2]
|
|
355
|
+
tail_min_len = TAIL_HEAD_LEN + TAIL_SUFFIX_LEN + 3
|
|
356
|
+
if len(tail_value) > tail_min_len:
|
|
357
|
+
tail_display = (
|
|
358
|
+
tail_value[:TAIL_HEAD_LEN] + "..." + tail_value[-TAIL_SUFFIX_LEN:]
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
tail_display = tail_value
|
|
362
|
+
if len(full_value) > tail_min_len:
|
|
363
|
+
start = len(tail_value) - TAIL_SUFFIX_LEN
|
|
364
|
+
if start < 0:
|
|
365
|
+
start = 0
|
|
366
|
+
end = len(tail_value) + FULL_TAIL_EXTRA_LEN
|
|
367
|
+
if end > len(full_value):
|
|
368
|
+
end = len(full_value)
|
|
369
|
+
snippet = full_value[start:end]
|
|
370
|
+
if end < len(full_value):
|
|
371
|
+
snippet = snippet + "..."
|
|
372
|
+
full_display = full_value[:TAIL_HEAD_LEN] + "..." + snippet
|
|
373
|
+
else:
|
|
374
|
+
full_display = full_value
|
|
375
|
+
tail_report = {
|
|
376
|
+
"full": full_display,
|
|
377
|
+
"tail": tail_display,
|
|
378
|
+
"result": tailCheckResult,
|
|
379
|
+
}
|
|
380
|
+
else:
|
|
381
|
+
tail_report = {"result": "Not Required"}
|
|
382
|
+
hash_report = {
|
|
383
|
+
"extractedHash": image_hash,
|
|
384
|
+
"computedHash": computed_hash,
|
|
385
|
+
"result": imageHashCompareCheckResult,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
report = {
|
|
389
|
+
"lengthCheck": length_report,
|
|
390
|
+
"tailCheck": tail_report,
|
|
391
|
+
"startVerify": startVerifyResult,
|
|
392
|
+
"endtVerify": endVerifyResult,
|
|
393
|
+
"payloadVerify": payloadVerifyResult,
|
|
394
|
+
"imageHashVerify": imageHashVerifyResult,
|
|
395
|
+
"imageHashCompareCheck": hash_report,
|
|
396
|
+
"verdict": verdict,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return report
|
Pixseal/keyInput.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import TypeAlias
|
|
3
|
+
|
|
4
|
+
from cryptography import x509
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
|
7
|
+
|
|
8
|
+
PublicKeyInput: TypeAlias = (
|
|
9
|
+
RSAPublicKey | x509.Certificate | bytes | bytearray | memoryview | str | Path
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
PrivateKeyInput: TypeAlias = RSAPrivateKey | bytes | bytearray | memoryview | str | Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_public_key_bytes(
|
|
16
|
+
public_key_input: bytes | bytearray | memoryview | str | Path,
|
|
17
|
+
) -> bytes:
|
|
18
|
+
if isinstance(public_key_input, (bytes, bytearray, memoryview)):
|
|
19
|
+
return bytes(public_key_input)
|
|
20
|
+
public_key_path = Path(public_key_input)
|
|
21
|
+
if not public_key_path.is_file():
|
|
22
|
+
raise FileNotFoundError(
|
|
23
|
+
f"Public key/certificate file not found: {public_key_path}"
|
|
24
|
+
)
|
|
25
|
+
return public_key_path.read_bytes()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _public_key_from_cert(cert: x509.Certificate) -> RSAPublicKey:
|
|
29
|
+
public_key = cert.public_key()
|
|
30
|
+
if not isinstance(public_key, RSAPublicKey):
|
|
31
|
+
raise TypeError("Certificate does not contain an RSA public key")
|
|
32
|
+
|
|
33
|
+
print("[Cert] Certificate loaded")
|
|
34
|
+
return public_key
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_public_key(public_key_input: PublicKeyInput) -> RSAPublicKey:
|
|
38
|
+
if isinstance(public_key_input, RSAPublicKey):
|
|
39
|
+
return public_key_input
|
|
40
|
+
if isinstance(public_key_input, x509.Certificate):
|
|
41
|
+
return _public_key_from_cert(public_key_input)
|
|
42
|
+
|
|
43
|
+
key_data = _load_public_key_bytes(public_key_input)
|
|
44
|
+
if b"BEGIN CERTIFICATE" in key_data:
|
|
45
|
+
cert = x509.load_pem_x509_certificate(key_data)
|
|
46
|
+
return _public_key_from_cert(cert)
|
|
47
|
+
if b"BEGIN PUBLIC KEY" in key_data or b"BEGIN RSA PUBLIC KEY" in key_data:
|
|
48
|
+
public_key = serialization.load_pem_public_key(key_data)
|
|
49
|
+
if not isinstance(public_key, RSAPublicKey):
|
|
50
|
+
raise TypeError("Provided file is not an RSA public key")
|
|
51
|
+
return public_key
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
cert = x509.load_der_x509_certificate(key_data)
|
|
55
|
+
except ValueError:
|
|
56
|
+
public_key = serialization.load_der_public_key(key_data)
|
|
57
|
+
if not isinstance(public_key, RSAPublicKey):
|
|
58
|
+
raise TypeError("Provided file is not an RSA public key")
|
|
59
|
+
return public_key
|
|
60
|
+
|
|
61
|
+
return _public_key_from_cert(cert)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_private_key_bytes(
|
|
65
|
+
private_key_input: bytes | bytearray | memoryview | str | Path,
|
|
66
|
+
) -> bytes:
|
|
67
|
+
if isinstance(private_key_input, (bytes, bytearray, memoryview)):
|
|
68
|
+
return bytes(private_key_input)
|
|
69
|
+
private_key_path = Path(private_key_input)
|
|
70
|
+
if not private_key_path.is_file():
|
|
71
|
+
raise FileNotFoundError(f"Private key file not found: {private_key_path}")
|
|
72
|
+
return private_key_path.read_bytes()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_private_key(private_key_input: PrivateKeyInput) -> RSAPrivateKey:
|
|
76
|
+
if isinstance(private_key_input, RSAPrivateKey):
|
|
77
|
+
return private_key_input
|
|
78
|
+
|
|
79
|
+
key_data = _load_private_key_bytes(private_key_input)
|
|
80
|
+
if b"BEGIN" in key_data:
|
|
81
|
+
if b"PRIVATE KEY" not in key_data:
|
|
82
|
+
raise ValueError("Provided file does not contain a private key")
|
|
83
|
+
if b"ENCRYPTED PRIVATE KEY" in key_data:
|
|
84
|
+
raise ValueError("Encrypted private keys require a password")
|
|
85
|
+
private_key = serialization.load_pem_private_key(key_data, password=None)
|
|
86
|
+
else:
|
|
87
|
+
private_key = serialization.load_der_private_key(key_data, password=None)
|
|
88
|
+
|
|
89
|
+
if not isinstance(private_key, RSAPrivateKey):
|
|
90
|
+
raise TypeError("Provided file is not an RSA private key")
|
|
91
|
+
return private_key
|
Pixseal/simpleImage.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loader that prefers the Cython implementation and falls back to pure Python.
|
|
3
|
+
Use environment variable PIXSEAL_SIMPLEIMAGE_BACKEND to force a backend:
|
|
4
|
+
- "cython": require compiled extension
|
|
5
|
+
- "python": force pure Python implementation
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from os import getenv
|
|
9
|
+
|
|
10
|
+
_backend = getenv("PIXSEAL_SIMPLEIMAGE_BACKEND", "auto").lower()
|
|
11
|
+
_loaded_module = "Pixseal.simpleImage_py"
|
|
12
|
+
|
|
13
|
+
if _backend == "python":
|
|
14
|
+
from . import simpleImage_py as _impl # type: ignore
|
|
15
|
+
elif _backend == "cython":
|
|
16
|
+
try:
|
|
17
|
+
from . import simpleImage_ext as _impl # type: ignore
|
|
18
|
+
except ImportError as exc: # pragma: no cover
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"Cython backend requested but Pixseal.simpleImage_ext is not available. "
|
|
21
|
+
"Build the extension or choose the python backend."
|
|
22
|
+
) from exc
|
|
23
|
+
else:
|
|
24
|
+
try: # pragma: no cover
|
|
25
|
+
from . import simpleImage_ext as _impl # type: ignore
|
|
26
|
+
except ImportError: # pragma: no cover
|
|
27
|
+
from . import simpleImage_py as _impl # type: ignore
|
|
28
|
+
|
|
29
|
+
SimpleImage = _impl.SimpleImage # type: ignore
|
|
30
|
+
ImageInput = _impl.ImageInput # type: ignore
|
|
31
|
+
_loaded_module = _impl.__name__
|
|
32
|
+
|
|
33
|
+
print(f"[SimpleImage] Loaded from: {_loaded_module}")
|
|
34
|
+
|
|
35
|
+
__all__ = ["SimpleImage", "ImageInput"]
|
|
Binary file
|