Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,28 +1,106 @@
|
|
1 |
"""
|
2 |
Secure KeyLock Decoder API Server
|
3 |
|
4 |
-
This script deploys a secure Gradio application that acts as a server-side API
|
5 |
for decrypting and retrieving JSON data hidden within PNG images.
|
6 |
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
"""
|
27 |
|
28 |
import os
|
@@ -61,7 +139,7 @@ try:
|
|
61 |
logger.info("Successfully loaded public key from keylock_pub.pem for display.")
|
62 |
except FileNotFoundError:
|
63 |
logger.error("FATAL: keylock_pub.pem not found. The UI will show an error.")
|
64 |
-
PUBLIC_KEY_PEM_STRING = "Error: keylock_pub.pem file not found on the server."
|
65 |
except Exception as e:
|
66 |
logger.error(f"Error loading public key: {e}")
|
67 |
PUBLIC_KEY_PEM_STRING = f"Error loading public key: {e}"
|
@@ -90,21 +168,12 @@ def decode_data(image_base64_string: str) -> dict:
|
|
90 |
------------------------------------------------------------------------------------------
|
91 |
... | AES Nonce (12 bytes) | AES-GCM Ciphertext + Tag (remaining bytes) |
|
92 |
------------------------------------------------------------------------
|
93 |
-
|
94 |
-
The process is as follows:
|
95 |
-
1. Read the first 32 LSBs to get the total length of the crypto payload.
|
96 |
-
2. Extract the entire crypto payload from the image's LSBs.
|
97 |
-
3. Unpack the crypto payload:
|
98 |
-
a. Decrypt the RSA-encrypted AES key using the server's private key.
|
99 |
-
b. Extract the nonce.
|
100 |
-
c. Use the recovered AES key and nonce to decrypt the final ciphertext.
|
101 |
-
4. Decode the decrypted bytes (UTF-8) and parse them as JSON.
|
102 |
"""
|
103 |
if not KEYLOCK_PRIV_KEY:
|
104 |
error_msg = "Server Error: The API is not configured with a private key. Please contact the administrator."
|
105 |
logger.error(error_msg)
|
106 |
raise gr.Error(error_msg)
|
107 |
-
|
108 |
try:
|
109 |
# 1. Decode Base64 and load image data
|
110 |
image_buffer = base64.b64decode(image_base64_string)
|
@@ -114,11 +183,10 @@ def decode_data(image_base64_string: str) -> dict:
|
|
114 |
# 2. Extract data length from the LSB header
|
115 |
if pixel_data.size < HEADER_BITS:
|
116 |
raise ValueError(f"Image is too small to contain a valid header. Minimum pixel count: {HEADER_BITS}")
|
117 |
-
|
118 |
header_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[:HEADER_BITS])
|
119 |
data_length = int(header_binary_string, 2)
|
120 |
|
121 |
-
# If length is 0, it's a valid image with no data.
|
122 |
if data_length == 0:
|
123 |
logger.info("Image decoded with zero data length. Returning empty dict.")
|
124 |
return {}
|
@@ -128,42 +196,36 @@ def decode_data(image_base64_string: str) -> dict:
|
|
128 |
end_offset = HEADER_BITS + data_bits_count
|
129 |
if pixel_data.size < end_offset:
|
130 |
raise ValueError("Image data corrupt or truncated. The actual image size is smaller than specified in the header.")
|
131 |
-
|
132 |
data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
|
133 |
crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
|
134 |
-
|
135 |
# 4. Load private key and unpack the crypto payload
|
136 |
private_key = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY.encode(), password=None)
|
137 |
-
|
138 |
-
# Unpack Encrypted AES Key
|
139 |
offset = 4 # First 4 bytes define the length of the encrypted AES key
|
140 |
encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
|
141 |
-
|
142 |
-
# Sanity check: ensure the key length matches the server's RSA key size
|
143 |
rsa_key_size_bytes = private_key.key_size // 8
|
144 |
if encrypted_aes_key_len != rsa_key_size_bytes:
|
145 |
raise ValueError(f"Key mismatch: The data was encrypted with a {encrypted_aes_key_len*8}-bit key, but the server uses a {private_key.key_size}-bit key.")
|
146 |
|
147 |
encrypted_aes_key = crypto_payload[offset : offset + encrypted_aes_key_len]
|
148 |
offset += encrypted_aes_key_len
|
149 |
-
|
150 |
-
# Unpack Nonce
|
151 |
nonce = crypto_payload[offset : offset + AES_GCM_NONCE_SIZE]
|
152 |
offset += AES_GCM_NONCE_SIZE
|
153 |
-
|
154 |
-
# The rest is the ciphertext + authentication tag
|
155 |
ciphertext_with_tag = crypto_payload[offset:]
|
156 |
|
157 |
# 5. Decrypt the data
|
158 |
-
# a. Decrypt the AES key with the private RSA key
|
159 |
recovered_aes_key = private_key.decrypt(
|
160 |
encrypted_aes_key,
|
161 |
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
|
162 |
)
|
163 |
-
# b. Decrypt the payload with the recovered AES key
|
164 |
aesgcm = AESGCM(recovered_aes_key)
|
165 |
decrypted_bytes = aesgcm.decrypt(nonce, ciphertext_with_tag, None)
|
166 |
-
|
167 |
logger.info("Successfully decoded and decrypted a payload from an image.")
|
168 |
return json.loads(decrypted_bytes.decode('utf-8'))
|
169 |
|
@@ -179,31 +241,28 @@ def decode_data(image_base64_string: str) -> dict:
|
|
179 |
# ==============================================================================
|
180 |
with gr.Blocks(title="Secure Decoder API", theme=gr.themes.Soft()) as demo:
|
181 |
gr.Markdown("# 🔐 Secure KeyLock Decoder API")
|
182 |
-
gr.Markdown("This application provides a secure API endpoint to decrypt and extract JSON data hidden within PNG images.")
|
183 |
|
184 |
with gr.Tabs():
|
185 |
with gr.TabItem("🚀 Quick Start & Documentation"):
|
186 |
-
|
187 |
gr.Markdown("## What Is This?")
|
188 |
gr.Markdown(
|
189 |
"This service decodes messages that have been securely hidden inside images. It's designed for use cases where you need to transmit a small, sensitive JSON payload (like authentication tokens, user data, or configuration) "
|
190 |
"without it being easily detectable or readable. The process uses a combination of **steganography** (hiding data in the image's pixels) and **hybrid encryption** (the security of RSA combined with the speed of AES)."
|
191 |
)
|
192 |
|
193 |
-
with gr.Accordion("How It Works", open=False):
|
194 |
gr.Markdown(
|
195 |
"""
|
196 |
The security relies on a well-established cryptographic pattern called **Hybrid Encryption**. Here’s the step-by-step process a client must follow to create a compatible image:
|
197 |
-
1. **Client Generates a One-Time Key**: The client creates a random, single-use 32-byte symmetric key for AES-GCM encryption.
|
198 |
-
2. **Client Encrypts Data**: The client takes the JSON payload
|
199 |
-
3. **Client Encrypts the One-Time Key**:
|
200 |
-
4. **Client Creates the Payload**: The client bundles
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
5. **Client Hides the Payload**: The client uses **Least Significant Bit (LSB) steganography** to hide the binary payload in the pixels of a template PNG image. It also writes a 32-bit header specifying the payload's total length.
|
205 |
-
6. **Client Sends Image**: The final image, which looks normal to the human eye, is sent to this API endpoint.
|
206 |
-
7. **Server Decodes**: This server reverses the process: it extracts the binary payload from the LSBs, uses its private RSA key to decrypt the one-time AES key, and then uses that key to decrypt the final message.
|
207 |
"""
|
208 |
)
|
209 |
|
@@ -211,70 +270,36 @@ with gr.Blocks(title="Secure Decoder API", theme=gr.themes.Soft()) as demo:
|
|
211 |
gr.Markdown("## How to Use This API")
|
212 |
gr.Markdown("### Step 1: Get the Server's Public Key")
|
213 |
gr.Markdown("You must use this public key to encrypt data intended for this server. Save it to a file (e.g., `server_pub.pem`).")
|
214 |
-
gr.Code(value=PUBLIC_KEY_PEM_STRING, language="
|
215 |
-
|
216 |
with gr.Accordion("Step 2: Create the Encrypted Image (Client-Side Python Example)", open=True):
|
217 |
-
gr.Markdown("Use the following Python code in your client application to create a KeyLock image.
|
218 |
gr.Code(
|
219 |
language="python",
|
220 |
label="Client-Side Image Creation Script (create_keylock_image.py)",
|
221 |
value="""
|
222 |
-
import json
|
223 |
-
import os
|
224 |
-
import struct
|
225 |
-
from PIL import Image
|
226 |
-
import numpy as np
|
227 |
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
228 |
-
from cryptography.hazmat.primitives import hashes
|
229 |
-
from cryptography.hazmat.primitives import serialization
|
230 |
from cryptography.hazmat.primitives.asymmetric import padding
|
231 |
|
232 |
def create_keylock_image(public_key_pem: str, json_data: dict, template_image_path: str, output_path: str):
|
233 |
-
\"\"\"
|
234 |
-
Encrypts a JSON payload and hides it in a template image using hybrid encryption.
|
235 |
-
\"\"\"
|
236 |
-
# 1. Load the server's public key
|
237 |
public_key = serialization.load_pem_public_key(public_key_pem.encode())
|
238 |
-
|
239 |
-
# 2. Prepare the data and generate a one-time AES key
|
240 |
data_to_encrypt = json.dumps(json_data).encode('utf-8')
|
241 |
aes_key = AESGCM.generate_key(bit_length=256)
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
# 3. Encrypt the data with AES-GCM
|
246 |
-
ciphertext_with_tag = aesgcm.encrypt(nonce, data_to_encrypt, None)
|
247 |
-
|
248 |
-
# 4. Encrypt the one-time AES key with the server's public RSA key
|
249 |
-
encrypted_aes_key = public_key.encrypt(
|
250 |
-
aes_key,
|
251 |
-
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
|
252 |
-
)
|
253 |
-
|
254 |
-
# 5. Assemble the final crypto payload
|
255 |
-
# Format: [length of encrypted_aes_key (4 bytes)] [encrypted_aes_key] [nonce (12 bytes)] [ciphertext]
|
256 |
payload = struct.pack('>I', len(encrypted_aes_key)) + encrypted_aes_key + nonce + ciphertext_with_tag
|
257 |
-
|
258 |
-
# 6. Hide the payload in the image using LSB steganography
|
259 |
img = Image.open(template_image_path).convert("RGB")
|
260 |
pixel_data = np.array(img)
|
261 |
-
|
262 |
-
payload_binary_string = ''.join(f'{byte:08b}' for byte in payload)
|
263 |
-
header_binary_string = f'{len(payload):032b}' # 32-bit header for payload length
|
264 |
-
full_binary_string = header_binary_string + payload_binary_string
|
265 |
-
|
266 |
if len(full_binary_string) > pixel_data.size:
|
267 |
-
raise ValueError(f"Data is too large
|
268 |
-
|
269 |
flat_pixels = pixel_data.ravel()
|
270 |
for i in range(len(full_binary_string)):
|
271 |
-
|
272 |
-
|
273 |
-
# Modify the least significant bit
|
274 |
-
flat_pixels[i] = (pixel_val & 0b11111110) | bit_val
|
275 |
-
|
276 |
-
new_img = Image.fromarray(pixel_data)
|
277 |
-
new_img.save(output_path, "PNG")
|
278 |
print(f"Successfully created encrypted image at {output_path}")
|
279 |
|
280 |
# --- Example Usage ---
|
@@ -282,21 +307,9 @@ if __name__ == '__main__':
|
|
282 |
SERVER_PUBLIC_KEY = \"\"\"-----BEGIN PUBLIC KEY-----
|
283 |
YOUR_SERVER_PUBLIC_KEY_HERE
|
284 |
-----END PUBLIC KEY-----\"\"\"
|
285 |
-
|
286 |
-
my_secret_data
|
287 |
-
|
288 |
-
"token": "abcdef-123456-ghijkl-789012",
|
289 |
-
"permissions": ["read", "write"]
|
290 |
-
}
|
291 |
-
|
292 |
-
# Ensure you have a 'template.png' in the same directory
|
293 |
-
create_keylock_image(
|
294 |
-
public_key_pem=SERVER_PUBLIC_KEY,
|
295 |
-
json_data=my_secret_data,
|
296 |
-
template_image_path="template.png",
|
297 |
-
output_path="encrypted_image.png"
|
298 |
-
)
|
299 |
-
"""
|
300 |
)
|
301 |
|
302 |
gr.Markdown("### Step 3: Call the API Endpoint")
|
@@ -306,7 +319,7 @@ YOUR_SERVER_PUBLIC_KEY_HERE
|
|
306 |
"- **Body**: A JSON object containing a base64-encoded string of the image."
|
307 |
)
|
308 |
gr.Code(language="json", label="JSON Payload Schema", value='{\n "data": [\n "<base64_encoded_image_string>"\n ]\n}')
|
309 |
-
|
310 |
with gr.Accordion("Python `requests` Example", open=True):
|
311 |
gr.Code(
|
312 |
language="python",
|
@@ -315,67 +328,34 @@ YOUR_SERVER_PUBLIC_KEY_HERE
|
|
315 |
import requests
|
316 |
import base64
|
317 |
|
318 |
-
# Assumes you have created 'encrypted_image.png' using the script from Step 2
|
319 |
IMAGE_PATH = "encrypted_image.png"
|
320 |
API_URL = "https://YOUR-SPACE-NAME.hf.space/run/keylock-auth-decoder" # <-- Change this URL
|
321 |
|
322 |
-
# Read image and encode it to base64
|
323 |
with open(IMAGE_PATH, "rb") as f:
|
324 |
b64_string = base64.b64encode(f.read()).decode('utf-8')
|
325 |
|
326 |
-
|
327 |
-
payload = {
|
328 |
-
"data": [b64_string]
|
329 |
-
}
|
330 |
-
|
331 |
-
# Send the request
|
332 |
-
response = requests.post(API_URL, json=payload)
|
333 |
|
334 |
if response.status_code == 200:
|
335 |
-
|
336 |
-
print("Success! Decrypted data:")
|
337 |
-
print(result['data'][0])
|
338 |
else:
|
339 |
-
print(f"Error: {response.status_code}")
|
340 |
-
print(response.text)
|
341 |
"""
|
342 |
)
|
343 |
|
344 |
-
with gr.Accordion("cURL Example", open=False):
|
345 |
-
gr.Code(language="python", label="Example using cURL",
|
346 |
-
value="""
|
347 |
-
# 1. Encode your image to base64 and store it in a variable
|
348 |
-
# On macOS: B64_STRING=$(base64 /path/to/encrypted_image.png)
|
349 |
-
# On Linux: B64_STRING=$(base64 -w 0 /path/to/encrypted_image.png)
|
350 |
-
|
351 |
-
# 2. Call the API
|
352 |
-
curl -X POST \\
|
353 |
-
-H "Content-Type: application/json" \\
|
354 |
-
-d '{"data": ["'$B64_STRING'"]}' \\
|
355 |
-
https://YOUR-SPACE-NAME.hf.space/run/keylock-auth-decoder
|
356 |
-
"""
|
357 |
-
)
|
358 |
-
|
359 |
with gr.TabItem("⚙️ Server Status & Error Guide"):
|
360 |
gr.Markdown("## Server Status")
|
361 |
-
key_status = "✅ Loaded successfully from secrets." if KEYLOCK_PRIV_KEY else "❌ NOT FOUND. The API is non-functional. The administrator must set the `KEYLOCK_PRIV_KEY` secret in the Space settings."
|
362 |
-
gr.Textbox(label="Private Key Status", value=key_status, interactive=False, lines=
|
363 |
-
|
364 |
gr.Markdown("---")
|
365 |
gr.Markdown("## Error Handling Guide")
|
366 |
gr.Markdown(
|
367 |
"""
|
368 |
-
|
369 |
-
|
370 |
-
- **`"
|
371 |
-
|
372 |
-
2. The image file was corrupted during transfer or storage, altering the hidden data.
|
373 |
-
|
374 |
-
- **`"Decryption failed. ... Key mismatch"`**: The RSA key size used to encrypt the data (e.g., 2048-bit) does not match the server's key size (e.g., 4096-bit). Ensure you are using the exact public key provided by this service.
|
375 |
-
|
376 |
-
- **`"Image data corrupt or truncated..."`**: The header inside the image indicates a certain data length, but the image is not large enough to contain that much data. The file is likely incomplete.
|
377 |
-
|
378 |
-
- **`"Server Error: ... not configured"`**: This is a server-side problem. The administrator has not correctly configured the private key secret for this Gradio Space.
|
379 |
"""
|
380 |
)
|
381 |
|
@@ -384,13 +364,13 @@ curl -X POST \\
|
|
384 |
api_input = gr.Textbox()
|
385 |
api_output = gr.JSON()
|
386 |
gr.Interface(
|
387 |
-
fn=decode_data,
|
388 |
-
inputs=api_input,
|
389 |
-
outputs=api_output,
|
390 |
api_name="keylock-auth-decoder",
|
391 |
-
# No need for title/description here as it's a hidden API generator
|
392 |
)
|
393 |
|
394 |
if __name__ == "__main__":
|
395 |
-
#
|
|
|
396 |
demo.launch()
|
|
|
1 |
"""
|
2 |
Secure KeyLock Decoder API Server
|
3 |
|
4 |
+
This script deploys a secure Gradio application that acts as a server-side API
|
5 |
for decrypting and retrieving JSON data hidden within PNG images.
|
6 |
|
7 |
+
================================================================================
|
8 |
+
▶️ DEPLOYMENT GUIDE
|
9 |
+
================================================================================
|
10 |
+
|
11 |
+
---
|
12 |
+
OPTION 1: DEPLOY TO HUGGING FACE SPACES (RECOMMENDED)
|
13 |
+
---
|
14 |
+
This is the easiest and most secure way to deploy this application.
|
15 |
+
|
16 |
+
1. **Generate RSA Keys:**
|
17 |
+
First, you need a private/public RSA key pair. Use OpenSSL on your local machine:
|
18 |
+
```bash
|
19 |
+
# Generate a 4096-bit private key (stronger)
|
20 |
+
openssl genpkey -algorithm RSA -out keylock_priv.pem -pkeyopt rsa_keygen_bits:4096
|
21 |
+
|
22 |
+
# Extract the public key from the private key
|
23 |
+
openssl rsa -pubout -in keylock_priv.pem -out keylock_pub.pem
|
24 |
+
```
|
25 |
+
This will create two files: `keylock_priv.pem` (keep this secret!) and `keylock_pub.pem` (this is safe to share).
|
26 |
+
|
27 |
+
2. **Create a Hugging Face Space:**
|
28 |
+
- Go to Hugging Face and create a new "Space".
|
29 |
+
- Choose the "Gradio" SDK.
|
30 |
+
- Give it a name (e.g., "my-keylock-decoder").
|
31 |
+
|
32 |
+
3. **Upload Files to the Space Repository:**
|
33 |
+
- Rename this script to `app.py`.
|
34 |
+
- Create a `requirements.txt` file with the following content:
|
35 |
+
```
|
36 |
+
gradio
|
37 |
+
numpy
|
38 |
+
Pillow
|
39 |
+
cryptography
|
40 |
+
```
|
41 |
+
- Upload `app.py`, `requirements.txt`, and the public key `keylock_pub.pem` to your Space's repository.
|
42 |
+
- **DO NOT UPLOAD THE PRIVATE KEY (`keylock_priv.pem`)!**
|
43 |
+
|
44 |
+
4. **Set the Private Key as a Secret:**
|
45 |
+
- In your Space, go to the "Settings" tab.
|
46 |
+
- Find the "Repository secrets" section.
|
47 |
+
- Click "New secret".
|
48 |
+
- **Name:** `KEYLOCK_PRIV_KEY` (this name must be exact).
|
49 |
+
- **Value:** Open `keylock_priv.pem` on your local machine, copy its ENTIRE content (including `-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----`), and paste it into the value field.
|
50 |
+
- The application will now automatically and securely load this key at runtime.
|
51 |
+
|
52 |
+
---
|
53 |
+
OPTION 2: RUN LOCALLY FOR DEVELOPMENT
|
54 |
+
---
|
55 |
+
Use this for testing on your own computer.
|
56 |
+
|
57 |
+
1. **Generate Keys:** Follow Step 1 from the Hugging Face guide.
|
58 |
+
|
59 |
+
2. **Install Dependencies:**
|
60 |
+
```bash
|
61 |
+
pip install gradio numpy Pillow cryptography
|
62 |
+
```
|
63 |
+
|
64 |
+
3. **Set Environment Variable:**
|
65 |
+
You must provide the private key as an environment variable.
|
66 |
+
- Open `keylock_priv.pem`, copy its entire content into your clipboard.
|
67 |
+
- In your terminal (Linux/macOS):
|
68 |
+
```bash
|
69 |
+
export KEYLOCK_PRIV_KEY='PASTE_THE_ENTIRE_KEY_CONTENT_HERE'
|
70 |
+
python app.py
|
71 |
+
```
|
72 |
+
- In Windows PowerShell:
|
73 |
+
```powershell
|
74 |
+
$env:KEYLOCK_PRIV_KEY='PASTE_THE_ENTIRE_KEY_CONTENT_HERE'
|
75 |
+
python app.py
|
76 |
+
```
|
77 |
+
|
78 |
+
4. **Run the Script:** The app will be available at `http://127.0.0.1:7860`.
|
79 |
+
|
80 |
+
---
|
81 |
+
OPTION 3: DEPLOY TO A SELF-HOSTED SERVER
|
82 |
+
---
|
83 |
+
For advanced users deploying on their own VPS or server.
|
84 |
+
|
85 |
+
1. **Generate Keys & Install Dependencies:** Follow steps 1 & 2 from the local guide.
|
86 |
+
|
87 |
+
2. **Launch the App:**
|
88 |
+
Modify the `demo.launch()` line at the bottom of this script to bind to all network interfaces:
|
89 |
+
`demo.launch(server_name="0.0.0.0", server_port=7860)`
|
90 |
+
|
91 |
+
3. **Manage Environment Variable:**
|
92 |
+
Set the `KEYLOCK_PRIV_KEY` environment variable using a production-safe method like a `.env` file with `python-dotenv`, systemd service files, or your container orchestration platform (e.g., Docker, Kubernetes).
|
93 |
+
|
94 |
+
4. **Use a Reverse Proxy (CRITICAL):**
|
95 |
+
Do not expose the Gradio port directly to the internet. Place the application behind a reverse proxy like Nginx or Caddy. The proxy will handle SSL/TLS termination (HTTPS), provide better security, and manage traffic.
|
96 |
+
|
97 |
+
================================================================================
|
98 |
+
|
99 |
+
This application implements a hybrid security model:
|
100 |
+
1. **Steganography (LSB):** The encrypted payload is hidden in the least significant
|
101 |
+
bits (LSB) of the image's pixel data.
|
102 |
+
2. **Hybrid Encryption (RSA-KEM + AES-GCM):** The actual JSON payload is encrypted
|
103 |
+
with a one-time AES key, which itself is encrypted with the server's RSA public key.
|
104 |
"""
|
105 |
|
106 |
import os
|
|
|
139 |
logger.info("Successfully loaded public key from keylock_pub.pem for display.")
|
140 |
except FileNotFoundError:
|
141 |
logger.error("FATAL: keylock_pub.pem not found. The UI will show an error.")
|
142 |
+
PUBLIC_KEY_PEM_STRING = "Error: keylock_pub.pem file not found on the server. Make sure it's in your repository."
|
143 |
except Exception as e:
|
144 |
logger.error(f"Error loading public key: {e}")
|
145 |
PUBLIC_KEY_PEM_STRING = f"Error loading public key: {e}"
|
|
|
168 |
------------------------------------------------------------------------------------------
|
169 |
... | AES Nonce (12 bytes) | AES-GCM Ciphertext + Tag (remaining bytes) |
|
170 |
------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
"""
|
172 |
if not KEYLOCK_PRIV_KEY:
|
173 |
error_msg = "Server Error: The API is not configured with a private key. Please contact the administrator."
|
174 |
logger.error(error_msg)
|
175 |
raise gr.Error(error_msg)
|
176 |
+
|
177 |
try:
|
178 |
# 1. Decode Base64 and load image data
|
179 |
image_buffer = base64.b64decode(image_base64_string)
|
|
|
183 |
# 2. Extract data length from the LSB header
|
184 |
if pixel_data.size < HEADER_BITS:
|
185 |
raise ValueError(f"Image is too small to contain a valid header. Minimum pixel count: {HEADER_BITS}")
|
186 |
+
|
187 |
header_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[:HEADER_BITS])
|
188 |
data_length = int(header_binary_string, 2)
|
189 |
|
|
|
190 |
if data_length == 0:
|
191 |
logger.info("Image decoded with zero data length. Returning empty dict.")
|
192 |
return {}
|
|
|
196 |
end_offset = HEADER_BITS + data_bits_count
|
197 |
if pixel_data.size < end_offset:
|
198 |
raise ValueError("Image data corrupt or truncated. The actual image size is smaller than specified in the header.")
|
199 |
+
|
200 |
data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
|
201 |
crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
|
202 |
+
|
203 |
# 4. Load private key and unpack the crypto payload
|
204 |
private_key = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY.encode(), password=None)
|
205 |
+
|
|
|
206 |
offset = 4 # First 4 bytes define the length of the encrypted AES key
|
207 |
encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
|
208 |
+
|
|
|
209 |
rsa_key_size_bytes = private_key.key_size // 8
|
210 |
if encrypted_aes_key_len != rsa_key_size_bytes:
|
211 |
raise ValueError(f"Key mismatch: The data was encrypted with a {encrypted_aes_key_len*8}-bit key, but the server uses a {private_key.key_size}-bit key.")
|
212 |
|
213 |
encrypted_aes_key = crypto_payload[offset : offset + encrypted_aes_key_len]
|
214 |
offset += encrypted_aes_key_len
|
215 |
+
|
|
|
216 |
nonce = crypto_payload[offset : offset + AES_GCM_NONCE_SIZE]
|
217 |
offset += AES_GCM_NONCE_SIZE
|
218 |
+
|
|
|
219 |
ciphertext_with_tag = crypto_payload[offset:]
|
220 |
|
221 |
# 5. Decrypt the data
|
|
|
222 |
recovered_aes_key = private_key.decrypt(
|
223 |
encrypted_aes_key,
|
224 |
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
|
225 |
)
|
|
|
226 |
aesgcm = AESGCM(recovered_aes_key)
|
227 |
decrypted_bytes = aesgcm.decrypt(nonce, ciphertext_with_tag, None)
|
228 |
+
|
229 |
logger.info("Successfully decoded and decrypted a payload from an image.")
|
230 |
return json.loads(decrypted_bytes.decode('utf-8'))
|
231 |
|
|
|
241 |
# ==============================================================================
|
242 |
with gr.Blocks(title="Secure Decoder API", theme=gr.themes.Soft()) as demo:
|
243 |
gr.Markdown("# 🔐 Secure KeyLock Decoder API")
|
244 |
+
gr.Markdown("This application provides a secure API endpoint to decrypt and extract JSON data hidden within PNG images. **See the deployment guide at the top of `app.py` for setup instructions.**")
|
245 |
|
246 |
with gr.Tabs():
|
247 |
with gr.TabItem("🚀 Quick Start & Documentation"):
|
248 |
+
|
249 |
gr.Markdown("## What Is This?")
|
250 |
gr.Markdown(
|
251 |
"This service decodes messages that have been securely hidden inside images. It's designed for use cases where you need to transmit a small, sensitive JSON payload (like authentication tokens, user data, or configuration) "
|
252 |
"without it being easily detectable or readable. The process uses a combination of **steganography** (hiding data in the image's pixels) and **hybrid encryption** (the security of RSA combined with the speed of AES)."
|
253 |
)
|
254 |
|
255 |
+
with gr.Accordion("How It Works (The Gory Details)", open=False):
|
256 |
gr.Markdown(
|
257 |
"""
|
258 |
The security relies on a well-established cryptographic pattern called **Hybrid Encryption**. Here’s the step-by-step process a client must follow to create a compatible image:
|
259 |
+
1. **Client Generates a One-Time Key**: The client creates a random, single-use 32-byte symmetric key for AES-GCM encryption.
|
260 |
+
2. **Client Encrypts Data**: The client takes the JSON payload and encrypts it using the one-time AES key.
|
261 |
+
3. **Client Encrypts the One-Time Key**: The client encrypts the AES key using this server's **Public RSA Key** (provided below).
|
262 |
+
4. **Client Creates the Payload**: The client bundles the encrypted AES key, nonce, and ciphertext into a single binary payload.
|
263 |
+
5. **Client Hides the Payload**: The client uses **Least Significant Bit (LSB) steganography** to hide the payload in a template image.
|
264 |
+
6. **Client Sends Image**: The final image is sent to this API endpoint.
|
265 |
+
7. **Server Decodes**: This server reverses the process: it extracts the payload, uses its private RSA key to decrypt the AES key, and then decrypts the final message.
|
|
|
|
|
|
|
266 |
"""
|
267 |
)
|
268 |
|
|
|
270 |
gr.Markdown("## How to Use This API")
|
271 |
gr.Markdown("### Step 1: Get the Server's Public Key")
|
272 |
gr.Markdown("You must use this public key to encrypt data intended for this server. Save it to a file (e.g., `server_pub.pem`).")
|
273 |
+
gr.Code(value=PUBLIC_KEY_PEM_STRING, language="pem", label="Server Public Key")
|
274 |
+
|
275 |
with gr.Accordion("Step 2: Create the Encrypted Image (Client-Side Python Example)", open=True):
|
276 |
+
gr.Markdown("Use the following Python code in your client application to create a KeyLock image.")
|
277 |
gr.Code(
|
278 |
language="python",
|
279 |
label="Client-Side Image Creation Script (create_keylock_image.py)",
|
280 |
value="""
|
281 |
+
import json; import os; import struct; from PIL import Image; import numpy as np
|
|
|
|
|
|
|
|
|
282 |
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
283 |
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
|
284 |
from cryptography.hazmat.primitives.asymmetric import padding
|
285 |
|
286 |
def create_keylock_image(public_key_pem: str, json_data: dict, template_image_path: str, output_path: str):
|
|
|
|
|
|
|
|
|
287 |
public_key = serialization.load_pem_public_key(public_key_pem.encode())
|
|
|
|
|
288 |
data_to_encrypt = json.dumps(json_data).encode('utf-8')
|
289 |
aes_key = AESGCM.generate_key(bit_length=256)
|
290 |
+
nonce = os.urandom(12)
|
291 |
+
ciphertext_with_tag = AESGCM(aes_key).encrypt(nonce, data_to_encrypt, None)
|
292 |
+
encrypted_aes_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
293 |
payload = struct.pack('>I', len(encrypted_aes_key)) + encrypted_aes_key + nonce + ciphertext_with_tag
|
|
|
|
|
294 |
img = Image.open(template_image_path).convert("RGB")
|
295 |
pixel_data = np.array(img)
|
296 |
+
full_binary_string = f'{len(payload):032b}' + ''.join(f'{byte:08b}' for byte in payload)
|
|
|
|
|
|
|
|
|
297 |
if len(full_binary_string) > pixel_data.size:
|
298 |
+
raise ValueError(f"Data is too large. Needs {len(full_binary_string)} pixels, image has {pixel_data.size}.")
|
|
|
299 |
flat_pixels = pixel_data.ravel()
|
300 |
for i in range(len(full_binary_string)):
|
301 |
+
flat_pixels[i] = (flat_pixels[i] & 0b11111110) | int(full_binary_string[i])
|
302 |
+
Image.fromarray(pixel_data).save(output_path, "PNG")
|
|
|
|
|
|
|
|
|
|
|
303 |
print(f"Successfully created encrypted image at {output_path}")
|
304 |
|
305 |
# --- Example Usage ---
|
|
|
307 |
SERVER_PUBLIC_KEY = \"\"\"-----BEGIN PUBLIC KEY-----
|
308 |
YOUR_SERVER_PUBLIC_KEY_HERE
|
309 |
-----END PUBLIC KEY-----\"\"\"
|
310 |
+
my_secret_data = {"user_id": "user-12345", "token": "abcdef-123456"}
|
311 |
+
create_keylock_image(SERVER_PUBLIC_KEY, my_secret_data, "template.png", "encrypted_image.png")
|
312 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
)
|
314 |
|
315 |
gr.Markdown("### Step 3: Call the API Endpoint")
|
|
|
319 |
"- **Body**: A JSON object containing a base64-encoded string of the image."
|
320 |
)
|
321 |
gr.Code(language="json", label="JSON Payload Schema", value='{\n "data": [\n "<base64_encoded_image_string>"\n ]\n}')
|
322 |
+
|
323 |
with gr.Accordion("Python `requests` Example", open=True):
|
324 |
gr.Code(
|
325 |
language="python",
|
|
|
328 |
import requests
|
329 |
import base64
|
330 |
|
|
|
331 |
IMAGE_PATH = "encrypted_image.png"
|
332 |
API_URL = "https://YOUR-SPACE-NAME.hf.space/run/keylock-auth-decoder" # <-- Change this URL
|
333 |
|
|
|
334 |
with open(IMAGE_PATH, "rb") as f:
|
335 |
b64_string = base64.b64encode(f.read()).decode('utf-8')
|
336 |
|
337 |
+
response = requests.post(API_URL, json={"data": [b64_string]})
|
|
|
|
|
|
|
|
|
|
|
|
|
338 |
|
339 |
if response.status_code == 200:
|
340 |
+
print("Success! Decrypted data:", response.json()['data'][0])
|
|
|
|
|
341 |
else:
|
342 |
+
print(f"Error: {response.status_code}", response.text)
|
|
|
343 |
"""
|
344 |
)
|
345 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
with gr.TabItem("⚙️ Server Status & Error Guide"):
|
347 |
gr.Markdown("## Server Status")
|
348 |
+
key_status = "✅ Loaded successfully from secrets." if KEYLOCK_PRIV_KEY else "❌ NOT FOUND. The API is non-functional. The administrator must set the `KEYLOCK_PRIV_KEY` secret in the Space settings. See the deployment guide in `app.py`."
|
349 |
+
gr.Textbox(label="Private Key Status", value=key_status, interactive=False, lines=3)
|
350 |
+
|
351 |
gr.Markdown("---")
|
352 |
gr.Markdown("## Error Handling Guide")
|
353 |
gr.Markdown(
|
354 |
"""
|
355 |
+
- **`"Decryption failed. ... InvalidTag"`**: The most common error. It means the image was encrypted with a **different public key** or the image file was corrupted.
|
356 |
+
- **`"Decryption failed. ... Key mismatch"`**: The RSA key size used to encrypt the data does not match the server's key size. Use the public key provided by this service.
|
357 |
+
- **`"Image data corrupt or truncated..."`**: The image file is incomplete or damaged.
|
358 |
+
- **`"Server Error: ... not configured"`**: A server-side problem. The administrator has not set the `KEYLOCK_PRIV_KEY` secret correctly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
359 |
"""
|
360 |
)
|
361 |
|
|
|
364 |
api_input = gr.Textbox()
|
365 |
api_output = gr.JSON()
|
366 |
gr.Interface(
|
367 |
+
fn=decode_data,
|
368 |
+
inputs=api_input,
|
369 |
+
outputs=api_output,
|
370 |
api_name="keylock-auth-decoder",
|
|
|
371 |
)
|
372 |
|
373 |
if __name__ == "__main__":
|
374 |
+
# To run on a server, you might use: demo.launch(server_name="0.0.0.0")
|
375 |
+
# See the deployment guide at the top of this file for more details.
|
376 |
demo.launch()
|