broadfield-dev commited on
Commit
6c312f4
Β·
verified Β·
1 Parent(s): 6a2d885

Update client/app.py

Browse files
Files changed (1) hide show
  1. client/app.py +105 -87
client/app.py CHANGED
@@ -20,13 +20,12 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(level
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."""
@@ -61,7 +60,7 @@ def prepare_base_image(uploaded_image: Image.Image | None, progress) -> Image.Im
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."""
@@ -103,15 +102,12 @@ def create_encrypted_image(
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)
@@ -119,13 +115,10 @@ def create_encrypted_image(
119
  except IOError:
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]
@@ -143,152 +136,177 @@ def create_encrypted_image(
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:
185
  logger.error(f"Error creating image: {e}", exc_info=True)
186
  return None, f"❌ Error: {e}", gr.Tabs()
187
 
188
- def send_keylock_wrapper(service_name: str, image: Image.Image, available_endpoints: list):
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):
232
- creator_status = gr.Textbox(label="Status", interactive=False, lines=1)
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__":
 
20
  logger = logging.getLogger(__name__)
21
 
22
  # --- Constants ---
 
23
  DEFAULT_IMAGE_URL = "https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?q=80&w=1200&auto=format&fit=crop"
24
  IMAGE_SIZE = 600
25
  T2I_MODEL = "sd-community/sdxl-lightning"
26
  T2I_PROMPT = "A stunning view of a distant galaxy, nebulae, and constellations, digital art, vibrant colors, cinematic lighting, 8k, masterpiece."
27
 
28
+ # --- Helper Functions for Image Handling (Unchanged) ---
29
 
30
  def resize_and_crop(img: Image.Image, size: int = IMAGE_SIZE) -> Image.Image:
31
  """Resizes an image to fit within a square of `size` and then center-crops it."""
 
60
  logger.error(f"Fatal: All image sources failed. Text-to-image failed with: {e}")
61
  raise gr.Error(f"Failed to obtain a base image. AI generation error: {e}")
62
 
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."""
 
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)
 
115
  except IOError:
116
  font_bold = ImageFont.load_default(size=28)
117
  font_regular = ImageFont.load_default(size=14)
118
+ overlay_color, title_color = (15, 23, 42, 190), (226, 232, 240)
119
+ key_color, value_color = (148, 163, 184), (241, 245, 249)
 
 
120
  draw.rectangle([0, 20, width, 80], fill=overlay_color)
121
  draw.text((width / 2, 50), "KeyLock Secure Data", fill=title_color, font=font_bold, anchor="ms")
 
122
  if overlay_option != "None":
123
  lines = list(data_dict.keys()) if overlay_option == "Keys Only" else [f"{k}: {v}" for k, v in data_dict.items()]
124
  line_heights = [draw.textbbox((0, 0), line, font=font_regular)[3] for line in lines]
 
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')
150
 
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
+
 
221
  if isinstance(result, str) and os.path.exists(result):
222
+ with open(result, 'r', encoding='utf-8') as f: decrypted_data = json.load(f)
223
  else: decrypted_data = result
224
+
225
  yield decrypted_data, "βœ… Success! Data decrypted by remote server."
226
  except Exception as e:
227
+ logger.error(f"Error sending to server: {e}", exc_info=True)
228
  yield None, f"❌ Error calling server API: {e}"
229
 
230
  # --- Gradio UI ---
231
  theme = gr.themes.Soft(primary_hue="blue", secondary_hue="sky", neutral_hue="slate")
232
 
233
  with gr.Blocks(theme=theme, title="KeyLock Operations Dashboard") as demo:
234
+ servers_state = gr.State([])
235
 
236
  gr.Markdown("# πŸ”‘ KeyLock Operations Dashboard")
237
+ gr.Markdown("A client to discover KeyLock servers, create encrypted images, and test decryption against live APIs.")
238
+
239
+ with gr.Accordion("πŸ”— Add a KeyLock Server", open=True):
240
+ with gr.Row():
241
+ server_url_input = gr.Textbox(label="Server Base URL", placeholder="https://your-hf-space-name.hf.space")
242
+ add_server_button = gr.Button("Test & Add Server", variant="secondary")
243
+ global_status = gr.Textbox(label="Status", interactive=False, lines=1, value="Enter a server URL to begin.")
244
 
245
  with gr.Tabs() as tabs:
246
  with gr.TabItem("β‘  Create KeyLock", id=0):
 
247
  with gr.Row(variant="panel"):
248
  with gr.Column(scale=2):
249
+ creator_service_dropdown = gr.Dropdown(label="Target Server", interactive=True, info="Select a server to encrypt data for.")
250
+ creator_secret_input = gr.Textbox(lines=5, label="Secret Data (key:value format)", placeholder="API_KEY: sk-123...\nUSER: demo-user")
251
  creator_base_image_input = gr.Image(label="Optional Base Image (600x600 recommended)", type="pil", sources=["upload"])
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")
265
+
266
+ with gr.TabItem("ℹ️ Key Generation", id=2):
 
 
 
267
  with gr.Accordion("πŸ”‘ RSA Key Pair Generator", open=True):
268
+ gr.Markdown("Use this utility to generate a new key pair for a new server. The server administrator must configure the server with the private key.")
269
+ gen_keys_button = gr.Button("βš™οΈ Generate New 2048-bit Key Pair")
270
+ output_private_key = gr.Textbox(lines=10, label="Generated Private Key (KEEP THIS SECRET!)", interactive=False, show_copy_button=True)
271
+ output_public_key = gr.Textbox(lines=10, label="Generated Public Key", interactive=False, show_copy_button=True)
 
 
 
272
 
273
  # --- Event Handlers ---
274
  def sync_dropdowns(x): return x
275
+
276
+ def update_dropdowns_from_state(servers_list):
277
+ server_names = [s['name'] for s in servers_list] if servers_list else []
278
+ selected = server_names[-1] if server_names else None
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(
303
+ fn=send_keylock_wrapper,
304
+ inputs=[send_service_dropdown, client_image_input, servers_state],
305
+ outputs=[client_json_output, global_status]
306
  )
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__":