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

Update client/app.py

Browse files
Files changed (1) hide show
  1. client/app.py +112 -137
client/app.py CHANGED
@@ -1,9 +1,3 @@
1
- # The client code is excellent as-is. The only change needed is to make it
2
- # read the endpoints from a local file for the turnkey local demo.
3
- # Find this line:
4
- # CREATOR_ENDPOINTS_JSON_URL = "https://huggingface.co/spaces/broadfield-dev/KeyLock-Auth-Creator/raw/main/endpoints.json"
5
- # And replace the logic to be local-first.
6
-
7
  import gradio as gr
8
  from PIL import Image, ImageDraw, ImageFont, ImageOps
9
  import base64
@@ -15,6 +9,7 @@ import requests
15
  import struct
16
  import numpy as np
17
  from cryptography.hazmat.primitives import serialization
 
18
  from cryptography.hazmat.primitives.asymmetric import rsa, padding
19
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
20
  from cryptography.hazmat.primitives import hashes
@@ -25,15 +20,14 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(level
25
  logger = logging.getLogger(__name__)
26
 
27
  # --- Constants ---
28
- # Use a local file for easy testing. In a real-world scenario, this could be a URL.
29
  ENDPOINTS_CONFIG_PATH = "endpoints.json"
30
  DEFAULT_IMAGE_URL = "https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?q=80&w=1200&auto=format&fit=crop"
31
  IMAGE_SIZE = 600
32
  T2I_MODEL = "sd-community/sdxl-lightning"
33
  T2I_PROMPT = "A stunning view of a distant galaxy, nebulae, and constellations, digital art, vibrant colors, cinematic lighting, 8k, masterpiece."
34
 
35
- # --- Helper Functions for Image Handling (Your original code is perfect here) ---
36
- # ...
37
  def resize_and_crop(img: Image.Image, size: int = IMAGE_SIZE) -> Image.Image:
38
  """Resizes an image to fit within a square of `size` and then center-crops it."""
39
  try:
@@ -43,16 +37,12 @@ def resize_and_crop(img: Image.Image, size: int = IMAGE_SIZE) -> Image.Image:
43
  return img.resize((size, size), Image.Resampling.LANCZOS)
44
 
45
  def prepare_base_image(uploaded_image: Image.Image | None, progress) -> Image.Image:
46
- """
47
- Provides a base image using the fallback logic, updating a Gradio progress object.
48
- """
49
  if uploaded_image:
50
  progress(0, desc="βœ… Using uploaded image...")
51
- logger.info("Using user-uploaded image.")
52
  return resize_and_crop(uploaded_image)
53
  try:
54
- progress(0, desc="⏳ No image uploaded. Fetching default background...")
55
- logger.info(f"Fetching default image from URL: {DEFAULT_IMAGE_URL}")
56
  response = requests.get(DEFAULT_IMAGE_URL, timeout=15)
57
  response.raise_for_status()
58
  img = Image.open(io.BytesIO(response.content)).convert("RGB")
@@ -61,23 +51,33 @@ def prepare_base_image(uploaded_image: Image.Image | None, progress) -> Image.Im
61
  except Exception as e:
62
  logger.warning(f"Could not fetch default image: {e}. Falling back to AI generation.")
63
  try:
64
- progress(0, desc=f"⏳ Generating new image with {T2I_MODEL}...")
65
- logger.info(f"Generating a new image using model: {T2I_MODEL}")
66
  client = InferenceClient()
67
  image_bytes = client.text_to_image(T2I_PROMPT, model=T2I_MODEL)
68
  img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
69
- progress(0, desc="βœ… New image generated successfully.")
70
  return resize_and_crop(img)
71
  except Exception as e:
72
  logger.error(f"Fatal: All image sources failed. Text-to-image failed with: {e}")
73
  raise gr.Error(f"Failed to obtain a base image. AI generation error: {e}")
74
 
75
- # --- Core Cryptography and Image Creation (Your original code is perfect here) ---
76
- # ...
77
  def generate_rsa_keys():
78
- private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
79
- private_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()).decode('utf-8')
80
- public_pem = private_key.public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode('utf-8')
 
 
 
 
 
 
 
 
 
 
 
81
  return private_pem, public_pem
82
 
83
  def create_encrypted_image(
@@ -86,30 +86,32 @@ def create_encrypted_image(
86
  base_image: Image.Image,
87
  overlay_option: str
88
  ) -> Image.Image:
89
- if not secret_data_str.strip():
90
- raise ValueError("Secret data cannot be empty.")
91
- if not public_key_pem.strip():
92
- raise ValueError("Public Key cannot be empty.")
93
  data_dict = {}
94
  for line in secret_data_str.splitlines():
95
  line = line.strip()
96
  if not line or line.startswith('#'): continue
97
  parts = line.split(':', 1) if ':' in line else line.split('=', 1)
98
- if len(parts) == 2:
99
- data_dict[parts[0].strip()] = parts[1].strip().strip("'\"")
100
- if not data_dict:
101
- raise ValueError("No valid key-value pairs found in secret data.")
102
  json_bytes = json.dumps(data_dict).encode('utf-8')
103
  public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
104
  aes_key, nonce = os.urandom(32), os.urandom(12)
105
  ciphertext = AESGCM(aes_key).encrypt(nonce, json_bytes, None)
106
  rsa_encrypted_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
 
 
107
  encrypted_payload = struct.pack('>I', len(rsa_encrypted_key)) + rsa_encrypted_key + nonce + ciphertext
108
-
109
  img = base_image.copy().convert("RGB")
110
  width, height = img.size
111
 
112
- # Add overlays
 
113
  draw = ImageDraw.Draw(img, "RGBA")
114
  try:
115
  font_bold = ImageFont.truetype("DejaVuSans-Bold.ttf", 30)
@@ -118,93 +120,65 @@ def create_encrypted_image(
118
  font_bold = ImageFont.load_default(size=28)
119
  font_regular = ImageFont.load_default(size=14)
120
 
121
- # ... (the rest of your styling logic is great)
122
- overlay_color = (15, 23, 42, 190)
123
- title_color = (226, 232, 240)
124
- key_color = (148, 163, 184)
125
- value_color = (241, 245, 249)
126
 
127
  draw.rectangle([0, 20, width, 80], fill=overlay_color)
128
  draw.text((width / 2, 50), "KeyLock Secure Data", fill=title_color, font=font_bold, anchor="ms")
129
 
130
  if overlay_option != "None":
131
- box_padding = 15
132
- line_spacing = 6
133
- text_start_x = 35
134
-
135
- if overlay_option == "Keys Only":
136
- lines = list(data_dict.keys())
137
- else:
138
- lines = [f"{key}: {value}" for key, value in data_dict.items()]
139
-
140
  line_heights = [draw.textbbox((0, 0), line, font=font_regular)[3] for line in lines]
141
- total_text_height = sum(line_heights) + (len(lines) - 1) * line_spacing
142
- box_height = total_text_height + (box_padding * 2)
143
  box_y0 = height - box_height - 20
144
-
145
  draw.rectangle([20, box_y0, width - 20, height - 20], fill=overlay_color)
146
- current_y = box_y0 + box_padding
147
-
148
- if overlay_option == "Keys Only":
149
- for i, key in enumerate(data_dict.keys()):
150
- draw.text((text_start_x, current_y), key, fill=key_color, font=font_regular)
151
- current_y += line_heights[i] + line_spacing
152
- elif overlay_option == "Keys and Values":
153
- for i, (key, value) in enumerate(data_dict.items()):
154
- key_text = f"{key}:"
155
- draw.text((text_start_x, current_y), key_text, fill=key_color, font=font_regular)
156
- key_bbox = draw.textbbox((text_start_x, current_y), key_text, font=font_regular)
157
  draw.text((key_bbox[2] + 8, current_y), str(value), fill=value_color, font=font_regular)
158
- current_y += line_heights[i] + line_spacing
159
 
160
- # --- Steganography ---
161
  pixel_data = np.array(img.convert("RGB")).ravel()
162
  header = struct.pack('>I', len(encrypted_payload))
163
  binary_payload = ''.join(format(b, '08b') for b in header + encrypted_payload)
164
-
165
  if len(binary_payload) > pixel_data.size:
166
- raise ValueError(f"Data is too large for the image. Max size: {pixel_data.size // 8} bytes.")
 
167
  pixel_data[:len(binary_payload)] = (pixel_data[:len(binary_payload)] & 0xFE) | np.array(list(binary_payload), dtype=np.uint8)
168
  stego_pixels = pixel_data.reshape((height, width, 3))
169
  return Image.fromarray(stego_pixels, 'RGB')
170
 
171
-
172
  # --- Gradio Wrapper Functions ---
 
173
  def get_server_list():
174
  status = f"Fetching server list from '{ENDPOINTS_CONFIG_PATH}'..."
175
  yield gr.Dropdown(choices=[], value=None, label="⏳ Fetching..."), status, []
176
  try:
177
  with open(ENDPOINTS_CONFIG_PATH, "r") as f:
178
  all_entries = json.load(f)
179
-
180
  valid_endpoints = [e for e in all_entries if isinstance(e, dict) and "name" in e and "public_key" in e and "link" in e]
181
- if not valid_endpoints:
182
- raise ValueError("No valid server configurations found in the file.")
183
  endpoint_names = [e['name'] for e in valid_endpoints]
184
- status = f"βœ… Success! Found {len(endpoint_names)} valid servers."
185
  yield gr.Dropdown(choices=endpoint_names, value=endpoint_names[0] if endpoint_names else None, label="Target Server"), status, valid_endpoints
186
  except Exception as e:
187
  status = f"❌ Error loading configuration: {e}"
188
- logger.error(status)
189
  yield gr.Dropdown(choices=[], value=None, label="Error fetching servers"), status, []
190
 
191
-
192
  def create_keylock_wrapper(service_name: str, secret_data: str, available_endpoints: list, uploaded_image: Image.Image | None, overlay_option: str, progress=gr.Progress(track_tqdm=True)):
193
- if not service_name:
194
- raise gr.Error("Please select a target server.")
195
  public_key = next((e['public_key'] for e in available_endpoints if e['name'] == service_name), None)
196
- if not public_key:
197
- raise gr.Error(f"Could not find public key for '{service_name}'. Please refresh the server list.")
198
  try:
199
  base_image = prepare_base_image(uploaded_image, progress)
200
  progress(0.5, desc="Encrypting and embedding data...")
201
- # Note: The image created here is slightly different from the server example.
202
- # This client code uses a different struct packing (`>I` for payload length),
203
- # which is correct. The server code will need to match this.
204
- # Let's adjust the create_encrypted_image to match server expectations
205
- # No, wait, the server code *does* use >I for the *AES key length*, not the *total payload*.
206
- # Let's correct the client to be fully compatible.
207
-
208
  created_image = create_encrypted_image(secret_data, public_key, base_image, overlay_option)
209
  return created_image, f"βœ… Success! Image created for '{service_name}'.", gr.Tabs(selected=1)
210
  except Exception as e:
@@ -215,50 +189,43 @@ def send_keylock_wrapper(service_name: str, image: Image.Image, available_endpoi
215
  if not service_name: raise gr.Error("Please select a target server.")
216
  if image is None: raise gr.Error("Please create or upload an image to send.")
217
  endpoint_details = next((e for e in available_endpoints if e['name'] == service_name), None)
218
- if not endpoint_details or not endpoint_details.get('link'): raise gr.Error(f"Config Error for '{service_name}'.")
219
-
220
  server_url = endpoint_details['link']
221
- api_name = endpoint_details.get('api_endpoint', '/run/keylock-auth-decoder')
222
- status = f"Connecting to remote server: {server_url}"
223
- yield None, status
224
-
225
  try:
226
  with io.BytesIO() as buffer:
227
  image.save(buffer, format="PNG")
228
  b64_string = base64.b64encode(buffer.getvalue()).decode("utf-8")
229
-
230
  client = Client(server_url)
231
- # Using the new payload structure for Gradio > 4.2.0
232
  result = client.predict(image_base64_string=b64_string, api_name=api_name)
233
-
234
- # The result from gradio_client might be the path to a file if it's JSON
235
  if isinstance(result, str) and os.path.exists(result):
236
- with open(result) as f:
237
- decrypted_data = json.load(f)
238
- else:
239
- decrypted_data = result # Assume it's already a dict
240
-
241
  yield decrypted_data, "βœ… Success! Data decrypted by remote server."
242
  except Exception as e:
243
- logger.error(f"Error calling server with gradio_client: {e}", exc_info=True)
244
  yield None, f"❌ Error calling server API: {e}"
245
 
246
- # --- Gradio UI (Your original code is excellent) ---
247
- # ... (The rest of your UI code goes here)
248
- with gr.Blocks(theme="soft", title="KeyLock Operations Dashboard") as demo:
 
249
  endpoints_state = gr.State([])
250
 
251
  gr.Markdown("# πŸ”‘ KeyLock Operations Dashboard")
252
- gr.Markdown("A centralized dashboard to manage and demonstrate the entire KeyLock ecosystem. Key/Image creation is performed locally, while decryption is handled by a **live, remote API call** to a secure server.")
253
 
254
  with gr.Tabs() as tabs:
255
  with gr.TabItem("β‘  Create KeyLock", id=0):
256
- # ... UI components for creating
257
- with gr.Row():
258
  with gr.Column(scale=2):
259
- creator_service_dropdown = gr.Dropdown(label="Target Server", interactive=True)
260
- creator_secret_input = gr.Textbox(lines=5, label="Secret Data to Encrypt", placeholder="API_KEY: sk-123...\nUSER: demo-user")
261
- creator_base_image_input = gr.Image(label="Optional Base Image (600x600)", type="pil", sources=["upload"])
262
  creator_overlay_option = gr.Radio(label="Overlay Content", choices=["Keys and Values", "Keys Only", "None"], value="Keys and Values")
263
  creator_button = gr.Button("✨ Create Auth Image", variant="primary")
264
  with gr.Column(scale=3):
@@ -266,55 +233,63 @@ with gr.Blocks(theme="soft", title="KeyLock Operations Dashboard") as demo:
266
  creator_image_output = gr.Image(label="Generated Encrypted Image", type="pil", show_download_button=True, height=600)
267
 
268
  with gr.TabItem("β‘‘ Send KeyLock", id=1):
269
- # ... UI components for sending
270
- with gr.Row():
271
  with gr.Column(scale=1):
272
  send_service_dropdown = gr.Dropdown(label="Target Server", interactive=True)
273
- client_image_input = gr.Image(type="pil", label="Upload or Drag Encrypted Image Here", sources=["upload", "clipboard"])
274
  client_button = gr.Button("πŸ”“ Decrypt via Remote Server", variant="primary")
275
  with gr.Column(scale=1):
276
  client_status = gr.Textbox(label="Status", interactive=False, lines=2)
277
- client_json_output = gr.JSON(label="Decrypted Data")
278
-
279
  with gr.TabItem("ℹ️ Info & Key Generation", id=2):
280
- # ... UI components for info
281
- with gr.Accordion("πŸ”‘ RSA Key Pair Generator", open=False):
282
- output_public_key = gr.Textbox(lines=10, label="Generated Public Key", interactive=False, show_copy_button=True)
283
- output_private_key = gr.Textbox(lines=10, label="Generated Private Key", interactive=False, show_copy_button=True)
284
- gen_keys_button = gr.Button("βš™οΈ Generate New 2048-bit Key Pair")
 
 
 
 
 
 
285
 
286
  # --- Event Handlers ---
 
 
287
  def refresh_and_update_all():
288
- for dropdown_update, status_update, state_update in get_server_list():
289
- pass # Exhaust the generator
290
- # Sync the dropdowns
291
- creator_service_dropdown.update(**dropdown_update.model_dump())
292
- send_service_dropdown.update(**dropdown_update.model_dump())
293
- return dropdown_update, status_update, state_update
294
 
295
- demo.load(
296
- fn=get_server_list,
297
- outputs=[creator_service_dropdown, creator_status, endpoints_state]
298
- ).then(
299
- lambda x: x,
300
- inputs=creator_service_dropdown,
301
- outputs=send_service_dropdown
302
  )
303
-
304
- gen_keys_button.click(fn=generate_rsa_keys, inputs=None, outputs=[output_private_key, output_public_key])
305
  creator_button.click(
306
  fn=create_keylock_wrapper,
307
  inputs=[creator_service_dropdown, creator_secret_input, endpoints_state, creator_base_image_input, creator_overlay_option],
308
  outputs=[creator_image_output, creator_status, tabs]
309
  )
 
310
  client_button.click(
311
- fn=send_keylock_wrapper,
312
- inputs=[send_service_dropdown, client_image_input, endpoints_state],
313
  outputs=[client_json_output, client_status]
314
  )
315
 
316
- # When a new image is created, also update the sender tab's image
317
- creator_image_output.change(lambda x: x, inputs=creator_image_output, outputs=client_image_input)
 
 
 
 
318
 
319
  if __name__ == "__main__":
320
  demo.launch()
 
 
 
 
 
 
 
1
  import gradio as gr
2
  from PIL import Image, ImageDraw, ImageFont, ImageOps
3
  import base64
 
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
 
20
  logger = logging.getLogger(__name__)
21
 
22
  # --- Constants ---
 
23
  ENDPOINTS_CONFIG_PATH = "endpoints.json"
24
  DEFAULT_IMAGE_URL = "https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?q=80&w=1200&auto=format&fit=crop"
25
  IMAGE_SIZE = 600
26
  T2I_MODEL = "sd-community/sdxl-lightning"
27
  T2I_PROMPT = "A stunning view of a distant galaxy, nebulae, and constellations, digital art, vibrant colors, cinematic lighting, 8k, masterpiece."
28
 
29
+ # --- Helper Functions for Image Handling ---
30
+
31
  def resize_and_crop(img: Image.Image, size: int = IMAGE_SIZE) -> Image.Image:
32
  """Resizes an image to fit within a square of `size` and then center-crops it."""
33
  try:
 
37
  return img.resize((size, size), Image.Resampling.LANCZOS)
38
 
39
  def prepare_base_image(uploaded_image: Image.Image | None, progress) -> Image.Image:
40
+ """Provides a base image using a fallback logic, updating a Gradio progress object."""
 
 
41
  if uploaded_image:
42
  progress(0, desc="βœ… Using uploaded image...")
 
43
  return resize_and_crop(uploaded_image)
44
  try:
45
+ progress(0, desc="⏳ Fetching default background...")
 
46
  response = requests.get(DEFAULT_IMAGE_URL, timeout=15)
47
  response.raise_for_status()
48
  img = Image.open(io.BytesIO(response.content)).convert("RGB")
 
51
  except Exception as e:
52
  logger.warning(f"Could not fetch default image: {e}. Falling back to AI generation.")
53
  try:
54
+ progress(0, desc=f"⏳ Generating image with {T2I_MODEL}...")
 
55
  client = InferenceClient()
56
  image_bytes = client.text_to_image(T2I_PROMPT, model=T2I_MODEL)
57
  img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
58
+ progress(0, desc="βœ… New image generated.")
59
  return resize_and_crop(img)
60
  except Exception as e:
61
  logger.error(f"Fatal: All image sources failed. Text-to-image failed with: {e}")
62
  raise gr.Error(f"Failed to obtain a base image. AI generation error: {e}")
63
 
64
+ # --- Core Cryptography and Image Creation ---
65
+
66
  def generate_rsa_keys():
67
+ """Generates a new 2048-bit RSA private and public key pair."""
68
+ private_key = rsa.generate_private_key(
69
+ public_exponent=65537,
70
+ key_size=2048
71
+ )
72
+ private_pem = private_key.private_bytes(
73
+ encoding=serialization.Encoding.PEM,
74
+ format=serialization.PrivateFormat.PKCS8,
75
+ encryption_algorithm=serialization.NoEncryption()
76
+ ).decode('utf-8')
77
+ public_pem = private_key.public_key().public_bytes(
78
+ encoding=serialization.Encoding.PEM,
79
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
80
+ ).decode('utf-8')
81
  return private_pem, public_pem
82
 
83
  def create_encrypted_image(
 
86
  base_image: Image.Image,
87
  overlay_option: str
88
  ) -> Image.Image:
89
+ """Encrypts data and embeds it into an image using LSB steganography."""
90
+ if not secret_data_str.strip(): raise ValueError("Secret data cannot be empty.")
91
+ if not public_key_pem.strip(): raise ValueError("Public Key cannot be empty.")
92
+
93
  data_dict = {}
94
  for line in secret_data_str.splitlines():
95
  line = line.strip()
96
  if not line or line.startswith('#'): continue
97
  parts = line.split(':', 1) if ':' in line else line.split('=', 1)
98
+ if len(parts) == 2: data_dict[parts[0].strip()] = parts[1].strip().strip("'\"")
99
+ if not data_dict: raise ValueError("No valid key-value pairs found.")
100
+
 
101
  json_bytes = json.dumps(data_dict).encode('utf-8')
102
  public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
103
  aes_key, nonce = os.urandom(32), os.urandom(12)
104
  ciphertext = AESGCM(aes_key).encrypt(nonce, json_bytes, None)
105
  rsa_encrypted_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
106
+
107
+ # Payload format: [RSA Enc Key Len (4B)] [RSA Enc Key (var)] [Nonce (12B)] [Ciphertext (var)]
108
  encrypted_payload = struct.pack('>I', len(rsa_encrypted_key)) + rsa_encrypted_key + nonce + ciphertext
109
+
110
  img = base_image.copy().convert("RGB")
111
  width, height = img.size
112
 
113
+ # Add Stylish Overlays
114
+ # (Your existing overlay code is great and remains unchanged)
115
  draw = ImageDraw.Draw(img, "RGBA")
116
  try:
117
  font_bold = ImageFont.truetype("DejaVuSans-Bold.ttf", 30)
 
120
  font_bold = ImageFont.load_default(size=28)
121
  font_regular = ImageFont.load_default(size=14)
122
 
123
+ overlay_color = (15, 23, 42, 190); title_color = (226, 232, 240)
124
+ key_color = (148, 163, 184); value_color = (241, 245, 249)
 
 
 
125
 
126
  draw.rectangle([0, 20, width, 80], fill=overlay_color)
127
  draw.text((width / 2, 50), "KeyLock Secure Data", fill=title_color, font=font_bold, anchor="ms")
128
 
129
  if overlay_option != "None":
130
+ lines = list(data_dict.keys()) if overlay_option == "Keys Only" else [f"{k}: {v}" for k, v in data_dict.items()]
 
 
 
 
 
 
 
 
131
  line_heights = [draw.textbbox((0, 0), line, font=font_regular)[3] for line in lines]
132
+ total_text_height = sum(line_heights) + (len(lines) - 1) * 6
133
+ box_height = total_text_height + 30
134
  box_y0 = height - box_height - 20
 
135
  draw.rectangle([20, box_y0, width - 20, height - 20], fill=overlay_color)
136
+ current_y = box_y0 + 15
137
+ for i, (key, value) in enumerate(data_dict.items()):
138
+ if overlay_option == "Keys Only":
139
+ draw.text((35, current_y), key, fill=key_color, font=font_regular)
140
+ else:
141
+ key_text = f"{key}:"; draw.text((35, current_y), key_text, fill=key_color, font=font_regular)
142
+ key_bbox = draw.textbbox((35, current_y), key_text, font=font_regular)
 
 
 
 
143
  draw.text((key_bbox[2] + 8, current_y), str(value), fill=value_color, font=font_regular)
144
+ current_y += line_heights[i] + 6
145
 
146
+ # Steganography Payload: [Total Crypto Payload Length (4B)] [Crypto Payload]
147
  pixel_data = np.array(img.convert("RGB")).ravel()
148
  header = struct.pack('>I', len(encrypted_payload))
149
  binary_payload = ''.join(format(b, '08b') for b in header + encrypted_payload)
150
+
151
  if len(binary_payload) > pixel_data.size:
152
+ raise ValueError(f"Data is too large for the image. Max bytes: {pixel_data.size // 8}.")
153
+
154
  pixel_data[:len(binary_payload)] = (pixel_data[:len(binary_payload)] & 0xFE) | np.array(list(binary_payload), dtype=np.uint8)
155
  stego_pixels = pixel_data.reshape((height, width, 3))
156
  return Image.fromarray(stego_pixels, 'RGB')
157
 
 
158
  # --- Gradio Wrapper Functions ---
159
+
160
  def get_server_list():
161
  status = f"Fetching server list from '{ENDPOINTS_CONFIG_PATH}'..."
162
  yield gr.Dropdown(choices=[], value=None, label="⏳ Fetching..."), status, []
163
  try:
164
  with open(ENDPOINTS_CONFIG_PATH, "r") as f:
165
  all_entries = json.load(f)
 
166
  valid_endpoints = [e for e in all_entries if isinstance(e, dict) and "name" in e and "public_key" in e and "link" in e]
167
+ if not valid_endpoints: raise ValueError("No valid servers found in config file.")
 
168
  endpoint_names = [e['name'] for e in valid_endpoints]
169
+ status = f"βœ… Found {len(endpoint_names)} valid servers."
170
  yield gr.Dropdown(choices=endpoint_names, value=endpoint_names[0] if endpoint_names else None, label="Target Server"), status, valid_endpoints
171
  except Exception as e:
172
  status = f"❌ Error loading configuration: {e}"
 
173
  yield gr.Dropdown(choices=[], value=None, label="Error fetching servers"), status, []
174
 
 
175
  def create_keylock_wrapper(service_name: str, secret_data: str, available_endpoints: list, uploaded_image: Image.Image | None, overlay_option: str, progress=gr.Progress(track_tqdm=True)):
176
+ if not service_name: raise gr.Error("Please select a target server.")
 
177
  public_key = next((e['public_key'] for e in available_endpoints if e['name'] == service_name), None)
178
+ if not public_key: raise gr.Error(f"Could not find public key for '{service_name}'.")
 
179
  try:
180
  base_image = prepare_base_image(uploaded_image, progress)
181
  progress(0.5, desc="Encrypting and embedding data...")
 
 
 
 
 
 
 
182
  created_image = create_encrypted_image(secret_data, public_key, base_image, overlay_option)
183
  return created_image, f"βœ… Success! Image created for '{service_name}'.", gr.Tabs(selected=1)
184
  except Exception as e:
 
189
  if not service_name: raise gr.Error("Please select a target server.")
190
  if image is None: raise gr.Error("Please create or upload an image to send.")
191
  endpoint_details = next((e for e in available_endpoints if e['name'] == service_name), None)
192
+ if not endpoint_details: raise gr.Error(f"Config Error for '{service_name}'.")
 
193
  server_url = endpoint_details['link']
194
+ api_name = endpoint_details.get('api_endpoint', '/run/keylock-auth-decoder') # Default API name
195
+ yield None, f"Connecting to remote server: {server_url}"
 
 
196
  try:
197
  with io.BytesIO() as buffer:
198
  image.save(buffer, format="PNG")
199
  b64_string = base64.b64encode(buffer.getvalue()).decode("utf-8")
 
200
  client = Client(server_url)
 
201
  result = client.predict(image_base64_string=b64_string, api_name=api_name)
202
+
203
+ # Gradio_client can return a filepath for JSON, so we handle that case.
204
  if isinstance(result, str) and os.path.exists(result):
205
+ with open(result) as f: decrypted_data = json.load(f)
206
+ else: decrypted_data = result
207
+
 
 
208
  yield decrypted_data, "βœ… Success! Data decrypted by remote server."
209
  except Exception as e:
 
210
  yield None, f"❌ Error calling server API: {e}"
211
 
212
+ # --- Gradio UI ---
213
+ theme = gr.themes.Soft(primary_hue="blue", secondary_hue="sky", neutral_hue="slate")
214
+
215
+ with gr.Blocks(theme=theme, title="KeyLock Operations Dashboard") as demo:
216
  endpoints_state = gr.State([])
217
 
218
  gr.Markdown("# πŸ”‘ KeyLock Operations Dashboard")
219
+ gr.Markdown("A centralized dashboard to create and test encrypted images against live KeyLock servers.")
220
 
221
  with gr.Tabs() as tabs:
222
  with gr.TabItem("β‘  Create KeyLock", id=0):
223
+ gr.Markdown("## Step 1: Create an Encrypted Image (Local)")
224
+ with gr.Row(variant="panel"):
225
  with gr.Column(scale=2):
226
+ creator_service_dropdown = gr.Dropdown(label="Target Server", interactive=True, info="Select the API server to encrypt data for.")
227
+ creator_secret_input = gr.Textbox(lines=5, label="Secret Data", placeholder="API_KEY: sk-123...\nUSER: demo-user")
228
+ creator_base_image_input = gr.Image(label="Optional Base Image (600x600 recommended)", type="pil", sources=["upload"])
229
  creator_overlay_option = gr.Radio(label="Overlay Content", choices=["Keys and Values", "Keys Only", "None"], value="Keys and Values")
230
  creator_button = gr.Button("✨ Create Auth Image", variant="primary")
231
  with gr.Column(scale=3):
 
233
  creator_image_output = gr.Image(label="Generated Encrypted Image", type="pil", show_download_button=True, height=600)
234
 
235
  with gr.TabItem("β‘‘ Send KeyLock", id=1):
236
+ gr.Markdown("## Step 2: Decrypt via Live API Call")
237
+ with gr.Row(variant="panel"):
238
  with gr.Column(scale=1):
239
  send_service_dropdown = gr.Dropdown(label="Target Server", interactive=True)
240
+ client_image_input = gr.Image(type="pil", label="Upload or Drag Encrypted Image", sources=["upload", "clipboard"])
241
  client_button = gr.Button("πŸ”“ Decrypt via Remote Server", variant="primary")
242
  with gr.Column(scale=1):
243
  client_status = gr.Textbox(label="Status", interactive=False, lines=2)
244
+ client_json_output = gr.JSON(label="Decrypted Data from Server")
245
+
246
  with gr.TabItem("ℹ️ Info & Key Generation", id=2):
247
+ gr.Markdown("## About the KeyLock System")
248
+ gr.Markdown("This dashboard uses the `endpoints.json` file to discover servers. Each server holds a secret **private key**. This client uses the corresponding **public key** to encrypt data that only that specific server can decrypt.")
249
+
250
+ with gr.Accordion("πŸ”‘ RSA Key Pair Generator", open=True):
251
+ gr.Markdown("Use this utility to generate a new key pair for a new server. Add the **Public Key** to `endpoints.json` and set the **Private Key** as a secret on the server.")
252
+ gen_keys_button = gr.Button("βš™οΈ Generate New 2048-bit Key Pair", variant="secondary")
253
+ with gr.Row():
254
+ with gr.Column():
255
+ output_public_key = gr.Textbox(lines=10, label="Generated Public Key", interactive=False, show_copy_button=True)
256
+ with gr.Column():
257
+ output_private_key = gr.Textbox(lines=10, label="Generated Private Key (Keep this secret!)", interactive=False, show_copy_button=True)
258
 
259
  # --- Event Handlers ---
260
+ def sync_dropdowns(x): return x
261
+
262
  def refresh_and_update_all():
263
+ # This generator pattern is for Gradio's streaming output
264
+ for dropdown_update, status_update, state_update in get_server_list(): pass
265
+ return dropdown_update, dropdown_update, status_update, state_update
 
 
 
266
 
267
+ demo.load(fn=refresh_and_update_all, outputs=[creator_service_dropdown, send_service_dropdown, creator_status, endpoints_state])
268
+
269
+ gen_keys_button.click(
270
+ fn=generate_rsa_keys,
271
+ inputs=None,
272
+ outputs=[output_private_key, output_public_key]
 
273
  )
274
+
 
275
  creator_button.click(
276
  fn=create_keylock_wrapper,
277
  inputs=[creator_service_dropdown, creator_secret_input, endpoints_state, creator_base_image_input, creator_overlay_option],
278
  outputs=[creator_image_output, creator_status, tabs]
279
  )
280
+
281
  client_button.click(
282
+ fn=send_keylock_wrapper,
283
+ inputs=[send_service_dropdown, client_image_input, endpoints_state],
284
  outputs=[client_json_output, client_status]
285
  )
286
 
287
+ # Sync the creator/sender dropdowns when one changes
288
+ creator_service_dropdown.change(fn=sync_dropdowns, inputs=creator_service_dropdown, outputs=send_service_dropdown)
289
+ send_service_dropdown.change(fn=sync_dropdowns, inputs=send_service_dropdown, outputs=creator_service_dropdown)
290
+
291
+ # When a new image is created, auto-populate it in the sender tab
292
+ creator_image_output.change(fn=lambda x: x, inputs=creator_image_output, outputs=client_image_input)
293
 
294
  if __name__ == "__main__":
295
  demo.launch()