broadfield-dev commited on
Commit
a6eb2c8
·
verified ·
1 Parent(s): 60996af

Create server/app.py

Browse files
Files changed (1) hide show
  1. server/app.py +235 -0
server/app.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import json
4
+ import base64
5
+ import struct
6
+ import logging
7
+
8
+ import gradio as gr
9
+ from PIL import Image
10
+ import numpy as np
11
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
12
+ from cryptography.hazmat.primitives import hashes
13
+ from cryptography.hazmat.primitives import serialization
14
+ from cryptography.hazmat.primitives.asymmetric import padding
15
+ from cryptography.exceptions import InvalidTag
16
+
17
+ # --- Constants ---
18
+ HEADER_BITS = 32
19
+ AES_GCM_NONCE_SIZE = 12
20
+
21
+ # --- Configure Logging ---
22
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # --- Load Keys and Config ---
26
+ # For production (e.g., Hugging Face Spaces), the private key should be an environment variable.
27
+ KEYLOCK_PRIV_KEY = os.environ.get('KEYLOCK_PRIV_KEY')
28
+ KEYLOCK_STATUS_MESSAGE = ""
29
+
30
+ # For local testing, fall back to reading from a file if the environment variable is not set.
31
+ if not KEYLOCK_PRIV_KEY:
32
+ try:
33
+ with open("keylock_priv.pem", "r") as f:
34
+ KEYLOCK_PRIV_KEY = f.read()
35
+ logger.warning("Loaded private key from 'keylock_priv.pem'. This is for local testing only. Use environment variables in production.")
36
+ KEYLOCK_STATUS_MESSAGE = "⚠️ Loaded from `keylock_priv.pem` file. This is acceptable for local testing but insecure for production. Use secrets/environment variables for deployment."
37
+ except FileNotFoundError:
38
+ logger.error("FATAL: Private key not found as env var 'KEYLOCK_PRIV_KEY' or in 'keylock_priv.pem'. API is non-functional.")
39
+ KEYLOCK_STATUS_MESSAGE = "❌ NOT FOUND. The API is non-functional. The administrator must set the `KEYLOCK_PRIV_KEY` secret or provide a `keylock_priv.pem` file."
40
+ else:
41
+ logger.info("Successfully loaded private key from environment variable 'KEYLOCK_PRIV_KEY'.")
42
+ KEYLOCK_STATUS_MESSAGE = "✅ Loaded successfully from secrets/environment variable. This is the recommended secure configuration."
43
+
44
+
45
+ PUBLIC_KEY_PEM_STRING = ""
46
+ try:
47
+ with open("keylock_pub.pem", "r") as f:
48
+ PUBLIC_KEY_PEM_STRING = f.read()
49
+ logger.info("Successfully loaded public key from keylock_pub.pem for display.")
50
+ except FileNotFoundError:
51
+ logger.error("FATAL: keylock_pub.pem not found. The UI will show an error.")
52
+ PUBLIC_KEY_PEM_STRING = "Error: keylock_pub.pem file not found on the server. Make sure it's in your repository."
53
+ except Exception as e:
54
+ logger.error(f"Error loading public key: {e}")
55
+ PUBLIC_KEY_PEM_STRING = f"Error loading public key: {e}"
56
+
57
+ # --- Core Decryption Function ---
58
+ def decode_data(image_base64_string: str) -> dict:
59
+ """
60
+ Decrypts a payload hidden in a PNG image via LSB steganography and hybrid encryption.
61
+ """
62
+ if not KEYLOCK_PRIV_KEY:
63
+ error_msg = "Server Error: The API is not configured with a private key. Please contact the administrator."
64
+ logger.error(error_msg)
65
+ raise gr.Error(error_msg)
66
+
67
+ try:
68
+ # 1. Decode Base64 and load image data
69
+ image_buffer = base64.b64decode(image_base64_string)
70
+ img = Image.open(io.BytesIO(image_buffer)).convert("RGB")
71
+ pixel_data = np.array(img).ravel()
72
+
73
+ # 2. Extract data length from the LSB header
74
+ if pixel_data.size < HEADER_BITS:
75
+ raise ValueError(f"Image is too small. Minimum pixel count: {HEADER_BITS}")
76
+
77
+ header_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[:HEADER_BITS])
78
+ data_length = int(header_binary_string, 2)
79
+
80
+ if data_length == 0:
81
+ return {}
82
+
83
+ # 3. Extract the full crypto payload
84
+ data_bits_count = data_length * 8
85
+ end_offset = HEADER_BITS + data_bits_count
86
+ if pixel_data.size < end_offset:
87
+ raise ValueError("Image data corrupt or truncated.")
88
+
89
+ data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
90
+ crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
91
+
92
+ # 4. Load private key and unpack payload
93
+ private_key = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY.encode(), password=None)
94
+
95
+ offset = 4
96
+ encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
97
+ rsa_key_size_bytes = private_key.key_size // 8
98
+ if encrypted_aes_key_len != rsa_key_size_bytes:
99
+ raise ValueError(f"Key mismatch: Encrypted with a {encrypted_aes_key_len*8}-bit key, server uses {private_key.key_size}-bit key.")
100
+
101
+ encrypted_aes_key = crypto_payload[offset : offset + encrypted_aes_key_len]
102
+ offset += encrypted_aes_key_len
103
+ nonce = crypto_payload[offset : offset + AES_GCM_NONCE_SIZE]
104
+ offset += AES_GCM_NONCE_SIZE
105
+ ciphertext_with_tag = crypto_payload[offset:]
106
+
107
+ # 5. Decrypt
108
+ recovered_aes_key = private_key.decrypt(
109
+ encrypted_aes_key,
110
+ padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
111
+ )
112
+ aesgcm = AESGCM(recovered_aes_key)
113
+ decrypted_bytes = aesgcm.decrypt(nonce, ciphertext_with_tag, None)
114
+
115
+ logger.info("Successfully decrypted payload from image.")
116
+ return json.loads(decrypted_bytes.decode('utf-8'))
117
+
118
+ except (ValueError, InvalidTag, TypeError, struct.error) as e:
119
+ logger.error(f"Decryption failed: {e}")
120
+ raise gr.Error(f"Decryption failed. The image may be corrupt, not from a compatible client, or used the wrong public key. Details: {e}")
121
+ except Exception as e:
122
+ logger.error(f"An unexpected error occurred: {e}", exc_info=True)
123
+ raise gr.Error(f"An unexpected server error occurred. Details: {e}")
124
+
125
+ # ==============================================================================
126
+ # GRADIO INTERFACE
127
+ # ==============================================================================
128
+ with gr.Blocks(title="Secure Decoder API", theme=gr.themes.Soft()) as demo:
129
+ gr.Markdown("# 🔐 Secure KeyLock Decoder API")
130
+ gr.Markdown("This application provides a secure API endpoint to decrypt and extract JSON data hidden within PNG images.")
131
+
132
+ with gr.Tabs():
133
+ with gr.TabItem("🚀 Quick Start & Documentation"):
134
+ # ... (The rest of your extensive Gradio UI markdown goes here, it's perfect as is)
135
+ gr.Markdown("## What Is This?")
136
+ gr.Markdown(
137
+ "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) "
138
+ "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)."
139
+ )
140
+
141
+ with gr.Accordion("How It Works (The Gory Details)", open=False):
142
+ gr.Markdown(
143
+ """
144
+ 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:
145
+ 1. **Client Generates a One-Time Key**: The client creates a random, single-use 32-byte symmetric key for AES-GCM encryption.
146
+ 2. **Client Encrypts Data**: The client takes the JSON payload and encrypts it using the one-time AES key.
147
+ 3. **Client Encrypts the One-Time Key**: The client encrypts the AES key using this server's **Public RSA Key** (provided below).
148
+ 4. **Client Creates the Payload**: The client bundles the encrypted AES key, nonce, and ciphertext into a single binary payload.
149
+ 5. **Client Hides the Payload**: The client uses **Least Significant Bit (LSB) steganography** to hide the payload in a template image.
150
+ 6. **Client Sends Image**: The final image is sent to this API endpoint.
151
+ 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.
152
+ """
153
+ )
154
+
155
+ gr.Markdown("---")
156
+ gr.Markdown("## How to Use This API")
157
+ gr.Markdown("### Step 1: Get the Server's Public Key")
158
+ 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`).")
159
+ gr.Code(value=PUBLIC_KEY_PEM_STRING, language="pem", label="Server Public Key")
160
+
161
+ with gr.Accordion("Step 2: Create the Encrypted Image (Client-Side Python Example)", open=True):
162
+ gr.Markdown("Use the following Python code in your client application to create a KeyLock image.")
163
+ gr.Code(
164
+ language="python",
165
+ label="Client-Side Image Creation Script (create_keylock_image.py)",
166
+ value="""
167
+ import json; import os; import struct; from PIL import Image; import numpy as np
168
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
169
+ from cryptography.hazmat.primitives import hashes, serialization
170
+ from cryptography.hazmat.primitives.asymmetric import padding
171
+ def create_keylock_image(public_key_pem: str, json_data: dict, template_image_path: str, output_path: str):
172
+ public_key = serialization.load_pem_public_key(public_key_pem.encode())
173
+ data_to_encrypt = json.dumps(json_data).encode('utf-8')
174
+ aes_key = AESGCM.generate_key(bit_length=256)
175
+ nonce = os.urandom(12)
176
+ ciphertext_with_tag = AESGCM(aes_key).encrypt(nonce, data_to_encrypt, None)
177
+ encrypted_aes_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
178
+ payload = struct.pack('>I', len(encrypted_aes_key)) + encrypted_aes_key + nonce + ciphertext_with_tag
179
+ img = Image.open(template_image_path).convert("RGB")
180
+ pixel_data = np.array(img)
181
+ full_binary_string = f'{len(payload):032b}' + ''.join(f'{byte:08b}' for byte in payload)
182
+ if len(full_binary_string) > pixel_data.size:
183
+ raise ValueError(f"Data is too large. Needs {len(full_binary_string)} pixels, image has {pixel_data.size}.")
184
+ flat_pixels = pixel_data.ravel()
185
+ for i in range(len(full_binary_string)):
186
+ flat_pixels[i] = (flat_pixels[i] & 0b11111110) | int(full_binary_string[i])
187
+ Image.fromarray(pixel_data).save(output_path, "PNG")
188
+ print(f"Successfully created encrypted image at {output_path}")
189
+ # --- Example Usage ---
190
+ if __name__ == '__main__':
191
+ SERVER_PUBLIC_KEY = \"\"\"-----BEGIN PUBLIC KEY-----
192
+ YOUR_SERVER_PUBLIC_KEY_HERE
193
+ -----END PUBLIC KEY-----\"\"\"
194
+ my_secret_data = {"user_id": "user-12345", "token": "abcdef-123456"}
195
+ create_keylock_image(SERVER_PUBLIC_KEY, my_secret_data, "template.png", "encrypted_image.png")
196
+ """
197
+ )
198
+
199
+ gr.Markdown("### Step 3: Call the API Endpoint")
200
+ gr.Markdown(
201
+ "- **Method**: `POST`\n"
202
+ "- **Endpoint**: `/run/keylock-auth-decoder`\n"
203
+ "- **Body**: A JSON object containing a base64-encoded string of the image."
204
+ )
205
+ gr.Code(language="json", label="JSON Payload Schema", value='{\n "data": [\n "<base64_encoded_image_string>"\n ]\n}')
206
+
207
+
208
+ with gr.TabItem("⚙️ Server Status & Error Guide"):
209
+ gr.Markdown("## Server Status")
210
+ gr.Textbox(label="Private Key Status", value=KEYLOCK_STATUS_MESSAGE, interactive=False, lines=3)
211
+
212
+ gr.Markdown("---")
213
+ gr.Markdown("## Error Handling Guide")
214
+ gr.Markdown(
215
+ """
216
+ - **`"Decryption failed. ... InvalidTag"`**: The most common error. It means the image was encrypted with a **different public key** or the image file was corrupted.
217
+ - **`"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.
218
+ - **`"Image data corrupt or truncated..."`**: The image file is incomplete or damaged.
219
+ - **`"Server Error: ... not configured"`**: A server-side problem. The administrator has not set the `KEYLOCK_PRIV_KEY` secret correctly.
220
+ """
221
+ )
222
+
223
+ # --- Hidden component for the API endpoint ---
224
+ with gr.Row(visible=False):
225
+ api_input = gr.Textbox()
226
+ api_output = gr.JSON()
227
+ gr.Interface(
228
+ fn=decode_data,
229
+ inputs=api_input,
230
+ outputs=api_output,
231
+ api_name="keylock-auth-decoder",
232
+ )
233
+
234
+ if __name__ == "__main__":
235
+ demo.launch()