broadfield-dev commited on
Commit
5ba8863
·
verified ·
1 Parent(s): d00af0e

Update client/app.py

Browse files
Files changed (1) hide show
  1. client/app.py +64 -82
client/app.py CHANGED
@@ -7,9 +7,9 @@ import logging
7
  import os
8
  import requests
9
  import struct
 
10
  import numpy as np
11
  from cryptography.hazmat.primitives import serialization
12
- # Import 'rsa' for key generation
13
  from cryptography.hazmat.primitives.asymmetric import rsa, padding
14
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
15
  from cryptography.hazmat.primitives import hashes
@@ -63,32 +63,14 @@ def prepare_base_image(uploaded_image: Image.Image | None, progress) -> Image.Im
63
  # --- Core Cryptography and Image Creation (Unchanged) ---
64
 
65
  def generate_rsa_keys():
66
- """Generates a new 2048-bit RSA private and public key pair."""
67
- private_key = rsa.generate_private_key(
68
- public_exponent=65537,
69
- key_size=2048
70
- )
71
- private_pem = private_key.private_bytes(
72
- encoding=serialization.Encoding.PEM,
73
- format=serialization.PrivateFormat.PKCS8,
74
- encryption_algorithm=serialization.NoEncryption()
75
- ).decode('utf-8')
76
- public_pem = private_key.public_key().public_bytes(
77
- encoding=serialization.Encoding.PEM,
78
- format=serialization.PublicFormat.SubjectPublicKeyInfo
79
- ).decode('utf-8')
80
  return private_pem, public_pem
81
 
82
- def create_encrypted_image(
83
- secret_data_str: str,
84
- public_key_pem: str,
85
- base_image: Image.Image,
86
- overlay_option: str
87
- ) -> Image.Image:
88
- """Encrypts data and embeds it into an image using LSB steganography."""
89
  if not secret_data_str.strip(): raise ValueError("Secret data cannot be empty.")
90
  if not public_key_pem.strip(): raise ValueError("Public Key cannot be empty.")
91
-
92
  data_dict = {}
93
  for line in secret_data_str.splitlines():
94
  line = line.strip()
@@ -96,18 +78,15 @@ def create_encrypted_image(
96
  parts = line.split(':', 1) if ':' in line else line.split('=', 1)
97
  if len(parts) == 2: data_dict[parts[0].strip()] = parts[1].strip().strip("'\"")
98
  if not data_dict: raise ValueError("No valid key-value pairs found.")
99
-
100
  json_bytes = json.dumps(data_dict).encode('utf-8')
101
  public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
102
  aes_key, nonce = os.urandom(32), os.urandom(12)
103
  ciphertext = AESGCM(aes_key).encrypt(nonce, json_bytes, None)
104
  rsa_encrypted_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
105
-
106
  encrypted_payload = struct.pack('>I', len(rsa_encrypted_key)) + rsa_encrypted_key + nonce + ciphertext
107
  img = base_image.copy().convert("RGB")
108
  width, height = img.size
109
-
110
- # Stylish Overlays (Code unchanged)
111
  draw = ImageDraw.Draw(img, "RGBA")
112
  try:
113
  font_bold = ImageFont.truetype("DejaVuSans-Bold.ttf", 30)
@@ -128,22 +107,17 @@ def create_encrypted_image(
128
  draw.rectangle([20, box_y0, width - 20, height - 20], fill=overlay_color)
129
  current_y = box_y0 + 15
130
  for i, (key, value) in enumerate(data_dict.items()):
131
- if overlay_option == "Keys Only":
132
- draw.text((35, current_y), key, fill=key_color, font=font_regular)
133
  else:
134
  key_text = f"{key}:"; draw.text((35, current_y), key_text, fill=key_color, font=font_regular)
135
  key_bbox = draw.textbbox((35, current_y), key_text, font=font_regular)
136
  draw.text((key_bbox[2] + 8, current_y), str(value), fill=value_color, font=font_regular)
137
  current_y += line_heights[i] + 6
138
-
139
- # Steganography
140
  pixel_data = np.array(img.convert("RGB")).ravel()
141
  header = struct.pack('>I', len(encrypted_payload))
142
  binary_payload = ''.join(format(b, '08b') for b in header + encrypted_payload)
143
-
144
- if len(binary_payload) > pixel_data.size:
145
- raise ValueError(f"Data is too large for the image. Max bytes: {pixel_data.size // 8}.")
146
-
147
  pixel_data[:len(binary_payload)] = (pixel_data[:len(binary_payload)] & 0xFE) | np.array(list(binary_payload), dtype=np.uint8)
148
  stego_pixels = pixel_data.reshape((height, width, 3))
149
  return Image.fromarray(stego_pixels, 'RGB')
@@ -151,70 +125,80 @@ def create_encrypted_image(
151
  # --- New Server Management and Gradio Wrappers ---
152
 
153
  def add_server(url: str, current_servers: list):
154
- """Tests a server URL, fetches its public key and info, and adds it to the list."""
155
- if not url or not url.startswith(('http://', 'https://')):
156
- raise gr.Error("Please enter a valid server URL (e.g., https://...).")
157
-
158
  url = url.strip().rstrip('/')
159
  if any(s['url'] == url for s in current_servers):
160
- return current_servers, f"ℹ️ Server at {url} is already in the list."
161
-
162
  status = f"⏳ Testing server at {url}..."
163
  yield current_servers, status, gr.Button(interactive=False)
164
-
165
  try:
166
  client = Client(url, verbose=False)
167
-
168
- info_result_path = client.predict(api_name="/keylock-info")
169
- with open(info_result_path, 'r', encoding='utf-8') as f: server_info = json.load(f)
170
  server_name = server_info.get("name", url)
171
-
172
- pubkey_result_path = client.predict(api_name="/keylock-pub")
173
- with open(pubkey_result_path, 'r', encoding='utf-8') as f: public_key = f.read()
174
-
175
- if not public_key.strip().startswith("-----BEGIN PUBLIC KEY-----"):
176
- raise ValueError("Received invalid public key format from server.")
177
-
178
- new_server = {
179
- "name": f"{server_name} ({url.split('//')[1].split('/')[0]})",
180
- "url": url,
181
- "public_key": public_key,
182
- "api_endpoint": "/keylock-server"
183
- }
184
  updated_servers = current_servers + [new_server]
185
- status_message = f"✅ Successfully added server: {server_name}"
186
  yield updated_servers, status_message, gr.Button(interactive=True)
187
-
188
  except Exception as e:
189
  logger.error(f"Failed to add server at {url}: {e}", exc_info=True)
190
- status_message = f"❌ Failed to connect or validate server at {url}. Error: {e}"
191
  yield current_servers, status_message, gr.Button(interactive=True)
192
 
193
  def create_keylock_wrapper(service_name: str, secret_data: str, available_servers: list, uploaded_image: Image.Image | None, overlay_option: str, progress=gr.Progress(track_tqdm=True)):
 
 
 
 
 
194
  if not service_name: raise gr.Error("Please add and select a target server.")
195
  server = next((s for s in available_servers if s['name'] == service_name), None)
196
  if not server: raise gr.Error(f"Could not find config for '{service_name}'. Please re-add it.")
 
 
197
  try:
198
  base_image = prepare_base_image(uploaded_image, progress)
199
  progress(0.5, desc="Encrypting and embedding data...")
200
  created_image = create_encrypted_image(secret_data, server['public_key'], base_image, overlay_option)
201
- return created_image, f"✅ Success! Image created for '{service_name}'.", gr.Tabs(selected=1)
 
 
 
 
 
 
 
 
 
 
202
  except Exception as e:
203
  logger.error(f"Error creating image: {e}", exc_info=True)
204
- return None, f"❌ Error: {e}", gr.Tabs()
 
 
 
205
 
206
- def send_keylock_wrapper(service_name: str, image: Image.Image, available_servers: list):
 
207
  if not service_name: raise gr.Error("Please select a target server.")
208
- if image is None: raise gr.Error("Please create or upload an image to send.")
 
209
  server = next((s for s in available_servers if s['name'] == service_name), None)
210
  if not server: raise gr.Error(f"Config Error for '{service_name}'. Please re-add it.")
211
 
212
  server_url, api_name = server['url'], server['api_endpoint']
213
  yield None, f"⏳ Connecting to remote server: {server_url}"
214
  try:
215
- with io.BytesIO() as buffer:
216
- image.save(buffer, format="PNG")
217
- b64_string = base64.b64encode(buffer.getvalue()).decode("utf-8")
 
 
218
  client = Client(server_url)
219
  result = client.predict(image_base64_string=b64_string, api_name=api_name)
220
 
@@ -252,13 +236,16 @@ with gr.Blocks(theme=theme, title="KeyLock Operations Dashboard") as demo:
252
  creator_overlay_option = gr.Radio(label="Overlay Content", choices=["Keys and Values", "Keys Only", "None"], value="Keys and Values")
253
  creator_button = gr.Button("✨ Create Auth Image", variant="primary")
254
  with gr.Column(scale=3):
255
- creator_image_output = gr.Image(label="Generated Encrypted Image", type="pil", show_download_button=True, height=600)
 
 
256
 
257
  with gr.TabItem("② Send KeyLock", id=1):
258
  with gr.Row(variant="panel"):
259
  with gr.Column(scale=1):
260
  send_service_dropdown = gr.Dropdown(label="Target Server", interactive=True)
261
- client_image_input = gr.Image(type="pil", label="Upload or Drag Encrypted Image", sources=["upload", "clipboard"])
 
262
  client_button = gr.Button("🔓 Decrypt via Remote Server", variant="primary")
263
  with gr.Column(scale=1):
264
  client_json_output = gr.JSON(label="Decrypted Data from Server")
@@ -279,24 +266,16 @@ with gr.Blocks(theme=theme, title="KeyLock Operations Dashboard") as demo:
279
  dropdown_update = gr.Dropdown(choices=server_names, value=selected, label="Target Server", interactive=True)
280
  return dropdown_update, dropdown_update
281
 
282
- add_server_button.click(
283
- fn=add_server,
284
- inputs=[server_url_input, servers_state],
285
- outputs=[servers_state, global_status, add_server_button]
286
- ).then(fn=lambda: "", outputs=[server_url_input]) # Clear input after trying
287
 
288
- servers_state.change(
289
- fn=update_dropdowns_from_state,
290
- inputs=servers_state,
291
- outputs=[creator_service_dropdown, send_service_dropdown]
292
- )
293
 
294
  gen_keys_button.click(fn=generate_rsa_keys, inputs=None, outputs=[output_private_key, output_public_key])
295
 
296
  creator_button.click(
297
  fn=create_keylock_wrapper,
298
  inputs=[creator_service_dropdown, creator_secret_input, servers_state, creator_base_image_input, creator_overlay_option],
299
- outputs=[creator_image_output, global_status, tabs]
300
  )
301
 
302
  client_button.click(
@@ -307,7 +286,10 @@ with gr.Blocks(theme=theme, title="KeyLock Operations Dashboard") as demo:
307
 
308
  creator_service_dropdown.change(fn=sync_dropdowns, inputs=creator_service_dropdown, outputs=send_service_dropdown)
309
  send_service_dropdown.change(fn=sync_dropdowns, inputs=send_service_dropdown, outputs=creator_service_dropdown)
310
- creator_image_output.change(fn=lambda x: x, inputs=creator_image_output, outputs=client_image_input)
 
 
 
311
 
312
  if __name__ == "__main__":
313
  demo.launch()
 
7
  import os
8
  import requests
9
  import struct
10
+ import tempfile # <--- Import tempfile
11
  import numpy as np
12
  from cryptography.hazmat.primitives import serialization
 
13
  from cryptography.hazmat.primitives.asymmetric import rsa, padding
14
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
15
  from cryptography.hazmat.primitives import hashes
 
63
  # --- Core Cryptography and Image Creation (Unchanged) ---
64
 
65
  def generate_rsa_keys():
66
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
67
+ private_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()).decode('utf-8')
68
+ public_pem = private_key.public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode('utf-8')
 
 
 
 
 
 
 
 
 
 
 
69
  return private_pem, public_pem
70
 
71
+ def create_encrypted_image(secret_data_str: str, public_key_pem: str, base_image: Image.Image, overlay_option: str) -> Image.Image:
 
 
 
 
 
 
72
  if not secret_data_str.strip(): raise ValueError("Secret data cannot be empty.")
73
  if not public_key_pem.strip(): raise ValueError("Public Key cannot be empty.")
 
74
  data_dict = {}
75
  for line in secret_data_str.splitlines():
76
  line = line.strip()
 
78
  parts = line.split(':', 1) if ':' in line else line.split('=', 1)
79
  if len(parts) == 2: data_dict[parts[0].strip()] = parts[1].strip().strip("'\"")
80
  if not data_dict: raise ValueError("No valid key-value pairs found.")
 
81
  json_bytes = json.dumps(data_dict).encode('utf-8')
82
  public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
83
  aes_key, nonce = os.urandom(32), os.urandom(12)
84
  ciphertext = AESGCM(aes_key).encrypt(nonce, json_bytes, None)
85
  rsa_encrypted_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
 
86
  encrypted_payload = struct.pack('>I', len(rsa_encrypted_key)) + rsa_encrypted_key + nonce + ciphertext
87
  img = base_image.copy().convert("RGB")
88
  width, height = img.size
89
+ # (Overlay drawing code is unchanged and correct)
 
90
  draw = ImageDraw.Draw(img, "RGBA")
91
  try:
92
  font_bold = ImageFont.truetype("DejaVuSans-Bold.ttf", 30)
 
107
  draw.rectangle([20, box_y0, width - 20, height - 20], fill=overlay_color)
108
  current_y = box_y0 + 15
109
  for i, (key, value) in enumerate(data_dict.items()):
110
+ if overlay_option == "Keys Only": draw.text((35, current_y), key, fill=key_color, font=font_regular)
 
111
  else:
112
  key_text = f"{key}:"; draw.text((35, current_y), key_text, fill=key_color, font=font_regular)
113
  key_bbox = draw.textbbox((35, current_y), key_text, font=font_regular)
114
  draw.text((key_bbox[2] + 8, current_y), str(value), fill=value_color, font=font_regular)
115
  current_y += line_heights[i] + 6
116
+ # (Steganography code is unchanged and correct)
 
117
  pixel_data = np.array(img.convert("RGB")).ravel()
118
  header = struct.pack('>I', len(encrypted_payload))
119
  binary_payload = ''.join(format(b, '08b') for b in header + encrypted_payload)
120
+ if len(binary_payload) > pixel_data.size: raise ValueError(f"Data is too large. Max: {pixel_data.size // 8} bytes.")
 
 
 
121
  pixel_data[:len(binary_payload)] = (pixel_data[:len(binary_payload)] & 0xFE) | np.array(list(binary_payload), dtype=np.uint8)
122
  stego_pixels = pixel_data.reshape((height, width, 3))
123
  return Image.fromarray(stego_pixels, 'RGB')
 
125
  # --- New Server Management and Gradio Wrappers ---
126
 
127
  def add_server(url: str, current_servers: list):
128
+ # This function is correct from the previous fix. Unchanged.
129
+ if not url or not url.startswith(('http://', 'https://')): raise gr.Error("Please enter a valid server URL (e.g., https://...).")
 
 
130
  url = url.strip().rstrip('/')
131
  if any(s['url'] == url for s in current_servers):
132
+ gr.Info(f"Server at {url} is already in the list.")
133
+ return current_servers, f"ℹ️ Server at {url} is already in the list.", gr.Button(interactive=True)
134
  status = f"⏳ Testing server at {url}..."
135
  yield current_servers, status, gr.Button(interactive=False)
 
136
  try:
137
  client = Client(url, verbose=False)
138
+ server_info = client.predict(api_name="/keylock-info")
139
+ if not isinstance(server_info, dict): raise TypeError(f"Expected dict from /keylock-info, but got {type(server_info)}")
 
140
  server_name = server_info.get("name", url)
141
+ public_key = client.predict(api_name="/keylock-pub")
142
+ if not isinstance(public_key, str): raise TypeError(f"Expected a string from /keylock-pub, but got {type(public_key)}")
143
+ if not public_key.strip().startswith("-----BEGIN PUBLIC KEY-----"): raise ValueError("Received invalid public key format from server.")
144
+ new_server = {"name": f"{server_name} ({url.split('//')[1].split('/')[0]})", "url": url, "public_key": public_key, "api_endpoint": "/keylock-server"}
 
 
 
 
 
 
 
 
 
145
  updated_servers = current_servers + [new_server]
146
+ status_message = f"✅ Successfully added server: {server_name}"; gr.Info(status_message)
147
  yield updated_servers, status_message, gr.Button(interactive=True)
 
148
  except Exception as e:
149
  logger.error(f"Failed to add server at {url}: {e}", exc_info=True)
150
+ status_message = f"❌ Failed to connect or validate server at {url}. Error: {e}"; gr.Error(status_message)
151
  yield current_servers, status_message, gr.Button(interactive=True)
152
 
153
  def create_keylock_wrapper(service_name: str, secret_data: str, available_servers: list, uploaded_image: Image.Image | None, overlay_option: str, progress=gr.Progress(track_tqdm=True)):
154
+ """
155
+ Creates the encrypted image and returns it to two separate components:
156
+ 1. A gr.Image for visual preview (which may be re-encoded by Gradio).
157
+ 2. A gr.File for a guaranteed, uncorrupted PNG download.
158
+ """
159
  if not service_name: raise gr.Error("Please add and select a target server.")
160
  server = next((s for s in available_servers if s['name'] == service_name), None)
161
  if not server: raise gr.Error(f"Could not find config for '{service_name}'. Please re-add it.")
162
+
163
+ png_path = None # Initialize to handle potential errors
164
  try:
165
  base_image = prepare_base_image(uploaded_image, progress)
166
  progress(0.5, desc="Encrypting and embedding data...")
167
  created_image = create_encrypted_image(secret_data, server['public_key'], base_image, overlay_option)
168
+
169
+ # Save image to a temporary PNG file to prevent re-encoding.
170
+ # This temporary file will be served by gr.File for perfect data integrity.
171
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_f:
172
+ png_path = temp_f.name
173
+ created_image.save(png_path, "PNG", compress_level=1)
174
+
175
+ status_message = f"✅ Success! Image created for '{service_name}'. Use the link to download the uncorrupted file."
176
+ # Return path to both preview and file components. Make file component visible.
177
+ return png_path, png_path, status_message, gr.Tabs(selected=1), gr.File(visible=True)
178
+
179
  except Exception as e:
180
  logger.error(f"Error creating image: {e}", exc_info=True)
181
+ if png_path and os.path.exists(png_path):
182
+ os.remove(png_path) # Clean up temp file on error
183
+ # Return Nones and hide file component
184
+ return None, None, f"❌ Error: {e}", gr.Tabs(), gr.File(visible=False)
185
 
186
+ def send_keylock_wrapper(service_name: str, image_path: str, available_servers: list):
187
+ # This function now expects a file path `image_path` from gr.Image
188
  if not service_name: raise gr.Error("Please select a target server.")
189
+ if image_path is None: raise gr.Error("Please create or upload an image to send.")
190
+
191
  server = next((s for s in available_servers if s['name'] == service_name), None)
192
  if not server: raise gr.Error(f"Config Error for '{service_name}'. Please re-add it.")
193
 
194
  server_url, api_name = server['url'], server['api_endpoint']
195
  yield None, f"⏳ Connecting to remote server: {server_url}"
196
  try:
197
+ # The input `image_path` is a path to a temporary file created by gr.Image.
198
+ # We read it directly to ensure we get the correct bytes.
199
+ with open(image_path, "rb") as img_file:
200
+ b64_string = base64.b64encode(img_file.read()).decode("utf-8")
201
+
202
  client = Client(server_url)
203
  result = client.predict(image_base64_string=b64_string, api_name=api_name)
204
 
 
236
  creator_overlay_option = gr.Radio(label="Overlay Content", choices=["Keys and Values", "Keys Only", "None"], value="Keys and Values")
237
  creator_button = gr.Button("✨ Create Auth Image", variant="primary")
238
  with gr.Column(scale=3):
239
+ # Use two components: one for preview, one for lossless download
240
+ creator_image_preview = gr.Image(label="Image Preview (for display only)", type="filepath", height=500)
241
+ creator_file_output = gr.File(label="Download Uncorrupted PNG File", file_count="single", visible=False)
242
 
243
  with gr.TabItem("② Send KeyLock", id=1):
244
  with gr.Row(variant="panel"):
245
  with gr.Column(scale=1):
246
  send_service_dropdown = gr.Dropdown(label="Target Server", interactive=True)
247
+ # This component will receive the file path from the preview image
248
+ client_image_input = gr.Image(type="filepath", label="Upload or Drag Encrypted Image", sources=["upload", "clipboard"])
249
  client_button = gr.Button("🔓 Decrypt via Remote Server", variant="primary")
250
  with gr.Column(scale=1):
251
  client_json_output = gr.JSON(label="Decrypted Data from Server")
 
266
  dropdown_update = gr.Dropdown(choices=server_names, value=selected, label="Target Server", interactive=True)
267
  return dropdown_update, dropdown_update
268
 
269
+ add_server_button.click(fn=add_server, inputs=[server_url_input, servers_state], outputs=[servers_state, global_status, add_server_button]).then(fn=lambda: "", outputs=[server_url_input])
 
 
 
 
270
 
271
+ servers_state.change(fn=update_dropdowns_from_state, inputs=servers_state, outputs=[creator_service_dropdown, send_service_dropdown])
 
 
 
 
272
 
273
  gen_keys_button.click(fn=generate_rsa_keys, inputs=None, outputs=[output_private_key, output_public_key])
274
 
275
  creator_button.click(
276
  fn=create_keylock_wrapper,
277
  inputs=[creator_service_dropdown, creator_secret_input, servers_state, creator_base_image_input, creator_overlay_option],
278
+ outputs=[creator_image_preview, creator_file_output, global_status, tabs, creator_file_output]
279
  )
280
 
281
  client_button.click(
 
286
 
287
  creator_service_dropdown.change(fn=sync_dropdowns, inputs=creator_service_dropdown, outputs=send_service_dropdown)
288
  send_service_dropdown.change(fn=sync_dropdowns, inputs=send_service_dropdown, outputs=creator_service_dropdown)
289
+
290
+ # When a new image preview is generated, auto-populate it in the sender tab.
291
+ # This works because both components now use file paths.
292
+ creator_image_preview.change(fn=lambda x: x, inputs=creator_image_preview, outputs=client_image_input)
293
 
294
  if __name__ == "__main__":
295
  demo.launch()