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 ADDED
@@ -0,0 +1,34 @@
1
+ from .simpleImage import ImageInput, SimpleImage
2
+ from .imageSigner import (
3
+ BinaryProvider,
4
+ addHiddenBit,
5
+ signImage,
6
+ )
7
+ from .imageValidator import (
8
+ binaryToString,
9
+ deduplicate,
10
+ readHiddenBit,
11
+ validateImage,
12
+ )
13
+ from .keyInput import (
14
+ PublicKeyInput,
15
+ PrivateKeyInput,
16
+ resolve_public_key,
17
+ resolve_private_key,
18
+ )
19
+
20
+ __all__ = [
21
+ "SimpleImage",
22
+ "ImageInput",
23
+ "BinaryProvider",
24
+ "addHiddenBit",
25
+ "signImage",
26
+ "binaryToString",
27
+ "deduplicate",
28
+ "readHiddenBit",
29
+ "validateImage",
30
+ "PublicKeyInput",
31
+ "resolve_public_key",
32
+ "PrivateKeyInput",
33
+ "resolve_private_key",
34
+ ]
Pixseal/imageSigner.py ADDED
@@ -0,0 +1,342 @@
1
+ import base64
2
+ import hashlib
3
+ import hmac
4
+ import json
5
+ from typing import TYPE_CHECKING
6
+
7
+ from cryptography.hazmat.primitives import hashes, serialization
8
+ from cryptography.hazmat.primitives.asymmetric import padding
9
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
10
+
11
+ from .keyInput import PrivateKeyInput, resolve_private_key
12
+
13
+ # profiler check
14
+ try:
15
+ from line_profiler import profile
16
+ except ImportError:
17
+
18
+ def profile(func):
19
+ return func
20
+
21
+
22
+ # Dynamic typing
23
+ from .simpleImage import (
24
+ ImageInput as _RuntimeImageInput,
25
+ SimpleImage as _RuntimeSimpleImage,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from .simpleImage_py import ImageInput, SimpleImage
30
+ else:
31
+ ImageInput = _RuntimeImageInput
32
+ SimpleImage = _RuntimeSimpleImage
33
+
34
+
35
+ class BinaryProvider:
36
+ # Constructor
37
+ def __init__(
38
+ self,
39
+ payload,
40
+ startString="START-VALIDATION\n",
41
+ endString="\nEND-VALIDATION",
42
+ ):
43
+ self.hiddenBits = self._stringToBits(payload)
44
+ self.startBits = self._stringToBits(startString)
45
+ self.endBits = self._stringToBits(endString)
46
+
47
+ # Convert string to contiguous binary digits
48
+ def _stringToBits(self, string):
49
+ bits = []
50
+ for char in string:
51
+ binary = format(ord(char), "08b")
52
+ bits.extend(int(bit) for bit in binary)
53
+ return bits
54
+
55
+ def _expandPayload(self, count: int):
56
+ if count <= 0:
57
+ return []
58
+ payloadLen = len(self.hiddenBits)
59
+ if payloadLen == 0:
60
+ raise ValueError("Hidden payload is empty")
61
+ repeats, remainder = divmod(count, payloadLen)
62
+ return (self.hiddenBits * repeats) + self.hiddenBits[:remainder]
63
+
64
+ def buildBitArray(self, pixelCount: int):
65
+ startLen = len(self.startBits)
66
+ endLen = len(self.endBits)
67
+ if pixelCount < startLen + endLen:
68
+ raise ValueError("Image is too small to fit start/end sentinels")
69
+
70
+ bits = [0] * pixelCount
71
+
72
+ # 1. Place START-VALIDATION bits first
73
+ bits[:startLen] = self.startBits
74
+
75
+ # 2. Fill the remaining slots by repeating the payload bits
76
+ payloadSlots = pixelCount - startLen
77
+ payloadBits = self._expandPayload(payloadSlots)
78
+ bits[startLen:] = payloadBits[:payloadSlots]
79
+
80
+ # 3. Overwrite the tail with END-VALIDATION bits
81
+ tailStart = pixelCount - endLen
82
+ bits[tailStart:] = self.endBits
83
+
84
+ return bits
85
+
86
+
87
+ def make_channel_key(public_key: RSAPublicKey) -> bytes:
88
+ public_bytes = public_key.public_bytes(
89
+ encoding=serialization.Encoding.DER,
90
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
91
+ )
92
+ return hashlib.sha256(public_bytes).digest()
93
+
94
+
95
+ @profile
96
+ def _choose_channel(index: int, channel_key: bytes) -> int:
97
+ msg = index.to_bytes(8, "little", signed=False)
98
+ digest = hmac.new(channel_key, msg, hashlib.sha256).digest()
99
+ return digest[0] % 3
100
+
101
+
102
+ @profile
103
+ def addHiddenBit(
104
+ imageInput: ImageInput,
105
+ hiddenBinary: BinaryProvider,
106
+ channel_key: bytes | None = None,
107
+ ):
108
+ img = SimpleImage.open(imageInput)
109
+ width, height = img.size
110
+ pixels = img._pixels # direct buffer access for performance
111
+ total = width * height
112
+ payloadBits = hiddenBinary.buildBitArray(total)
113
+
114
+ if channel_key is None:
115
+ # Iterate over every pixel and inject one bit
116
+ for idx in range(total):
117
+ base = idx * 3
118
+ # Read the pixel
119
+ r = pixels[base]
120
+ g = pixels[base + 1]
121
+ b = pixels[base + 2]
122
+
123
+ # Calculate the distance from 127
124
+ diffR = r - 127
125
+ if diffR < 0:
126
+ diffR = -diffR
127
+ diffG = g - 127
128
+ if diffG < 0:
129
+ diffG = -diffG
130
+ diffB = b - 127
131
+ if diffB < 0:
132
+ diffB = -diffB
133
+
134
+ # Pick the component farthest from 127
135
+ maxDiff = diffR
136
+ if diffG > maxDiff:
137
+ maxDiff = diffG
138
+ if diffB > maxDiff:
139
+ maxDiff = diffB
140
+
141
+ # Actual value of that channel
142
+ if maxDiff == diffR:
143
+ targetColorValue = r
144
+ elif maxDiff == diffG:
145
+ targetColorValue = g
146
+ else:
147
+ targetColorValue = b
148
+
149
+ # Channels >=127 are decremented, <127 incremented
150
+ addDirection = 1 if targetColorValue < 127 else -1
151
+
152
+ # Pull next bit from provider
153
+ bit = payloadBits[idx]
154
+
155
+ # Force the selected channel parity to match the bit
156
+ if maxDiff == diffR:
157
+ if r % 2 != bit:
158
+ r += addDirection
159
+ if maxDiff == diffG:
160
+ if g % 2 != bit:
161
+ g += addDirection
162
+ if maxDiff == diffB:
163
+ if b % 2 != bit:
164
+ b += addDirection
165
+
166
+ # Write the updated pixel
167
+ pixels[base] = r
168
+ pixels[base + 1] = g
169
+ pixels[base + 2] = b
170
+ else:
171
+ # Keyed channel selection with explicit LSB overwrite.
172
+ for idx in range(total):
173
+ base = idx * 3
174
+ bit = payloadBits[idx] & 1
175
+ channel = _choose_channel(idx, channel_key)
176
+ offset = base + channel
177
+ pixels[offset] = (pixels[offset] & 0xFE) | bit
178
+
179
+ # Return the modified image
180
+ return img
181
+
182
+
183
+ # Helper function to encrypt a string with RSA private key
184
+ def stringSigner(plaintext: str, private_key: RSAPrivateKey) -> str:
185
+ signature = private_key.sign(
186
+ plaintext.encode("utf-8"),
187
+ padding.PSS(
188
+ mgf=padding.MGF1(hashes.SHA256()),
189
+ salt_length=padding.PSS.MAX_LENGTH,
190
+ ),
191
+ hashes.SHA256(),
192
+ )
193
+ return base64.b64encode(signature).decode("ascii")
194
+
195
+
196
+ # Helper function to calculate signature placeholder
197
+ def make_image_hash_placeholder() -> str:
198
+ """
199
+ Generate a placeholder string for the SHA256 image hash (hex length).
200
+ """
201
+ hash_hex_len = len(hashlib.sha256().hexdigest())
202
+ print("hash_hex_len = ", hash_hex_len)
203
+ return "0" * hash_hex_len
204
+
205
+
206
+ def make_hash_signature_placeholder(private_key) -> str:
207
+ """
208
+ Generate a placeholder string for the signature of the image hash.
209
+ """
210
+ key_bytes = (private_key.key_size + 7) // 8
211
+ signature_b64_len = len(base64.b64encode(b"\x00" * key_bytes))
212
+ print("signature_b64_len = ", signature_b64_len)
213
+ return "0" * signature_b64_len
214
+
215
+
216
+ # JSON field names
217
+ PAYLOAD_FIELD = "payload"
218
+ PAYLOAD_SIG_FIELD = "payloadSig"
219
+ IMAGE_HASH_FIELD = "imageHash"
220
+ IMAGE_HASH_SIG_FIELD = "imageHashSig"
221
+
222
+
223
+ # Helper function for building the JSON payload
224
+ def _build_payload_json(
225
+ payload: str | None,
226
+ payload_sig: str,
227
+ image_hash: str,
228
+ image_hash_sig: str,
229
+ ) -> str:
230
+ payload_obj = {
231
+ PAYLOAD_FIELD: payload,
232
+ PAYLOAD_SIG_FIELD: payload_sig,
233
+ IMAGE_HASH_FIELD: image_hash,
234
+ IMAGE_HASH_SIG_FIELD: image_hash_sig,
235
+ }
236
+
237
+ return json.dumps(payload_obj, separators=(",", ":"), ensure_ascii=True)
238
+
239
+
240
+ # main
241
+ # Image input (path or bytes) + payload string => returns image with embedded payload
242
+ def signImage(imageInput: ImageInput, payload: str, private_key: PrivateKeyInput):
243
+ """
244
+ Embed a payload into an image using the parity-based steganography scheme.
245
+
246
+ Args:
247
+ imageInput: File path, bytes, or file-like object accepted by SimpleImage.
248
+ payload: Text payload that should be signed (and optionally embedded).
249
+ private_key: RSA private key or PEM/DER-encoded key bytes/path used to
250
+ sign the payload hash, image hash, and sentinel markers.
251
+ includePlaintext: When True, embed the payload text alongside signatures.
252
+
253
+ Returns:
254
+ SimpleImage instance whose pixels include the signed payload.
255
+
256
+ Raises:
257
+ FileNotFoundError: If a private key path is provided but the file is missing.
258
+ ValueError: If the file is not a valid PEM private key.
259
+ """
260
+ if not payload:
261
+ raise TypeError("payload must be a non-empty string")
262
+
263
+ private_key = resolve_private_key(private_key)
264
+ payload_text = payload
265
+ payload_sig = stringSigner(payload_text, private_key)
266
+ image_hash_placeholder = make_image_hash_placeholder()
267
+ image_hash_sig_placeholder = make_hash_signature_placeholder(private_key)
268
+
269
+ payload_with_placeholder = _build_payload_json(
270
+ payload_text,
271
+ payload_sig,
272
+ image_hash_placeholder,
273
+ image_hash_sig_placeholder, # Placeholder for image hash signature
274
+ )
275
+ # print("payload #1")
276
+ # pprint(payload_with_placeholder)
277
+
278
+ # Sign the start/end markers
279
+ start_marker_sig = stringSigner("START-VALIDATION", private_key)
280
+ end_marker_sig = stringSigner("END-VALIDATION", private_key)
281
+ start_string = start_marker_sig + "\n"
282
+ end_string = "\n" + end_marker_sig
283
+ channel_key = make_channel_key(private_key.public_key())
284
+
285
+ # 1st injection: payload with placeholder
286
+ placeholder_binary = BinaryProvider(
287
+ payload=payload_with_placeholder + "\n",
288
+ startString=start_string,
289
+ endString=end_string,
290
+ )
291
+ image_with_placeholder = addHiddenBit(
292
+ imageInput,
293
+ placeholder_binary,
294
+ channel_key=channel_key,
295
+ )
296
+
297
+ # Calculate the image hash and sign it
298
+ image_hash = hashlib.sha256(image_with_placeholder._pixels).hexdigest()
299
+ if len(image_hash) != len(image_hash_placeholder):
300
+ raise ValueError(
301
+ "Signed hash length mismatch with placeholder"
302
+ + "\nhash len: "
303
+ + str(len(image_hash))
304
+ + "\nplaceholder len: "
305
+ + str(len(image_hash_placeholder))
306
+ )
307
+
308
+ # Sign the calculated hash
309
+ image_hash_sig = stringSigner(image_hash, private_key)
310
+ if len(image_hash_sig) != len(image_hash_sig_placeholder):
311
+ raise ValueError(
312
+ "Signed hash length mismatch with placeholder"
313
+ + "\nhash signiture len: "
314
+ + str(len(image_hash_sig))
315
+ + "\nplaceholder len: "
316
+ + str(len(image_hash_sig_placeholder))
317
+ )
318
+
319
+ # Prepare the final payload with the calculated hash
320
+ payload_final = _build_payload_json(
321
+ payload_text,
322
+ payload_sig,
323
+ image_hash,
324
+ image_hash_sig,
325
+ )
326
+ # print("payload #2")
327
+ # pprint(payload_final)
328
+
329
+ hiddenBinary = BinaryProvider(
330
+ payload=payload_final + "\n",
331
+ startString=start_string,
332
+ endString=end_string,
333
+ )
334
+
335
+ # Final injection: final payload
336
+ signedImage = addHiddenBit(
337
+ imageInput,
338
+ hiddenBinary,
339
+ channel_key=channel_key,
340
+ )
341
+
342
+ return signedImage