broadfield-dev commited on
Commit
17f05fd
Β·
verified Β·
1 Parent(s): d4b0251

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -83
app.py CHANGED
@@ -1,5 +1,5 @@
1
  import gradio as gr
2
- from PIL import Image, ImageDraw, ImageFont
3
  import base64
4
  import io
5
  import json
@@ -13,10 +13,12 @@ from cryptography.hazmat.primitives.asymmetric import rsa, padding
13
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
  from cryptography.hazmat.primitives import hashes
15
  from gradio_client import Client
 
16
 
17
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18
  logger = logging.getLogger(__name__)
19
 
 
20
  CREATOR_ENDPOINTS_JSON_URL = "https://huggingface.co/spaces/broadfield-dev/KeyLock-Auth-Creator/raw/main/endpoints.json"
21
  BASE_HF_URL = "https://huggingface.co/spaces/"
22
  CREATOR_SPACE_ID = "broadfield-dev/KeyLock-Auth-Creator"
@@ -26,21 +28,77 @@ SERVER_URL = f"{BASE_HF_URL}{SERVER_SPACE_ID}"
26
  CREATOR_APP_PY_URL = f"{CREATOR_URL}/raw/main/app.py"
27
  SERVER_APP_PY_URL = f"{SERVER_URL}/raw/main/app.py"
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  def generate_rsa_keys():
31
  private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
32
- private_pem = private_key.private_bytes(
33
- encoding=serialization.Encoding.PEM,
34
- format=serialization.PrivateFormat.PKCS8,
35
- encryption_algorithm=serialization.NoEncryption()
36
- ).decode('utf-8')
37
- public_pem = private_key.public_key().public_bytes(
38
- encoding=serialization.Encoding.PEM,
39
- format=serialization.PublicFormat.SubjectPublicKeyInfo
40
- ).decode('utf-8')
41
  return private_pem, public_pem
42
 
43
- def create_encrypted_image(secret_data_str: str, public_key_pem: str) -> Image.Image:
 
 
 
 
 
44
  if not secret_data_str.strip():
45
  raise ValueError("Secret data cannot be empty.")
46
  if not public_key_pem.strip():
@@ -49,8 +107,7 @@ def create_encrypted_image(secret_data_str: str, public_key_pem: str) -> Image.I
49
  data_dict = {}
50
  for line in secret_data_str.splitlines():
51
  line = line.strip()
52
- if not line or line.startswith('#'):
53
- continue
54
  parts = line.split(':', 1) if ':' in line else line.split('=', 1)
55
  if len(parts) == 2:
56
  data_dict[parts[0].strip()] = parts[1].strip().strip("'\"")
@@ -64,32 +121,73 @@ def create_encrypted_image(secret_data_str: str, public_key_pem: str) -> Image.I
64
  aes_key, nonce = os.urandom(32), os.urandom(12)
65
  ciphertext = AESGCM(aes_key).encrypt(nonce, json_bytes, None)
66
 
67
- rsa_encrypted_key = public_key.encrypt(
68
- aes_key,
69
- padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
70
- )
71
 
72
  encrypted_payload = struct.pack('>I', len(rsa_encrypted_key)) + rsa_encrypted_key + nonce + ciphertext
73
 
74
- img = Image.new('RGB', (800, 600), color=(45, 52, 54))
75
- draw = ImageDraw.Draw(img)
 
 
 
 
76
  try:
77
- font = ImageFont.truetype("DejaVuSans.ttf", 40)
 
 
78
  except IOError:
79
- font = ImageFont.load_default(size=30)
80
- draw.text((400, 300), "KeyLock Secure Data", fill=(223, 230, 233), font=font, anchor="ms")
81
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  pixel_data = np.array(img.convert("RGB")).ravel()
83
  binary_payload = ''.join(format(b, '08b') for b in struct.pack('>I', len(encrypted_payload)) + encrypted_payload)
84
 
85
  if len(binary_payload) > pixel_data.size:
86
- raise ValueError(f"Data is too large for the image. Max size: {pixel_data.size // 8} bytes.")
87
 
88
  pixel_data[:len(binary_payload)] = (pixel_data[:len(binary_payload)] & 0xFE) | np.array(list(binary_payload), dtype=np.uint8)
89
- stego_pixels = pixel_data.reshape((600, 800, 3))
90
 
91
  return Image.fromarray(stego_pixels, 'RGB')
92
 
 
 
93
  def get_server_list():
94
  status = "Fetching server list from remote config..."
95
  yield gr.Dropdown(choices=[], value=None, label="⏳ Fetching..."), status, []
@@ -97,62 +195,43 @@ def get_server_list():
97
  response = requests.get(CREATOR_ENDPOINTS_JSON_URL, timeout=10)
98
  response.raise_for_status()
99
  all_entries = response.json()
100
- valid_endpoints = []
101
-
102
- for entry in all_entries:
103
- if not isinstance(entry, dict) or "name" not in entry or "public_key" not in entry:
104
- logger.warning(f"Skipping invalid entry (missing name or public_key): {entry}")
105
- continue
106
-
107
- if "api_endpoint" not in entry:
108
- if "link" in entry:
109
- base_url = entry["link"].strip("/")
110
- entry["api_endpoint"] = f"{base_url}/run/predict/"
111
- logger.info(f"Auto-generating API endpoint for '{entry['name']}': {entry['api_endpoint']}")
112
- else:
113
- logger.warning(f"Skipping invalid entry '{entry['name']}' (missing 'api_endpoint' and 'link'): {entry}")
114
- continue
115
-
116
- valid_endpoints.append(entry)
117
-
118
  if not valid_endpoints:
119
- raise ValueError("No valid server configurations found in the remote file.")
120
-
121
  endpoint_names = [e['name'] for e in valid_endpoints]
122
  status = f"βœ… Success! Found {len(endpoint_names)} valid servers."
123
  yield gr.Dropdown(choices=endpoint_names, value=endpoint_names[0] if endpoint_names else None, label="Target Server"), status, valid_endpoints
124
  except Exception as e:
125
- status = f"❌ Error fetching or parsing configuration: {e}"
126
  logger.error(status)
127
  yield gr.Dropdown(choices=[], value=None, label="Error fetching servers"), status, []
128
 
129
- def create_keylock_wrapper(service_name: str, secret_data: str, available_endpoints: list):
130
  if not service_name:
131
  raise gr.Error("Please select a target server.")
132
  public_key = next((e['public_key'] for e in available_endpoints if e['name'] == service_name), None)
133
  if not public_key:
134
  raise gr.Error(f"Could not find public key for '{service_name}'. Please refresh the server list.")
135
  try:
136
- created_image = create_encrypted_image(secret_data, public_key)
 
 
 
 
 
 
137
  return created_image, f"βœ… Success! Image created for '{service_name}'."
138
  except Exception as e:
139
- logger.error(f"Error creating image: {e}")
140
  return None, f"❌ Error: {e}"
141
 
142
  def send_keylock_wrapper(service_name: str, image: Image.Image, available_endpoints: list):
143
- if not service_name:
144
- raise gr.Error("Please select a target server.")
145
- if image is None:
146
- raise gr.Error("Please create or upload an image to send.")
147
-
148
  endpoint_details = next((e for e in available_endpoints if e['name'] == service_name), None)
149
- if not endpoint_details:
150
- raise gr.Error(f"Configuration Error: Could not find details for '{service_name}'. Refresh the list.")
151
 
152
- server_url = endpoint_details.get('link')
153
- if not server_url:
154
- raise gr.Error(f"Configuration Error: The selected server '{service_name}' is missing a 'link' to its Space.")
155
-
156
  status = f"Connecting to remote server: {server_url}"
157
  yield None, status
158
 
@@ -162,28 +241,20 @@ def send_keylock_wrapper(service_name: str, image: Image.Image, available_endpoi
162
  b64_string = base64.b64encode(buffer.getvalue()).decode("utf-8")
163
 
164
  client = Client(server_url)
165
- result = client.predict(
166
- image_base64_string=b64_string,
167
- api_name="/keylock-auth-decoder"
168
- )
169
-
170
- decrypted_data = result
171
- if isinstance(decrypted_data, str):
172
- try:
173
- decrypted_data = json.loads(decrypted_data)
174
- except json.JSONDecodeError:
175
- pass
176
  yield decrypted_data, "βœ… Success! Data decrypted by remote server."
177
 
178
  except Exception as e:
179
- logger.error(f"Error calling server with gradio_client: {e}")
180
  yield None, f"❌ Error calling server API: {e}"
181
 
182
  def refresh_and_update_all():
183
- for dropdown_update, status_update, state_update in get_server_list():
184
- pass
185
  return dropdown_update, dropdown_update, status_update, state_update
186
 
 
 
187
  theme = gr.themes.Base(
188
  primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky, neutral_hue=gr.themes.colors.slate,
189
  font=(gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"),
@@ -202,22 +273,28 @@ with gr.Blocks(theme=theme, title="KeyLock Operations Dashboard") as demo:
202
  with gr.Tabs() as tabs:
203
  with gr.TabItem("β‘  Create KeyLock", id=0):
204
  gr.Markdown("## Step 1: Create an Encrypted Authentication Image (Local)")
205
- gr.Markdown("# ❗ DEMO: Don't send personal information encrypted to the demo server")
206
- gr.Markdown(f"This tool acts as the **Auth Creator**. It fetches a list of available servers from a public [configuration file]({CREATOR_ENDPOINTS_JSON_URL}), then uses the selected server's public key to encrypt your data into a PNG. **This entire creation process happens locally.**")
207
  with gr.Row(variant="panel"):
208
  with gr.Column(scale=2):
209
  with gr.Row():
210
  creator_service_dropdown = gr.Dropdown(label="Target Server", interactive=True, info="Select the API server to encrypt data for.")
211
  refresh_button = gr.Button("πŸ”„", scale=0, size="sm")
212
- creator_secret_input = gr.Textbox(lines=8, label="Secret Data to Encrypt", placeholder="API_KEY: sk-123...\nUSER: demo-user")
213
- creator_button = gr.Button("✨ Create Auth Image", variant="primary")
214
- with gr.Column(scale=1):
215
- creator_status = gr.Textbox(label="Status", interactive=False, lines=2)
216
- creator_image_output = gr.Image(label="Generated Encrypted Image", type="pil", show_download_button=True, format="png")
 
 
 
 
 
 
 
217
 
218
  with gr.TabItem("β‘‘ Send KeyLock", id=1):
219
  gr.Markdown("## Step 2: Decrypt via Live API Call")
220
- gr.Markdown("This tool acts as the **Client**. It sends the encrypted image you created in Step 1 to the live, remote **Decoder Server** you select from the same configuration list. The server uses its securely stored private key to decrypt the data and sends the result back.")
221
  with gr.Row(variant="panel"):
222
  with gr.Column(scale=1):
223
  gr.Markdown("### Configuration")
@@ -248,10 +325,17 @@ with gr.Blocks(theme=theme, title="KeyLock Operations Dashboard") as demo:
248
  output_private_key = gr.Textbox(lines=10, label="Generated Private Key", interactive=False, show_copy_button=True)
249
  gen_keys_button = gr.Button("βš™οΈ Generate New 2048-bit Key Pair", variant="secondary")
250
 
 
251
  gen_keys_button.click(fn=generate_rsa_keys, inputs=None, outputs=[output_private_key, output_public_key])
252
  refresh_button.click(fn=refresh_and_update_all, outputs=[creator_service_dropdown, send_service_dropdown, creator_status, endpoints_state])
253
  demo.load(fn=refresh_and_update_all, outputs=[creator_service_dropdown, send_service_dropdown, creator_status, endpoints_state])
254
- creator_button.click(fn=create_keylock_wrapper, inputs=[creator_service_dropdown, creator_secret_input, endpoints_state], outputs=[creator_image_output, creator_status])
 
 
 
 
 
 
255
  client_button.click(fn=send_keylock_wrapper, inputs=[send_service_dropdown, client_image_input, endpoints_state], outputs=[client_json_output, client_status])
256
 
257
  if __name__ == "__main__":
 
1
  import gradio as gr
2
+ from PIL import Image, ImageDraw, ImageFont, ImageOps
3
  import base64
4
  import io
5
  import json
 
13
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
  from cryptography.hazmat.primitives import hashes
15
  from gradio_client import Client
16
+ from huggingface_hub import InferenceClient
17
 
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
19
  logger = logging.getLogger(__name__)
20
 
21
+ # --- Constants ---
22
  CREATOR_ENDPOINTS_JSON_URL = "https://huggingface.co/spaces/broadfield-dev/KeyLock-Auth-Creator/raw/main/endpoints.json"
23
  BASE_HF_URL = "https://huggingface.co/spaces/"
24
  CREATOR_SPACE_ID = "broadfield-dev/KeyLock-Auth-Creator"
 
28
  CREATOR_APP_PY_URL = f"{CREATOR_URL}/raw/main/app.py"
29
  SERVER_APP_PY_URL = f"{SERVER_URL}/raw/main/app.py"
30
 
31
+ # NEW: Constants for improved image generation
32
+ DEFAULT_IMAGE_URL = "https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?q=80&w=1200&auto=format&fit=crop"
33
+ IMAGE_SIZE = 600
34
+ T2I_MODEL = "sd-community/sdxl-lightning"
35
+ T2I_PROMPT = "A stunning view of a distant galaxy, nebulae, and constellations, digital art, vibrant colors, cinematic lighting, 8k, masterpiece."
36
+
37
+ # --- Helper Functions for Image Handling ---
38
+
39
+ def resize_and_crop(img: Image.Image, size: int = IMAGE_SIZE) -> Image.Image:
40
+ """Resizes an image to fit within a square of `size` and then center-crops it."""
41
+ try:
42
+ # ImageOps.fit is perfect for this: it resizes while maintaining aspect ratio and crops from the center.
43
+ return ImageOps.fit(img, (size, size), Image.Resampling.LANCZOS)
44
+ except Exception as e:
45
+ logger.error(f"Failed to resize and crop image: {e}")
46
+ # Fallback to a simple resize if 'fit' fails
47
+ return img.resize((size, size), Image.Resampling.LANCZOS)
48
+
49
+ def prepare_base_image(uploaded_image: Image.Image | None, progress) -> Image.Image:
50
+ """
51
+ Provides a base image using the fallback logic, updating a Gradio progress object.
52
+ 1. User-uploaded image.
53
+ 2. Default URL image.
54
+ 3. AI-generated image.
55
+ All images are resized and cropped to a standard size.
56
+ """
57
+ # 1. User-uploaded image
58
+ if uploaded_image:
59
+ progress(0, desc="βœ… Using uploaded image...")
60
+ logger.info("Using user-uploaded image.")
61
+ return resize_and_crop(uploaded_image)
62
+
63
+ # 2. Default URL image
64
+ try:
65
+ progress(0, desc="⏳ No image uploaded. Fetching default background...")
66
+ logger.info(f"Fetching default image from URL: {DEFAULT_IMAGE_URL}")
67
+ response = requests.get(DEFAULT_IMAGE_URL, timeout=15)
68
+ response.raise_for_status()
69
+ img = Image.open(io.BytesIO(response.content)).convert("RGB")
70
+ progress(0, desc="βœ… Using default background image.")
71
+ return resize_and_crop(img)
72
+ except Exception as e:
73
+ logger.warning(f"Could not fetch default image: {e}. Falling back to AI generation.")
74
+
75
+ # 3. AI-generated image (last resort)
76
+ try:
77
+ progress(0, desc=f"⏳ Generating new image with {T2I_MODEL}...")
78
+ logger.info(f"Generating a new image using model: {T2I_MODEL}")
79
+ client = InferenceClient()
80
+ image_bytes = client.text_to_image(T2I_PROMPT, model=T2I_MODEL)
81
+ img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
82
+ progress(0, desc="βœ… New image generated successfully.")
83
+ return resize_and_crop(img)
84
+ except Exception as e:
85
+ logger.error(f"Fatal: All image sources failed. Text-to-image failed with: {e}")
86
+ raise gr.Error(f"Failed to obtain a base image. AI generation error: {e}")
87
+
88
+ # --- Core Cryptography and Image Creation ---
89
 
90
  def generate_rsa_keys():
91
  private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
92
+ private_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()).decode('utf-8')
93
+ public_pem = private_key.public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode('utf-8')
 
 
 
 
 
 
 
94
  return private_pem, public_pem
95
 
96
+ def create_encrypted_image(
97
+ secret_data_str: str,
98
+ public_key_pem: str,
99
+ base_image: Image.Image,
100
+ show_key_overlay: bool
101
+ ) -> Image.Image:
102
  if not secret_data_str.strip():
103
  raise ValueError("Secret data cannot be empty.")
104
  if not public_key_pem.strip():
 
107
  data_dict = {}
108
  for line in secret_data_str.splitlines():
109
  line = line.strip()
110
+ if not line or line.startswith('#'): continue
 
111
  parts = line.split(':', 1) if ':' in line else line.split('=', 1)
112
  if len(parts) == 2:
113
  data_dict[parts[0].strip()] = parts[1].strip().strip("'\"")
 
121
  aes_key, nonce = os.urandom(32), os.urandom(12)
122
  ciphertext = AESGCM(aes_key).encrypt(nonce, json_bytes, None)
123
 
124
+ rsa_encrypted_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
 
 
 
125
 
126
  encrypted_payload = struct.pack('>I', len(rsa_encrypted_key)) + rsa_encrypted_key + nonce + ciphertext
127
 
128
+ # Use the prepared base_image
129
+ img = base_image.copy().convert("RGB")
130
+ width, height = img.size
131
+
132
+ # --- Add Stylish Overlays ---
133
+ draw = ImageDraw.Draw(img, "RGBA") # Use RGBA to draw transparent shapes
134
  try:
135
+ font_bold = ImageFont.truetype("DejaVuSans-Bold.ttf", 30)
136
+ font_regular = ImageFont.truetype("DejaVuSans.ttf", 15)
137
+ font_small = ImageFont.truetype("DejaVuSans.ttf", 12)
138
  except IOError:
139
+ font_bold = ImageFont.load_default(size=28)
140
+ font_regular = ImageFont.load_default(size=14)
141
+ font_small = ImageFont.load_default(size=11)
142
+
143
+ # Colors matching the Gradio theme (slate, sky)
144
+ overlay_color = (15, 23, 42, 190) # very dark slate, semi-transparent
145
+ title_color = (226, 232, 240) # light slate/sky
146
+ key_color = (148, 163, 184) # slate-400 for keys
147
+ value_color = (241, 245, 249) # slate-100 for values
148
+
149
+ # Main title overlay (top)
150
+ draw.rectangle([0, 20, width, 80], fill=overlay_color)
151
+ draw.text((width / 2, 50), "KeyLock Secure Data", fill=title_color, font=font_bold, anchor="ms")
152
+
153
+ # Key/Value data overlay (bottom-left)
154
+ if show_key_overlay:
155
+ box_padding = 15
156
+ line_spacing = 6
157
+ text_start_x = 35
158
+
159
+ # Calculate box height dynamically
160
+ lines = [f"{key}: {value}" for key, value in data_dict.items()]
161
+ line_heights = [draw.textbbox((0,0), line, font=font_regular)[3] for line in lines]
162
+ total_text_height = sum(line_heights) + (len(lines) - 1) * line_spacing
163
+ box_height = total_text_height + (box_padding * 2)
164
+ box_y0 = height - box_height - 20
165
+
166
+ draw.rectangle([20, box_y0, width - 20, height - 20], fill=overlay_color)
167
+
168
+ current_y = box_y0 + box_padding
169
+ for i, (key, value) in enumerate(data_dict.items()):
170
+ # Draw key and value separately for different colors
171
+ key_text = f"{key}:"
172
+ draw.text((text_start_x, current_y), key_text, fill=key_color, font=font_regular)
173
+ key_bbox = draw.textbbox((text_start_x, current_y), key_text, font=font_regular)
174
+ draw.text((key_bbox[2] + 8, current_y), str(value), fill=value_color, font=font_regular)
175
+ current_y += line_heights[i] + line_spacing
176
+
177
+ # --- Steganography: Embed encrypted data into pixels ---
178
  pixel_data = np.array(img.convert("RGB")).ravel()
179
  binary_payload = ''.join(format(b, '08b') for b in struct.pack('>I', len(encrypted_payload)) + encrypted_payload)
180
 
181
  if len(binary_payload) > pixel_data.size:
182
+ raise ValueError(f"Data is too large for the image. Max size: {pixel_data.size // 8} bytes. Your data: ~{len(binary_payload) // 8} bytes.")
183
 
184
  pixel_data[:len(binary_payload)] = (pixel_data[:len(binary_payload)] & 0xFE) | np.array(list(binary_payload), dtype=np.uint8)
185
+ stego_pixels = pixel_data.reshape((height, width, 3))
186
 
187
  return Image.fromarray(stego_pixels, 'RGB')
188
 
189
+ # --- Gradio Wrapper Functions ---
190
+
191
  def get_server_list():
192
  status = "Fetching server list from remote config..."
193
  yield gr.Dropdown(choices=[], value=None, label="⏳ Fetching..."), status, []
 
195
  response = requests.get(CREATOR_ENDPOINTS_JSON_URL, timeout=10)
196
  response.raise_for_status()
197
  all_entries = response.json()
198
+ valid_endpoints = [e for e in all_entries if isinstance(e, dict) and "name" in e and "public_key" in e and ("api_endpoint" in e or "link" in e)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  if not valid_endpoints:
200
+ raise ValueError("No valid server configurations found.")
 
201
  endpoint_names = [e['name'] for e in valid_endpoints]
202
  status = f"βœ… Success! Found {len(endpoint_names)} valid servers."
203
  yield gr.Dropdown(choices=endpoint_names, value=endpoint_names[0] if endpoint_names else None, label="Target Server"), status, valid_endpoints
204
  except Exception as e:
205
+ status = f"❌ Error fetching configuration: {e}"
206
  logger.error(status)
207
  yield gr.Dropdown(choices=[], value=None, label="Error fetching servers"), status, []
208
 
209
+ def create_keylock_wrapper(service_name: str, secret_data: str, available_endpoints: list, uploaded_image: Image.Image | None, show_keys: bool, progress=gr.Progress(track_tqdm=True)):
210
  if not service_name:
211
  raise gr.Error("Please select a target server.")
212
  public_key = next((e['public_key'] for e in available_endpoints if e['name'] == service_name), None)
213
  if not public_key:
214
  raise gr.Error(f"Could not find public key for '{service_name}'. Please refresh the server list.")
215
  try:
216
+ # Step 1: Prepare the base image with status updates via the progress object
217
+ base_image = prepare_base_image(uploaded_image, progress)
218
+ progress(0.5, desc="Encrypting and embedding data...")
219
+
220
+ # Step 2: Create the final encrypted image with overlays
221
+ created_image = create_encrypted_image(secret_data, public_key, base_image, show_keys)
222
+
223
  return created_image, f"βœ… Success! Image created for '{service_name}'."
224
  except Exception as e:
225
+ logger.error(f"Error creating image: {e}", exc_info=True)
226
  return None, f"❌ Error: {e}"
227
 
228
  def send_keylock_wrapper(service_name: str, image: Image.Image, available_endpoints: list):
229
+ if not service_name: raise gr.Error("Please select a target server.")
230
+ if image is None: raise gr.Error("Please create or upload an image to send.")
 
 
 
231
  endpoint_details = next((e for e in available_endpoints if e['name'] == service_name), None)
232
+ if not endpoint_details or not endpoint_details.get('link'): raise gr.Error(f"Config Error for '{service_name}'.")
 
233
 
234
+ server_url = endpoint_details['link']
 
 
 
235
  status = f"Connecting to remote server: {server_url}"
236
  yield None, status
237
 
 
241
  b64_string = base64.b64encode(buffer.getvalue()).decode("utf-8")
242
 
243
  client = Client(server_url)
244
+ result = client.predict(image_base64_string=b64_string, api_name="/keylock-auth-decoder")
245
+ decrypted_data = json.loads(result) if isinstance(result, str) else result
 
 
 
 
 
 
 
 
 
246
  yield decrypted_data, "βœ… Success! Data decrypted by remote server."
247
 
248
  except Exception as e:
249
+ logger.error(f"Error calling server with gradio_client: {e}", exc_info=True)
250
  yield None, f"❌ Error calling server API: {e}"
251
 
252
  def refresh_and_update_all():
253
+ for dropdown_update, status_update, state_update in get_server_list(): pass
 
254
  return dropdown_update, dropdown_update, status_update, state_update
255
 
256
+ # --- Gradio UI ---
257
+
258
  theme = gr.themes.Base(
259
  primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky, neutral_hue=gr.themes.colors.slate,
260
  font=(gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"),
 
273
  with gr.Tabs() as tabs:
274
  with gr.TabItem("β‘  Create KeyLock", id=0):
275
  gr.Markdown("## Step 1: Create an Encrypted Authentication Image (Local)")
276
+ gr.Markdown(f"Encrypt your data into a PNG. You can upload your own background image, or we'll provide a stylish default or generate one for you. The encryption process happens entirely in your browser.")
 
277
  with gr.Row(variant="panel"):
278
  with gr.Column(scale=2):
279
  with gr.Row():
280
  creator_service_dropdown = gr.Dropdown(label="Target Server", interactive=True, info="Select the API server to encrypt data for.")
281
  refresh_button = gr.Button("πŸ”„", scale=0, size="sm")
282
+
283
+ creator_secret_input = gr.Textbox(lines=5, label="Secret Data to Encrypt", placeholder="API_KEY: sk-123...\nUSER: demo-user")
284
+
285
+ gr.Markdown("### Image Options")
286
+ creator_base_image_input = gr.Image(label="Optional Base Image (600x600 recommended)", type="pil", sources=["upload"], show_download_button=False)
287
+ creator_show_keys_checkbox = gr.Checkbox(label="Show key/value data on final image", value=True)
288
+
289
+ creator_button = gr.Button("✨ Create Auth Image", variant="primary", scale=2)
290
+
291
+ with gr.Column(scale=3):
292
+ creator_status = gr.Textbox(label="Status", interactive=False, lines=1)
293
+ creator_image_output = gr.Image(label="Generated Encrypted Image", type="pil", show_download_button=True, format="png", height=600)
294
 
295
  with gr.TabItem("β‘‘ Send KeyLock", id=1):
296
  gr.Markdown("## Step 2: Decrypt via Live API Call")
297
+ gr.Markdown("This tool acts as the **Client**. It sends the encrypted image you created in Step 1 to the live, remote **Decoder Server** you select. The server uses its securely stored private key to decrypt the data and sends the result back.")
298
  with gr.Row(variant="panel"):
299
  with gr.Column(scale=1):
300
  gr.Markdown("### Configuration")
 
325
  output_private_key = gr.Textbox(lines=10, label="Generated Private Key", interactive=False, show_copy_button=True)
326
  gen_keys_button = gr.Button("βš™οΈ Generate New 2048-bit Key Pair", variant="secondary")
327
 
328
+ # --- Event Handlers ---
329
  gen_keys_button.click(fn=generate_rsa_keys, inputs=None, outputs=[output_private_key, output_public_key])
330
  refresh_button.click(fn=refresh_and_update_all, outputs=[creator_service_dropdown, send_service_dropdown, creator_status, endpoints_state])
331
  demo.load(fn=refresh_and_update_all, outputs=[creator_service_dropdown, send_service_dropdown, creator_status, endpoints_state])
332
+
333
+ creator_button.click(
334
+ fn=create_keylock_wrapper,
335
+ inputs=[creator_service_dropdown, creator_secret_input, endpoints_state, creator_base_image_input, creator_show_keys_checkbox],
336
+ outputs=[creator_image_output, creator_status]
337
+ )
338
+
339
  client_button.click(fn=send_keylock_wrapper, inputs=[send_service_dropdown, client_image_input, endpoints_state], outputs=[client_json_output, client_status])
340
 
341
  if __name__ == "__main__":