romanbredehoft-zama commited on
Commit
1ba3f22
β€’
1 Parent(s): c20d2a2

Add app with mockup display

Browse files
Files changed (5) hide show
  1. .gitignore +5 -0
  2. README.md +1 -1
  3. app.py +521 -0
  4. requirements.txt +2 -0
  5. settings.py +21 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Virtual Environment
2
+ .venv
3
+
4
+ # Python cache
5
+ __pycache__
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Credit Card Approval Detection
3
  emoji: πŸ‘
4
  colorFrom: pink
5
  colorTo: purple
 
1
  ---
2
+ title: Credit Card Approval Prediction
3
  emoji: πŸ‘
4
  colorFrom: pink
5
  colorTo: purple
app.py ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A local gradio app that filters images using FHE."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import time
7
+ import gradio as gr
8
+ import numpy
9
+ import requests
10
+ from itertools import chain
11
+
12
+ from settings import (
13
+ REPO_DIR,
14
+ SERVER_URL,
15
+ FHE_KEYS,
16
+ CLIENT_FILES,
17
+ SERVER_FILES,
18
+ )
19
+
20
+ from concrete.ml.deployment.fhe_client_server import FHEModelClient
21
+
22
+
23
+ subprocess.Popen(["uvicorn", "server:app"], cwd=REPO_DIR)
24
+ time.sleep(3)
25
+
26
+
27
+ def shorten_bytes_object(bytes_object, limit=500):
28
+ """Shorten the input bytes object to a given length.
29
+
30
+ Encrypted data is too large for displaying it in the browser using Gradio. This function
31
+ provides a shorten representation of it.
32
+
33
+ Args:
34
+ bytes_object (bytes): The input to shorten
35
+ limit (int): The length to consider. Default to 500.
36
+
37
+ Returns:
38
+ str: Hexadecimal string shorten representation of the input byte object.
39
+
40
+ """
41
+ # Define a shift for better display
42
+ shift = 100
43
+ return bytes_object[shift : limit + shift].hex()
44
+
45
+
46
+ def get_client(user_id, filter_name):
47
+ """Get the client API.
48
+
49
+ Args:
50
+ user_id (int): The current user's ID.
51
+ filter_name (str): The filter chosen by the user
52
+
53
+ Returns:
54
+ FHEModelClient: The client API.
55
+ """
56
+ # TODO
57
+ # return FHEModelClient(
58
+ # FILTERS_PATH / f"{filter_name}/deployment",
59
+ # filter_name,
60
+ # key_dir=FHE_KEYS / f"{filter_name}_{user_id}",
61
+ # )
62
+
63
+ return None
64
+
65
+
66
+ def get_client_file_path(name, user_id, filter_name):
67
+ """Get the correct temporary file path for the client.
68
+
69
+ Args:
70
+ name (str): The desired file name.
71
+ user_id (int): The current user's ID.
72
+ filter_name (str): The filter chosen by the user
73
+
74
+ Returns:
75
+ pathlib.Path: The file path.
76
+ """
77
+ return CLIENT_FILES / f"{name}_{filter_name}_{user_id}"
78
+
79
+
80
+ def clean_temporary_files(n_keys=20):
81
+ """Clean keys and encrypted images.
82
+
83
+ A maximum of n_keys keys and associated temporary files are allowed to be stored. Once this
84
+ limit is reached, the oldest files are deleted.
85
+
86
+ Args:
87
+ n_keys (int): The maximum number of keys and associated files to be stored. Default to 20.
88
+
89
+ """
90
+ # Get the oldest key files in the key directory
91
+ key_dirs = sorted(FHE_KEYS.iterdir(), key=os.path.getmtime)
92
+
93
+ # If more than n_keys keys are found, remove the oldest
94
+ user_ids = []
95
+ if len(key_dirs) > n_keys:
96
+ n_keys_to_delete = len(key_dirs) - n_keys
97
+ for key_dir in key_dirs[:n_keys_to_delete]:
98
+ user_ids.append(key_dir.name)
99
+ shutil.rmtree(key_dir)
100
+
101
+ # Get all the encrypted objects in the temporary folder
102
+ client_files = CLIENT_FILES.iterdir()
103
+ server_files = SERVER_FILES.iterdir()
104
+
105
+ # Delete all files related to the ids whose keys were deleted
106
+ for file in chain(client_files, server_files):
107
+ for user_id in user_ids:
108
+ if user_id in file.name:
109
+ file.unlink()
110
+
111
+
112
+ def keygen(filter_name):
113
+ """Generate the private key associated to a filter.
114
+
115
+ Args:
116
+ filter_name (str): The current filter to consider.
117
+
118
+ Returns:
119
+ (user_id, True) (Tuple[int, bool]): The current user's ID and a boolean used for visual display.
120
+
121
+ """
122
+ # Clean temporary files
123
+ clean_temporary_files()
124
+
125
+ # Create an ID for the current user
126
+ user_id = numpy.random.randint(0, 2**32)
127
+
128
+ # Retrieve the client API
129
+ client = get_client(user_id, filter_name)
130
+
131
+ # Generate a private key
132
+ client.generate_private_and_evaluation_keys(force=True)
133
+
134
+ # Retrieve the serialized evaluation key. In this case, as circuits are fully leveled, this
135
+ # evaluation key is empty. However, for software reasons, it is still needed for proper FHE
136
+ # execution
137
+ evaluation_key = client.get_serialized_evaluation_keys()
138
+
139
+ # Save evaluation_key as bytes in a file as it is too large to pass through regular Gradio
140
+ # buttons (see https://github.com/gradio-app/gradio/issues/1877)
141
+ evaluation_key_path = get_client_file_path("evaluation_key", user_id, filter_name)
142
+
143
+ with evaluation_key_path.open("wb") as evaluation_key_file:
144
+ evaluation_key_file.write(evaluation_key)
145
+
146
+ return (user_id, True)
147
+
148
+
149
+ def encrypt(user_id, input_image, filter_name):
150
+ """Encrypt the given image for a specific user and filter.
151
+
152
+ Args:
153
+ user_id (int): The current user's ID.
154
+ input_image (numpy.ndarray): The image to encrypt.
155
+ filter_name (str): The current filter to consider.
156
+
157
+ Returns:
158
+ (input_image, encrypted_image_short) (Tuple[bytes]): The encrypted image and one of its
159
+ representation.
160
+
161
+ """
162
+ if user_id == "":
163
+ raise gr.Error("Please generate the private key first.")
164
+
165
+ if input_image is None:
166
+ raise gr.Error("Please choose an image first.")
167
+
168
+ # Retrieve the client API
169
+ client = get_client(user_id, filter_name)
170
+
171
+ # Pre-process, encrypt and serialize the image
172
+ encrypted_image = client.encrypt_serialize(input_image)
173
+
174
+ # Save encrypted_image to bytes in a file, since too large to pass through regular Gradio
175
+ # buttons, https://github.com/gradio-app/gradio/issues/1877
176
+ encrypted_image_path = get_client_file_path("encrypted_image", user_id, filter_name)
177
+
178
+ with encrypted_image_path.open("wb") as encrypted_image_file:
179
+ encrypted_image_file.write(encrypted_image)
180
+
181
+ # Create a truncated version of the encrypted image for display
182
+ encrypted_image_short = shorten_bytes_object(encrypted_image)
183
+
184
+ return (input_image, encrypted_image_short)
185
+
186
+
187
+ def send_input(user_id, filter_name):
188
+ """Send the encrypted input image as well as the evaluation key to the server.
189
+
190
+ Args:
191
+ user_id (int): The current user's ID.
192
+ filter_name (str): The current filter to consider.
193
+ """
194
+ # Get the evaluation key path
195
+ evaluation_key_path = get_client_file_path("evaluation_key", user_id, filter_name)
196
+
197
+ if user_id == "" or not evaluation_key_path.is_file():
198
+ raise gr.Error("Please generate the private key first.")
199
+
200
+ encrypted_input_path = get_client_file_path("encrypted_image", user_id, filter_name)
201
+
202
+ if not encrypted_input_path.is_file():
203
+ raise gr.Error("Please generate the private key and then encrypt an image first.")
204
+
205
+ # Define the data and files to post
206
+ data = {
207
+ "user_id": user_id,
208
+ "filter": filter_name,
209
+ }
210
+
211
+ files = [
212
+ ("files", open(encrypted_input_path, "rb")),
213
+ ("files", open(evaluation_key_path, "rb")),
214
+ ]
215
+
216
+ # Send the encrypted input image and evaluation key to the server
217
+ url = SERVER_URL + "send_input"
218
+ with requests.post(
219
+ url=url,
220
+ data=data,
221
+ files=files,
222
+ ) as response:
223
+ return response.ok
224
+
225
+
226
+ def run_fhe(user_id, filter_name):
227
+ """Apply the filter on the encrypted image previously sent using FHE.
228
+
229
+ Args:
230
+ user_id (int): The current user's ID.
231
+ filter_name (str): The current filter to consider.
232
+ """
233
+ data = {
234
+ "user_id": user_id,
235
+ "filter": filter_name,
236
+ }
237
+
238
+ # Trigger the FHE execution on the encrypted image previously sent
239
+ url = SERVER_URL + "run_fhe"
240
+ with requests.post(
241
+ url=url,
242
+ data=data,
243
+ ) as response:
244
+ if response.ok:
245
+ return response.json()
246
+ else:
247
+ raise gr.Error("Please wait for the input image to be sent to the server.")
248
+
249
+
250
+ def get_output(user_id, filter_name):
251
+ """Retrieve the encrypted output image.
252
+
253
+ Args:
254
+ user_id (int): The current user's ID.
255
+ filter_name (str): The current filter to consider.
256
+
257
+ Returns:
258
+ encrypted_output_image_short (bytes): A representation of the encrypted result.
259
+
260
+ """
261
+ data = {
262
+ "user_id": user_id,
263
+ "filter": filter_name,
264
+ }
265
+
266
+ # Retrieve the encrypted output image
267
+ url = SERVER_URL + "get_output"
268
+ with requests.post(
269
+ url=url,
270
+ data=data,
271
+ ) as response:
272
+ if response.ok:
273
+ encrypted_output = response.content
274
+
275
+ # Save the encrypted output to bytes in a file as it is too large to pass through regular
276
+ # Gradio buttons (see https://github.com/gradio-app/gradio/issues/1877)
277
+ encrypted_output_path = get_client_file_path("encrypted_output", user_id, filter_name)
278
+
279
+ with encrypted_output_path.open("wb") as encrypted_output_file:
280
+ encrypted_output_file.write(encrypted_output)
281
+
282
+ # TODO
283
+ # Decrypt the image using a different (wrong) key for display
284
+ # output_image_representation = decrypt_output_with_wrong_key(encrypted_output, filter_name)
285
+
286
+ # return output_image_representation
287
+
288
+ return None
289
+ else:
290
+ raise gr.Error("Please wait for the FHE execution to be completed.")
291
+
292
+
293
+ def decrypt_output(user_id, filter_name):
294
+ """Decrypt the result.
295
+
296
+ Args:
297
+ user_id (int): The current user's ID.
298
+ filter_name (str): The current filter to consider.
299
+
300
+ Returns:
301
+ (output_image, False, False) ((Tuple[numpy.ndarray, bool, bool]): The decrypted output, as
302
+ well as two booleans used for resetting Gradio checkboxes
303
+
304
+ """
305
+ if user_id == "":
306
+ raise gr.Error("Please generate the private key first.")
307
+
308
+ # Get the encrypted output path
309
+ encrypted_output_path = get_client_file_path("encrypted_output", user_id, filter_name)
310
+
311
+ if not encrypted_output_path.is_file():
312
+ raise gr.Error("Please run the FHE execution first.")
313
+
314
+ # Load the encrypted output as bytes
315
+ with encrypted_output_path.open("rb") as encrypted_output_file:
316
+ encrypted_output_image = encrypted_output_file.read()
317
+
318
+ # Retrieve the client API
319
+ client = get_client(user_id, filter_name)
320
+
321
+ # Deserialize, decrypt and post-process the encrypted output
322
+ output_image = client.deserialize_decrypt_post_process(encrypted_output_image)
323
+
324
+ return output_image, False, False
325
+
326
+
327
+ demo = gr.Blocks()
328
+
329
+
330
+ print("Starting the demo...")
331
+ with demo:
332
+ gr.Markdown(
333
+ """
334
+ <h1 align="center">Credit Card Approval Prediction Using Fully Homomorphic Encryption</h1>
335
+ """
336
+ )
337
+
338
+ gr.Markdown("## Client side")
339
+ gr.Markdown("### Step 1: Keygen. ")
340
+ gr.Markdown("#### Notes")
341
+ gr.Markdown(
342
+ """
343
+ - The private key is used to encrypt and decrypt the data and will never be shared.
344
+ - No public key is required for these filter operators.
345
+ """
346
+ )
347
+
348
+ with gr.Row():
349
+ with gr.Column():
350
+ gr.Markdown("### Client ")
351
+ keygen_button_1 = gr.Button("Generate the private key.")
352
+
353
+ # TODO : change the button name (add check emoji) instead maybe
354
+ with gr.Row():
355
+ keygen_checkbox_1 = gr.Checkbox(label="Private key generated:", interactive=False)
356
+
357
+
358
+ client_id = gr.Textbox(label="", max_lines=2, interactive=False, visible=False)
359
+
360
+ with gr.Column():
361
+ gr.Markdown("### Bank ")
362
+ keygen_button_2 = gr.Button("Generate the private key.")
363
+
364
+ # TODO : change the button name (add check emoji) instead maybe
365
+ with gr.Row():
366
+ keygen_checkbox_2 = gr.Checkbox(label="Private key generated:", interactive=False)
367
+
368
+ bank_id = gr.Textbox(label="", max_lines=2, interactive=False, visible=False)
369
+
370
+ with gr.Column():
371
+ gr.Markdown("### Third Party ")
372
+ keygen_button_3 = gr.Button("Generate the private key.")
373
+
374
+ # TODO : change the button name (add check emoji) instead maybe
375
+ with gr.Row():
376
+ keygen_checkbox_3 = gr.Checkbox(label="Private key generated:", interactive=False)
377
+
378
+ party_id = gr.Textbox(label="", max_lines=2, interactive=False, visible=False)
379
+
380
+
381
+ gr.Markdown("### Step 2: Infos. ")
382
+
383
+ with gr.Row():
384
+ with gr.Column():
385
+ gr.Markdown("### Client ")
386
+ # TODO : change infos
387
+ choice_1 = gr.Dropdown(choices=["Yes, No"], label="Choose", interactive=True)
388
+ slide_1 = gr.Slider(2, 20, value=4, label="Count", info="Choose between 2 and 20")
389
+
390
+ with gr.Column():
391
+ gr.Markdown("### Bank ")
392
+ # TODO : change infos
393
+ checkbox_1 = gr.CheckboxGroup(["USA", "Japan", "Pakistan"], label="Countries", info="Where are they from?")
394
+
395
+ with gr.Column():
396
+ gr.Markdown("### Third Party ")
397
+ # TODO : change infos
398
+ radio_1 = gr.Radio(["park", "zoo", "road"], label="Location", info="Where did they go?")
399
+
400
+ gr.Markdown("### Step 4: Encrypt the inputs using FHE.")
401
+ with gr.Row():
402
+ with gr.Column():
403
+ gr.Markdown("### Client ")
404
+ encrypt_button_1 = gr.Button("Encrypt the inputs using FHE.")
405
+ encrypted_input_1 = gr.Textbox(
406
+ label="Encrypted input representation:", max_lines=2, interactive=False
407
+ )
408
+
409
+
410
+ with gr.Column():
411
+ gr.Markdown("### Bank ")
412
+ encrypt_button_2 = gr.Button("Encrypt the inputs using FHE.")
413
+ encrypted_input_2 = gr.Textbox(
414
+ label="Encrypted input representation:", max_lines=2, interactive=False
415
+ )
416
+
417
+ with gr.Column():
418
+ gr.Markdown("### Third Party ")
419
+ encrypt_button_3 = gr.Button("Encrypt the inputs using FHE.")
420
+ encrypted_input_3 = gr.Textbox(
421
+ label="Encrypted input representation:", max_lines=2, interactive=False
422
+ )
423
+
424
+ gr.Markdown("### Step 5: Send the encrypted inputs to the server.")
425
+ with gr.Row():
426
+ with gr.Column():
427
+ gr.Markdown("### Client ")
428
+ send_input_button_1 = gr.Button("Send the encrypted inputs to the server.")
429
+ # TODO : change the button name (add check emoji) instead maybe
430
+ send_input_checkbox_1 = gr.Checkbox(label="Encrypted inputs sent.", interactive=False)
431
+
432
+
433
+ with gr.Column():
434
+ gr.Markdown("### Bank ")
435
+ send_input_button_2 = gr.Button("Send the encrypted inputs to the server.")
436
+ # TODO : change the button name (add check emoji) instead maybe
437
+ send_input_checkbox_2 = gr.Checkbox(label="Encrypted inputs sent.", interactive=False)
438
+
439
+ with gr.Column():
440
+ gr.Markdown("### Third Party ")
441
+ send_input_button_3 = gr.Button("Send the encrypted inputs to the server.")
442
+ # TODO : change the button name (add check emoji) instead maybe
443
+ send_input_checkbox_3 = gr.Checkbox(label="Encrypted inputs sent.", interactive=False)
444
+
445
+ gr.Markdown("## Server side")
446
+ gr.Markdown(
447
+ "The encrypted values are received by the server. The server can then compute the prediction "
448
+ "directly over them. Once the computation is finished, the server returns "
449
+ "the encrypted result to the client."
450
+ )
451
+
452
+ gr.Markdown("### Step 6: Run FHE execution.")
453
+ execute_fhe_button = gr.Button("Run FHE execution.")
454
+ fhe_execution_time = gr.Textbox(
455
+ label="Total FHE execution time (in seconds):", max_lines=1, interactive=False
456
+ )
457
+
458
+ gr.Markdown("## Client side")
459
+ gr.Markdown(
460
+ "The encrypted output is sent back to the client, who can finally decrypt it with the "
461
+ "private key."
462
+ )
463
+
464
+ gr.Markdown("### Step 7: Receive the encrypted output from the server.")
465
+ gr.Markdown(
466
+ "The output displayed here is the encrypted result sent by the server, which has been "
467
+ "decrypted using a different private key. This is only used to visually represent an "
468
+ "encrypted output."
469
+ )
470
+ get_output_button = gr.Button("Receive the encrypted output from the server.")
471
+
472
+ gr.Markdown("### Step 8: Decrypt the output.")
473
+ decrypt_button = gr.Button("Decrypt the output")
474
+
475
+ prediction_output = gr.Textbox(
476
+ label="Credit card approval decision: ", max_lines=1, interactive=False
477
+ )
478
+
479
+ # # Button to generate the private key
480
+ # keygen_button.click(
481
+ # keygen,
482
+ # inputs=[filter_name],
483
+ # outputs=[user_id, keygen_checkbox],
484
+ # )
485
+
486
+ # # Button to encrypt inputs on the client side
487
+ # encrypt_button.click(
488
+ # encrypt,
489
+ # inputs=[user_id, input_image, filter_name],
490
+ # outputs=[original_image, encrypted_input],
491
+ # )
492
+
493
+ # # Button to send the encodings to the server using post method
494
+ # send_input_button.click(
495
+ # send_input, inputs=[user_id, filter_name], outputs=[send_input_checkbox]
496
+ # )
497
+
498
+ # # Button to send the encodings to the server using post method
499
+ # execute_fhe_button.click(run_fhe, inputs=[user_id, filter_name], outputs=[fhe_execution_time])
500
+
501
+ # # Button to send the encodings to the server using post method
502
+ # get_output_button.click(
503
+ # get_output,
504
+ # inputs=[user_id, filter_name],
505
+ # outputs=[encrypted_output_representation]
506
+ # )
507
+
508
+ # # Button to decrypt the output on the client side
509
+ # decrypt_button.click(
510
+ # decrypt_output,
511
+ # inputs=[user_id, filter_name],
512
+ # outputs=[output_image, keygen_checkbox, send_input_checkbox],
513
+ # )
514
+
515
+ gr.Markdown(
516
+ "The app was built with [Concrete-ML](https://github.com/zama-ai/concrete-ml), a "
517
+ "Privacy-Preserving Machine Learning (PPML) open-source set of tools by [Zama](https://zama.ai/). "
518
+ "Try it yourself and don't forget to star on Github &#11088;."
519
+ )
520
+
521
+ demo.launch(share=False)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ concrete-ml==1.3.0
2
+ gradio==3.40.1
settings.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "All constants used in the project."
2
+
3
+ from pathlib import Path
4
+
5
+ # The directory of this project
6
+ REPO_DIR = Path(__file__).parent
7
+
8
+ # This repository's main necessary directories
9
+ FILTERS_PATH = REPO_DIR / "filters"
10
+ FHE_KEYS = REPO_DIR / ".fhe_keys"
11
+ CLIENT_FILES = REPO_DIR / "client_files"
12
+ SERVER_FILES = REPO_DIR / "server_files"
13
+
14
+ # Create the necessary directories
15
+ FHE_KEYS.mkdir(exist_ok=True)
16
+ CLIENT_FILES.mkdir(exist_ok=True)
17
+ SERVER_FILES.mkdir(exist_ok=True)
18
+
19
+ # Store the server's URL
20
+ SERVER_URL = "http://localhost:8000/"
21
+