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
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
|