Pixseal 0.2.2__pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.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,26 @@
1
+ from .simpleImage import ImageInput, SimpleImage
2
+ from .imageSigner import (
3
+ BinaryProvider,
4
+ addHiddenBit,
5
+ signImage,
6
+ )
7
+ from .imageValidator import (
8
+ binaryToString,
9
+ buildValidationReport,
10
+ deduplicate,
11
+ readHiddenBit,
12
+ validateImage,
13
+ )
14
+
15
+ __all__ = [
16
+ "SimpleImage",
17
+ "ImageInput",
18
+ "BinaryProvider",
19
+ "addHiddenBit",
20
+ "signImage",
21
+ "binaryToString",
22
+ "buildValidationReport",
23
+ "deduplicate",
24
+ "readHiddenBit",
25
+ "validateImage",
26
+ ]
Pixseal/imageSigner.py ADDED
@@ -0,0 +1,206 @@
1
+ from pathlib import Path
2
+ import base64
3
+ from typing import TYPE_CHECKING
4
+
5
+ from cryptography.hazmat.primitives import hashes, serialization
6
+ from cryptography.hazmat.primitives.asymmetric import padding
7
+
8
+ # profiler check
9
+ try:
10
+ from line_profiler import profile
11
+ except ImportError:
12
+
13
+ def profile(func):
14
+ return func
15
+
16
+
17
+ # Dynamic typing
18
+ from .simpleImage import (
19
+ ImageInput as _RuntimeImageInput,
20
+ SimpleImage as _RuntimeSimpleImage,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from .simpleImage_py import ImageInput, SimpleImage
25
+ else:
26
+ ImageInput = _RuntimeImageInput
27
+ SimpleImage = _RuntimeSimpleImage
28
+
29
+
30
+ class BinaryProvider:
31
+
32
+ # Constructor
33
+ def __init__(
34
+ self,
35
+ hiddenString,
36
+ startString="START-VALIDATION\n",
37
+ endString="\nEND-VALIDATION",
38
+ ):
39
+ self.hiddenBits = self._stringToBits(hiddenString)
40
+ self.startBits = self._stringToBits(startString)
41
+ self.endBits = self._stringToBits(endString)
42
+
43
+ # Convert string to contiguous binary digits
44
+ def _stringToBits(self, string):
45
+ bits = []
46
+ for char in string:
47
+ binary = format(ord(char), "08b")
48
+ bits.extend(int(bit) for bit in binary)
49
+ return bits
50
+
51
+ def _expandPayload(self, count: int):
52
+ if count <= 0:
53
+ return []
54
+ payloadLen = len(self.hiddenBits)
55
+ if payloadLen == 0:
56
+ raise ValueError("Hidden payload is empty")
57
+ repeats, remainder = divmod(count, payloadLen)
58
+ return (self.hiddenBits * repeats) + self.hiddenBits[:remainder]
59
+
60
+ def buildBitArray(self, pixelCount: int):
61
+ startLen = len(self.startBits)
62
+ endLen = len(self.endBits)
63
+ if pixelCount < startLen + endLen:
64
+ raise ValueError("Image is too small to fit start/end sentinels")
65
+
66
+ bits = [0] * pixelCount
67
+
68
+ # 1. Place START-VALIDATION bits first
69
+ bits[:startLen] = self.startBits
70
+
71
+ # 2. Fill the remaining slots by repeating the payload bits
72
+ payloadSlots = pixelCount - startLen
73
+ payloadBits = self._expandPayload(payloadSlots)
74
+ bits[startLen:] = payloadBits[:payloadSlots]
75
+
76
+ # 3. Overwrite the tail with END-VALIDATION bits
77
+ tailStart = pixelCount - endLen
78
+ bits[tailStart:] = self.endBits
79
+
80
+ return bits
81
+
82
+
83
+ @profile
84
+ def addHiddenBit(imageInput: ImageInput, hiddenBinary: BinaryProvider):
85
+ img = SimpleImage.open(imageInput)
86
+ width, height = img.size
87
+ pixels = img._pixels # direct buffer access for performance
88
+ total = width * height
89
+ payloadBits = hiddenBinary.buildBitArray(total)
90
+
91
+ # Iterate over every pixel and inject one bit
92
+ for idx in range(total):
93
+ base = idx * 3
94
+ # Read the pixel
95
+ r = pixels[base]
96
+ g = pixels[base + 1]
97
+ b = pixels[base + 2]
98
+
99
+ # Calculate the distance from 127
100
+ diffR = r - 127
101
+ if diffR < 0:
102
+ diffR = -diffR
103
+ diffG = g - 127
104
+ if diffG < 0:
105
+ diffG = -diffG
106
+ diffB = b - 127
107
+ if diffB < 0:
108
+ diffB = -diffB
109
+
110
+ # Pick the component farthest from 127
111
+ maxDiff = diffR
112
+ if diffG > maxDiff:
113
+ maxDiff = diffG
114
+ if diffB > maxDiff:
115
+ maxDiff = diffB
116
+
117
+ # Actual value of that channel
118
+ if maxDiff == diffR:
119
+ targetColorValue = r
120
+ elif maxDiff == diffG:
121
+ targetColorValue = g
122
+ else:
123
+ targetColorValue = b
124
+
125
+ # Channels >=127 are decremented, <127 incremented
126
+ addDirection = 1 if targetColorValue < 127 else -1
127
+
128
+ # Pull next bit from provider
129
+ bit = payloadBits[idx]
130
+
131
+ # Force the selected channel parity to match the bit
132
+ if maxDiff == diffR:
133
+ if r % 2 != bit:
134
+ r += addDirection
135
+ if maxDiff == diffG:
136
+ if g % 2 != bit:
137
+ g += addDirection
138
+ if maxDiff == diffB:
139
+ if b % 2 != bit:
140
+ b += addDirection
141
+
142
+ # Write the updated pixel
143
+ pixels[base] = r
144
+ pixels[base + 1] = g
145
+ pixels[base + 2] = b
146
+
147
+ # Return the modified image
148
+ return img
149
+
150
+
151
+ def stringCryptor(plaintext: str, public_key) -> str:
152
+
153
+ ciphertext = public_key.encrypt(
154
+ plaintext.encode("utf-8"),
155
+ padding.OAEP(
156
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
157
+ algorithm=hashes.SHA256(),
158
+ label=None,
159
+ ),
160
+ )
161
+
162
+ return base64.b64encode(ciphertext).decode("ascii")
163
+
164
+
165
+ # main
166
+ # Image input (path or bytes) + payload string => returns image with embedded payload
167
+ def signImage(imageInput: ImageInput, hiddenString, publicKeyPath=None):
168
+ """
169
+ Embed a payload into an image using the parity-based steganography scheme.
170
+
171
+ Args:
172
+ imageInput: File path, bytes, or file-like object accepted by SimpleImage.
173
+ hiddenString: Text payload that should be written into the image.
174
+ publicKeyPath: Optional path to a PEM-encoded RSA public key used to
175
+ encrypt the payload and sentinel markers before embedding.
176
+
177
+ Returns:
178
+ SimpleImage instance whose pixels include the signed payload.
179
+
180
+ Raises:
181
+ FileNotFoundError: If a public key path is provided but the file is missing.
182
+ ValueError: If the file is not a valid PEM public key.
183
+ """
184
+
185
+ if publicKeyPath: # When encryption key is supplied
186
+ key_path = Path(publicKeyPath)
187
+ if not key_path.is_file():
188
+ raise FileNotFoundError(f"Public key file not found: {publicKeyPath}")
189
+
190
+ pem_data = key_path.read_bytes()
191
+ if b"BEGIN PUBLIC KEY" not in pem_data:
192
+ raise ValueError("Provided file does not contain a valid public key")
193
+
194
+ public_key = serialization.load_pem_public_key(pem_data)
195
+
196
+ hiddenBinary = BinaryProvider(
197
+ hiddenString=stringCryptor(hiddenString, public_key) + "\n",
198
+ startString=stringCryptor("START-VALIDATION", public_key) + "\n",
199
+ endString="\n" + stringCryptor("END-VALIDATION", public_key),
200
+ )
201
+
202
+ else: # Plain-text payload
203
+ hiddenBinary = BinaryProvider(hiddenString + "\n")
204
+
205
+ signedImage = addHiddenBit(imageInput, hiddenBinary)
206
+ return signedImage
@@ -0,0 +1,221 @@
1
+ import base64
2
+ from typing import TYPE_CHECKING
3
+
4
+ from cryptography.hazmat.primitives import hashes, serialization
5
+ from cryptography.hazmat.primitives.asymmetric import padding
6
+
7
+ # profiler check
8
+ try:
9
+ from line_profiler import profile
10
+ except ImportError:
11
+
12
+ def profile(func):
13
+ return func
14
+
15
+
16
+ # Dynamic typing
17
+ from .simpleImage import (
18
+ ImageInput as _RuntimeImageInput,
19
+ SimpleImage as _RuntimeSimpleImage,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from .simpleImage_py import ImageInput, SimpleImage
24
+ else:
25
+ ImageInput = _RuntimeImageInput
26
+ SimpleImage = _RuntimeSimpleImage
27
+
28
+
29
+ def binaryToString(binaryCode):
30
+ string = []
31
+ for i in range(0, len(binaryCode), 8):
32
+ byte = binaryCode[i : i + 8]
33
+ decimal = int(byte, 2)
34
+ character = chr(decimal)
35
+ string.append(character)
36
+ return "".join(string)
37
+
38
+
39
+ @profile
40
+ def readHiddenBit(imageInput: ImageInput):
41
+ img = SimpleImage.open(imageInput)
42
+ width, height = img.size
43
+ pixels = img._pixels # direct buffer access for performance
44
+ total = width * height
45
+ bits = []
46
+ append_bit = bits.append
47
+
48
+ for idx in range(total):
49
+ base = idx * 3
50
+ r = pixels[base]
51
+ g = pixels[base + 1]
52
+ b = pixels[base + 2]
53
+
54
+ diffR = r - 127
55
+ if diffR < 0:
56
+ diffR = -diffR
57
+ diffG = g - 127
58
+ if diffG < 0:
59
+ diffG = -diffG
60
+ diffB = b - 127
61
+ if diffB < 0:
62
+ diffB = -diffB
63
+
64
+ maxDiff = diffR
65
+ if diffG > maxDiff:
66
+ maxDiff = diffG
67
+ if diffB > maxDiff:
68
+ maxDiff = diffB
69
+
70
+ append_bit("1" if maxDiff % 2 == 0 else "0")
71
+
72
+ return "".join(bits)
73
+
74
+
75
+ def deduplicate(arr):
76
+ deduplicated = []
77
+ freq = {}
78
+ most_common = None
79
+ most_count = 0
80
+
81
+ for i, value in enumerate(arr):
82
+ freq[value] = freq.get(value, 0) + 1
83
+ if freq[value] > most_count:
84
+ most_count = freq[value]
85
+ most_common = value
86
+
87
+ if i == 0 or value != arr[i - 1]:
88
+ deduplicated.append(value)
89
+
90
+ return deduplicated, most_common
91
+
92
+
93
+ def tailCheck(arr: list[str]):
94
+ if len(arr) != 4:
95
+ return None # Not required
96
+
97
+ full_cipher = arr[1] # complete ciphertext
98
+ truncated_cipher = arr[2] # incomplete ciphertext
99
+
100
+ return full_cipher.startswith(truncated_cipher)
101
+
102
+
103
+ def buildValidationReport(decrypted, tailCheck: bool, skipPlain: bool = False):
104
+ # Length after deduplication/decryption
105
+ arrayLength = len(decrypted)
106
+
107
+ # 1. Check that the deduplicated sequence length is valid
108
+ lengthCheck = arrayLength in (3, 4)
109
+
110
+ # 2. Validate start/end markers
111
+ startCheck = decrypted[0] == "START-VALIDATION" if decrypted else False
112
+ endCheck = decrypted[-1] == "END-VALIDATION" if decrypted else False
113
+
114
+ # 4. Determine whether payload was successfully decrypted
115
+ decryptedPayload = decrypted[1] if len(decrypted) > 1 else ""
116
+ isDecrypted = bool(decryptedPayload) and not decryptedPayload.endswith("==")
117
+
118
+ checkList = [lengthCheck, startCheck, endCheck, isDecrypted]
119
+ # 5. Parse tailCheck result
120
+ if tailCheck is None:
121
+ tailCheckResult = "Not Required"
122
+ else:
123
+ tailCheckResult = tailCheck
124
+ checkList.append(tailCheckResult)
125
+
126
+ # Overall verdict requires every check to pass
127
+ verdict = all(checkList)
128
+
129
+ result = {
130
+ "arrayLength": arrayLength,
131
+ "lengthCheck": lengthCheck,
132
+ "startCheck": startCheck,
133
+ "endCheck": endCheck,
134
+ "isDecrypted": isDecrypted,
135
+ "tailCheckResult": tailCheckResult,
136
+ "verdict": verdict,
137
+ }
138
+
139
+ if skipPlain:
140
+ result["decryptSkipMessage"] = (
141
+ "Skip decrypt: payload was plain or corrupted text despite decrypt request."
142
+ )
143
+
144
+ return result
145
+
146
+
147
+ def decrypt_array(deduplicated, privKeyPath):
148
+ # Load PEM private key
149
+ with open(privKeyPath, "rb") as key_file:
150
+ private_key = serialization.load_pem_private_key(
151
+ key_file.read(),
152
+ password=None,
153
+ )
154
+
155
+ decrypted = []
156
+ skippedPlainCount = 0
157
+ decryptError = False
158
+ for item in deduplicated:
159
+ if item.endswith("=="):
160
+ try:
161
+ cipher_bytes = base64.b64decode(item)
162
+ plain_bytes = private_key.decrypt(
163
+ cipher_bytes,
164
+ padding.OAEP(
165
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
166
+ algorithm=hashes.SHA256(),
167
+ label=None,
168
+ ),
169
+ )
170
+ decrypted.append(plain_bytes.decode("utf-8"))
171
+ except Exception as exc:
172
+ print(exc)
173
+ decryptError = True
174
+ decrypted.append(item)
175
+ else:
176
+ skippedPlainCount += 1
177
+ decrypted.append(item)
178
+
179
+ expectedPlainCount = 0
180
+ if len(deduplicated) == 4:
181
+ expectedPlainCount = 1
182
+
183
+ skippedPlain = decryptError or skippedPlainCount != expectedPlainCount
184
+
185
+ return decrypted, skippedPlain
186
+
187
+
188
+ # main
189
+ def validateImage(imageInput: ImageInput, privKeyPath=None):
190
+ """
191
+ Extract the embedded payload from an image and optionally decrypt it.
192
+
193
+ Args:
194
+ imageInput: File path, bytes, or file-like object accepted by SimpleImage.
195
+ privKeyPath: Optional path to a PEM-encoded RSA private key used to
196
+ decrypt the extracted ciphertext.
197
+
198
+ Returns:
199
+ Dict with the most common extracted string, decrypted sequence, and
200
+ a validation report describing the sentinel checks and verdict.
201
+ """
202
+ resultBinary = readHiddenBit(imageInput)
203
+ resultString = binaryToString(resultBinary)
204
+ splited = resultString.split("\n")
205
+ deduplicated, most_common = deduplicate(splited)
206
+
207
+ if privKeyPath:
208
+ decrypted, skippedPlain = decrypt_array(deduplicated, privKeyPath)
209
+ else:
210
+ decrypted = deduplicated
211
+ skippedPlain = False
212
+
213
+ report = buildValidationReport(
214
+ decrypted=decrypted, tailCheck=tailCheck(deduplicated), skipPlain=skippedPlain
215
+ )
216
+
217
+ return {
218
+ "extractedString": decrypt_array({most_common}, privKeyPath)[0][0],
219
+ "decrypted": decrypted,
220
+ "validationReport": report,
221
+ }
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"]