broadfield-dev commited on
Commit
5898cdb
Β·
verified Β·
1 Parent(s): 7b2cec6

Create client/app.py

Browse files
Files changed (1) hide show
  1. client/app.py +320 -0
client/app.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
10
+ import io
11
+ import json
12
+ import logging
13
+ import os
14
+ 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
21
+ from gradio_client import Client, handle_file
22
+ from huggingface_hub import InferenceClient
23
+
24
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
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:
40
+ return ImageOps.fit(img, (size, size), Image.Resampling.LANCZOS)
41
+ except Exception as e:
42
+ logger.error(f"Failed to resize and crop image: {e}")
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")
59
+ progress(0, desc="βœ… Using default background image.")
60
+ return resize_and_crop(img)
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(
84
+ secret_data_str: str,
85
+ public_key_pem: str,
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)
116
+ font_regular = ImageFont.truetype("DejaVuSans.ttf", 15)
117
+ except IOError:
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:
211
+ logger.error(f"Error creating image: {e}", exc_info=True)
212
+ return None, f"❌ Error: {e}", gr.Tabs()
213
+
214
+ def send_keylock_wrapper(service_name: str, image: Image.Image, available_endpoints: list):
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):
265
+ creator_status = gr.Textbox(label="Status", interactive=False, lines=1)
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()