broadfield-dev commited on
Commit
9844f6e
·
verified ·
1 Parent(s): 7e96e67

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -169
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
- It implements a hybrid security model:
8
- 1. **Steganography (LSB):** The encrypted payload is hidden in the least significant
9
- bits (LSB) of the image's pixel data. This makes the data-carrying image
10
- visually indistinguishable from the original.
11
- 2. **Hybrid Encryption (RSA-KEM + AES-GCM):**
12
- - A one-time, symmetric AES-GCM key is generated to encrypt the actual JSON payload.
13
- - This AES key is then encrypted using the server's public RSA key.
14
- - The final payload embedded in the image consists of the encrypted AES key,
15
- the AES nonce, and the encrypted JSON data (ciphertext).
16
-
17
- This server holds the private RSA key required to decrypt the AES key, and subsequently,
18
- the final payload. The public RSA key must be distributed to clients who wish to
19
- encrypt data for this service.
20
-
21
- Required Environment Variables:
22
- - `KEYLOCK_PRIV_KEY`: A string containing the server's PEM-encoded private RSA key.
23
-
24
- Required Files:
25
- - `keylock_pub.pem`: A file containing the server's corresponding PEM-encoded public key.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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. This key will only be used for this one message.
198
- 2. **Client Encrypts Data**: The client takes the JSON payload, converts it to bytes, and encrypts it using the one-time AES key. This produces the `ciphertext` and an authentication `tag`.
199
- 3. **Client Encrypts the One-Time Key**: To send the AES key securely, the client encrypts it using this server's **Public RSA Key** (provided below). Only the server, with its matching private key, can decrypt this.
200
- 4. **Client Creates the Payload**: The client bundles everything into a single binary payload in a specific order:
201
- - The encrypted AES key.
202
- - The `nonce` (a random value used in AES encryption).
203
- - The `ciphertext` and `tag`.
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="python", label="Server Public Key")
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. This code performs the inverse of the server's operation.")
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
- aesgcm = AESGCM(aes_key)
243
- nonce = os.urandom(12) # 96-bit nonce
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 for the template image. Needs {len(full_binary_string)} pixels, image has {pixel_data.size}.")
268
-
269
  flat_pixels = pixel_data.ravel()
270
  for i in range(len(full_binary_string)):
271
- pixel_val = flat_pixels[i]
272
- bit_val = int(full_binary_string[i])
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
- "user_id": "user-12345",
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
- # Prepare the JSON payload
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
- result = response.json()
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=2)
363
-
364
  gr.Markdown("---")
365
  gr.Markdown("## Error Handling Guide")
366
  gr.Markdown(
367
  """
368
- If your API call fails, check the error message for clues:
369
-
370
- - **`"Decryption failed. ... InvalidTag"`**: This is the most common error. It almost always means one of two things:
371
- 1. The image was encrypted with a **different public key** than the one this server uses.
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
- # Launch the Gradio app
 
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()