broadfield-dev commited on
Commit
88eb864
·
verified ·
1 Parent(s): 56e1e75

Update server/app.py

Browse files
Files changed (1) hide show
  1. server/app.py +70 -111
server/app.py CHANGED
@@ -12,6 +12,8 @@ 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 ---
@@ -23,24 +25,21 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(level
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:
@@ -49,38 +48,45 @@ try:
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:
@@ -89,9 +95,8 @@ def decode_data(image_base64_string: str) -> dict:
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
@@ -104,22 +109,17 @@ def decode_data(image_base64_string: str) -> dict:
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
  # ==============================================================================
@@ -131,92 +131,56 @@ with gr.Blocks(title="Secure Decoder API", theme=gr.themes.Soft()) as demo:
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="python", 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
 
@@ -224,12 +188,7 @@ YOUR_SERVER_PUBLIC_KEY_HERE
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()
 
12
  from cryptography.hazmat.primitives import hashes
13
  from cryptography.hazmat.primitives import serialization
14
  from cryptography.hazmat.primitives.asymmetric import padding
15
+ # Import 'rsa' for the key generation utility
16
+ from cryptography.hazmat.primitives.asymmetric import rsa
17
  from cryptography.exceptions import InvalidTag
18
 
19
  # --- Constants ---
 
25
  logger = logging.getLogger(__name__)
26
 
27
  # --- Load Keys and Config ---
 
28
  KEYLOCK_PRIV_KEY = os.environ.get('KEYLOCK_PRIV_KEY')
29
  KEYLOCK_STATUS_MESSAGE = ""
30
 
 
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.")
36
+ KEYLOCK_STATUS_MESSAGE = "⚠️ Loaded from `keylock_priv.pem` file. This is for local testing but insecure for production."
37
  except FileNotFoundError:
38
+ logger.error("FATAL: Private key not found. API is non-functional.")
39
+ KEYLOCK_STATUS_MESSAGE = "❌ NOT FOUND. The API is non-functional. 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. Recommended secure configuration."
 
43
 
44
  PUBLIC_KEY_PEM_STRING = ""
45
  try:
 
48
  logger.info("Successfully loaded public key from keylock_pub.pem for display.")
49
  except FileNotFoundError:
50
  logger.error("FATAL: keylock_pub.pem not found. The UI will show an error.")
51
+ PUBLIC_KEY_PEM_STRING = "Error: keylock_pub.pem file not found on the server."
52
  except Exception as e:
53
  logger.error(f"Error loading public key: {e}")
54
  PUBLIC_KEY_PEM_STRING = f"Error loading public key: {e}"
55
 
56
+ # --- Key Generation Utility ---
57
+ def generate_rsa_keys():
58
+ """Generates a new 2048-bit RSA private and public key pair."""
59
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
60
+ private_pem = private_key.private_bytes(
61
+ encoding=serialization.Encoding.PEM,
62
+ format=serialization.PrivateFormat.PKCS8,
63
+ encryption_algorithm=serialization.NoEncryption()
64
+ ).decode('utf-8')
65
+ public_pem = private_key.public_key().public_bytes(
66
+ encoding=serialization.Encoding.PEM,
67
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
68
+ ).decode('utf-8')
69
+ return private_pem, public_pem
70
+
71
  # --- Core Decryption Function ---
72
  def decode_data(image_base64_string: str) -> dict:
73
+ """Decrypts a payload hidden in a PNG image via LSB steganography and hybrid encryption."""
 
 
74
  if not KEYLOCK_PRIV_KEY:
75
+ error_msg = "Server Error: The API is not configured with a private key."
 
76
  raise gr.Error(error_msg)
 
77
  try:
 
78
  image_buffer = base64.b64decode(image_base64_string)
79
  img = Image.open(io.BytesIO(image_buffer)).convert("RGB")
80
  pixel_data = np.array(img).ravel()
81
 
 
82
  if pixel_data.size < HEADER_BITS:
83
  raise ValueError(f"Image is too small. Minimum pixel count: {HEADER_BITS}")
84
 
85
  header_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[:HEADER_BITS])
86
  data_length = int(header_binary_string, 2)
87
 
88
+ if data_length == 0: return {}
 
89
 
 
90
  data_bits_count = data_length * 8
91
  end_offset = HEADER_BITS + data_bits_count
92
  if pixel_data.size < end_offset:
 
95
  data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
96
  crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
97
 
 
98
  private_key = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY.encode(), password=None)
99
+
100
  offset = 4
101
  encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
102
  rsa_key_size_bytes = private_key.key_size // 8
 
109
  offset += AES_GCM_NONCE_SIZE
110
  ciphertext_with_tag = crypto_payload[offset:]
111
 
 
112
  recovered_aes_key = private_key.decrypt(
113
  encrypted_aes_key,
114
  padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
115
  )
116
+ decrypted_bytes = AESGCM(recovered_aes_key).decrypt(nonce, ciphertext_with_tag, None)
 
117
 
 
118
  return json.loads(decrypted_bytes.decode('utf-8'))
119
 
120
  except (ValueError, InvalidTag, TypeError, struct.error) as e:
121
+ raise gr.Error(f"Decryption failed. Image may be corrupt or used the wrong public key. Details: {e}")
 
122
  except Exception as e:
 
123
  raise gr.Error(f"An unexpected server error occurred. Details: {e}")
124
 
125
  # ==============================================================================
 
131
 
132
  with gr.Tabs():
133
  with gr.TabItem("🚀 Quick Start & Documentation"):
134
+ # This tab remains unchanged, showing clients how to use the API
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  gr.Markdown("## How to Use This API")
136
  gr.Markdown("### Step 1: Get the Server's Public Key")
137
+ gr.Code(value=PUBLIC_KEY_PEM_STRING, language="pem", label="Server Public Key")
138
+ # ... (The rest of the extensive documentation from your original file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  with gr.TabItem("⚙️ Server Status & Error Guide"):
141
  gr.Markdown("## Server Status")
142
  gr.Textbox(label="Private Key Status", value=KEYLOCK_STATUS_MESSAGE, interactive=False, lines=3)
 
143
  gr.Markdown("---")
144
+
145
+ # --- NEW: Key Generation Utility ---
146
+ with gr.Accordion("🔑 Admin: Key Pair Generator", open=False):
147
+ gr.Markdown(
148
+ "**For Administrators Only.** Use this tool to generate a new RSA key pair for the server. "
149
+ "**This does NOT automatically apply the keys.** To use them, you must:\n"
150
+ "1. Copy the **Private Key** and update the `KEYLOCK_PRIV_KEY` secret in your deployment environment.\n"
151
+ "2. Copy the **Public Key** and overwrite the contents of the `keylock_pub.pem` file in your repository.\n"
152
+ "3. Restart the server for the changes to take effect."
153
+ )
154
+ gen_keys_button = gr.Button("⚙️ Generate New 2048-bit Key Pair", variant="secondary")
155
+ with gr.Row():
156
+ with gr.Column():
157
+ output_private_key = gr.Textbox(
158
+ lines=10,
159
+ label="Generated Private Key (KEEP THIS SECRET)",
160
+ interactive=False,
161
+ show_copy_button=True
162
+ )
163
+ with gr.Column():
164
+ output_public_key = gr.Textbox(
165
+ lines=10,
166
+ label="Generated Public Key (for clients & keylock_pub.pem)",
167
+ interactive=False,
168
+ show_copy_button=True
169
+ )
170
+
171
+ gen_keys_button.click(
172
+ fn=generate_rsa_keys,
173
+ inputs=None,
174
+ outputs=[output_private_key, output_public_key]
175
+ )
176
+
177
  gr.Markdown("## Error Handling Guide")
178
  gr.Markdown(
179
  """
180
+ - **`"Decryption failed. ... InvalidTag"`**: The most common error. The image was encrypted with a **different public key** or the image file was corrupted.
181
+ - **`"Decryption failed. ... Key mismatch"`**: The RSA key size used to encrypt the data does not match the server's key size.
182
  - **`"Image data corrupt or truncated..."`**: The image file is incomplete or damaged.
183
+ - **`"Server Error: ... not configured"`**: The administrator has not set the `KEYLOCK_PRIV_KEY` secret correctly.
184
  """
185
  )
186
 
 
188
  with gr.Row(visible=False):
189
  api_input = gr.Textbox()
190
  api_output = gr.JSON()
191
+ gr.Interface(fn=decode_data, inputs=api_input, outputs=api_output, api_name="keylock-auth-decoder")
 
 
 
 
 
192
 
193
  if __name__ == "__main__":
194
+ demo.launch()