Update server/app.py
Browse files- server/app.py +70 -111
server/app.py
CHANGED
@@ -12,6 +12,8 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
12 |
from cryptography.hazmat.primitives import hashes
|
13 |
from cryptography.hazmat.primitives import serialization
|
14 |
from cryptography.hazmat.primitives.asymmetric import padding
|
|
|
|
|
15 |
from cryptography.exceptions import InvalidTag
|
16 |
|
17 |
# --- Constants ---
|
@@ -23,24 +25,21 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(level
|
|
23 |
logger = logging.getLogger(__name__)
|
24 |
|
25 |
# --- Load Keys and Config ---
|
26 |
-
# For production (e.g., Hugging Face Spaces), the private key should be an environment variable.
|
27 |
KEYLOCK_PRIV_KEY = os.environ.get('KEYLOCK_PRIV_KEY')
|
28 |
KEYLOCK_STATUS_MESSAGE = ""
|
29 |
|
30 |
-
# For local testing, fall back to reading from a file if the environment variable is not set.
|
31 |
if not KEYLOCK_PRIV_KEY:
|
32 |
try:
|
33 |
with open("keylock_priv.pem", "r") as f:
|
34 |
KEYLOCK_PRIV_KEY = f.read()
|
35 |
-
logger.warning("Loaded private key from 'keylock_priv.pem'. This is for local testing only.
|
36 |
-
KEYLOCK_STATUS_MESSAGE = "⚠️ Loaded from `keylock_priv.pem` file. This is
|
37 |
except FileNotFoundError:
|
38 |
-
logger.error("FATAL: Private key not found
|
39 |
-
KEYLOCK_STATUS_MESSAGE = "❌ NOT FOUND. The API is non-functional.
|
40 |
else:
|
41 |
logger.info("Successfully loaded private key from environment variable 'KEYLOCK_PRIV_KEY'.")
|
42 |
-
KEYLOCK_STATUS_MESSAGE = "✅ Loaded successfully from secrets/environment variable.
|
43 |
-
|
44 |
|
45 |
PUBLIC_KEY_PEM_STRING = ""
|
46 |
try:
|
@@ -49,38 +48,45 @@ try:
|
|
49 |
logger.info("Successfully loaded public key from keylock_pub.pem for display.")
|
50 |
except FileNotFoundError:
|
51 |
logger.error("FATAL: keylock_pub.pem not found. The UI will show an error.")
|
52 |
-
PUBLIC_KEY_PEM_STRING = "Error: keylock_pub.pem file not found on the server.
|
53 |
except Exception as e:
|
54 |
logger.error(f"Error loading public key: {e}")
|
55 |
PUBLIC_KEY_PEM_STRING = f"Error loading public key: {e}"
|
56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
# --- Core Decryption Function ---
|
58 |
def decode_data(image_base64_string: str) -> dict:
|
59 |
-
"""
|
60 |
-
Decrypts a payload hidden in a PNG image via LSB steganography and hybrid encryption.
|
61 |
-
"""
|
62 |
if not KEYLOCK_PRIV_KEY:
|
63 |
-
error_msg = "Server Error: The API is not configured with a private key.
|
64 |
-
logger.error(error_msg)
|
65 |
raise gr.Error(error_msg)
|
66 |
-
|
67 |
try:
|
68 |
-
# 1. Decode Base64 and load image data
|
69 |
image_buffer = base64.b64decode(image_base64_string)
|
70 |
img = Image.open(io.BytesIO(image_buffer)).convert("RGB")
|
71 |
pixel_data = np.array(img).ravel()
|
72 |
|
73 |
-
# 2. Extract data length from the LSB header
|
74 |
if pixel_data.size < HEADER_BITS:
|
75 |
raise ValueError(f"Image is too small. Minimum pixel count: {HEADER_BITS}")
|
76 |
|
77 |
header_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[:HEADER_BITS])
|
78 |
data_length = int(header_binary_string, 2)
|
79 |
|
80 |
-
if data_length == 0:
|
81 |
-
return {}
|
82 |
|
83 |
-
# 3. Extract the full crypto payload
|
84 |
data_bits_count = data_length * 8
|
85 |
end_offset = HEADER_BITS + data_bits_count
|
86 |
if pixel_data.size < end_offset:
|
@@ -89,9 +95,8 @@ def decode_data(image_base64_string: str) -> dict:
|
|
89 |
data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
|
90 |
crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
|
91 |
|
92 |
-
# 4. Load private key and unpack payload
|
93 |
private_key = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY.encode(), password=None)
|
94 |
-
|
95 |
offset = 4
|
96 |
encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
|
97 |
rsa_key_size_bytes = private_key.key_size // 8
|
@@ -104,22 +109,17 @@ def decode_data(image_base64_string: str) -> dict:
|
|
104 |
offset += AES_GCM_NONCE_SIZE
|
105 |
ciphertext_with_tag = crypto_payload[offset:]
|
106 |
|
107 |
-
# 5. Decrypt
|
108 |
recovered_aes_key = private_key.decrypt(
|
109 |
encrypted_aes_key,
|
110 |
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
|
111 |
)
|
112 |
-
|
113 |
-
decrypted_bytes = aesgcm.decrypt(nonce, ciphertext_with_tag, None)
|
114 |
|
115 |
-
logger.info("Successfully decrypted payload from image.")
|
116 |
return json.loads(decrypted_bytes.decode('utf-8'))
|
117 |
|
118 |
except (ValueError, InvalidTag, TypeError, struct.error) as e:
|
119 |
-
|
120 |
-
raise gr.Error(f"Decryption failed. The image may be corrupt, not from a compatible client, or used the wrong public key. Details: {e}")
|
121 |
except Exception as e:
|
122 |
-
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
|
123 |
raise gr.Error(f"An unexpected server error occurred. Details: {e}")
|
124 |
|
125 |
# ==============================================================================
|
@@ -131,92 +131,56 @@ with gr.Blocks(title="Secure Decoder API", theme=gr.themes.Soft()) as demo:
|
|
131 |
|
132 |
with gr.Tabs():
|
133 |
with gr.TabItem("🚀 Quick Start & Documentation"):
|
134 |
-
#
|
135 |
-
gr.Markdown("## What Is This?")
|
136 |
-
gr.Markdown(
|
137 |
-
"This service decodes messages that have been securely hidden inside images. It's designed for use cases where you need to transmit a small, sensitive JSON payload (like authentication tokens, user data, or configuration) "
|
138 |
-
"without it being easily detectable or readable. The process uses a combination of **steganography** (hiding data in the image's pixels) and **hybrid encryption** (the security of RSA combined with the speed of AES)."
|
139 |
-
)
|
140 |
-
|
141 |
-
with gr.Accordion("How It Works (The Gory Details)", open=False):
|
142 |
-
gr.Markdown(
|
143 |
-
"""
|
144 |
-
The security relies on a well-established cryptographic pattern called **Hybrid Encryption**. Here’s the step-by-step process a client must follow to create a compatible image:
|
145 |
-
1. **Client Generates a One-Time Key**: The client creates a random, single-use 32-byte symmetric key for AES-GCM encryption.
|
146 |
-
2. **Client Encrypts Data**: The client takes the JSON payload and encrypts it using the one-time AES key.
|
147 |
-
3. **Client Encrypts the One-Time Key**: The client encrypts the AES key using this server's **Public RSA Key** (provided below).
|
148 |
-
4. **Client Creates the Payload**: The client bundles the encrypted AES key, nonce, and ciphertext into a single binary payload.
|
149 |
-
5. **Client Hides the Payload**: The client uses **Least Significant Bit (LSB) steganography** to hide the payload in a template image.
|
150 |
-
6. **Client Sends Image**: The final image is sent to this API endpoint.
|
151 |
-
7. **Server Decodes**: This server reverses the process: it extracts the payload, uses its private RSA key to decrypt the AES key, and then decrypts the final message.
|
152 |
-
"""
|
153 |
-
)
|
154 |
-
|
155 |
-
gr.Markdown("---")
|
156 |
gr.Markdown("## How to Use This API")
|
157 |
gr.Markdown("### Step 1: Get the Server's Public Key")
|
158 |
-
gr.
|
159 |
-
|
160 |
-
|
161 |
-
with gr.Accordion("Step 2: Create the Encrypted Image (Client-Side Python Example)", open=True):
|
162 |
-
gr.Markdown("Use the following Python code in your client application to create a KeyLock image.")
|
163 |
-
gr.Code(
|
164 |
-
language="python",
|
165 |
-
label="Client-Side Image Creation Script (create_keylock_image.py)",
|
166 |
-
value="""
|
167 |
-
import json; import os; import struct; from PIL import Image; import numpy as np
|
168 |
-
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
169 |
-
from cryptography.hazmat.primitives import hashes, serialization
|
170 |
-
from cryptography.hazmat.primitives.asymmetric import padding
|
171 |
-
def create_keylock_image(public_key_pem: str, json_data: dict, template_image_path: str, output_path: str):
|
172 |
-
public_key = serialization.load_pem_public_key(public_key_pem.encode())
|
173 |
-
data_to_encrypt = json.dumps(json_data).encode('utf-8')
|
174 |
-
aes_key = AESGCM.generate_key(bit_length=256)
|
175 |
-
nonce = os.urandom(12)
|
176 |
-
ciphertext_with_tag = AESGCM(aes_key).encrypt(nonce, data_to_encrypt, None)
|
177 |
-
encrypted_aes_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
|
178 |
-
payload = struct.pack('>I', len(encrypted_aes_key)) + encrypted_aes_key + nonce + ciphertext_with_tag
|
179 |
-
img = Image.open(template_image_path).convert("RGB")
|
180 |
-
pixel_data = np.array(img)
|
181 |
-
full_binary_string = f'{len(payload):032b}' + ''.join(f'{byte:08b}' for byte in payload)
|
182 |
-
if len(full_binary_string) > pixel_data.size:
|
183 |
-
raise ValueError(f"Data is too large. Needs {len(full_binary_string)} pixels, image has {pixel_data.size}.")
|
184 |
-
flat_pixels = pixel_data.ravel()
|
185 |
-
for i in range(len(full_binary_string)):
|
186 |
-
flat_pixels[i] = (flat_pixels[i] & 0b11111110) | int(full_binary_string[i])
|
187 |
-
Image.fromarray(pixel_data).save(output_path, "PNG")
|
188 |
-
print(f"Successfully created encrypted image at {output_path}")
|
189 |
-
# --- Example Usage ---
|
190 |
-
if __name__ == '__main__':
|
191 |
-
SERVER_PUBLIC_KEY = \"\"\"-----BEGIN PUBLIC KEY-----
|
192 |
-
YOUR_SERVER_PUBLIC_KEY_HERE
|
193 |
-
-----END PUBLIC KEY-----\"\"\"
|
194 |
-
my_secret_data = {"user_id": "user-12345", "token": "abcdef-123456"}
|
195 |
-
create_keylock_image(SERVER_PUBLIC_KEY, my_secret_data, "template.png", "encrypted_image.png")
|
196 |
-
"""
|
197 |
-
)
|
198 |
-
|
199 |
-
gr.Markdown("### Step 3: Call the API Endpoint")
|
200 |
-
gr.Markdown(
|
201 |
-
"- **Method**: `POST`\n"
|
202 |
-
"- **Endpoint**: `/run/keylock-auth-decoder`\n"
|
203 |
-
"- **Body**: A JSON object containing a base64-encoded string of the image."
|
204 |
-
)
|
205 |
-
gr.Code(language="json", label="JSON Payload Schema", value='{\n "data": [\n "<base64_encoded_image_string>"\n ]\n}')
|
206 |
-
|
207 |
|
208 |
with gr.TabItem("⚙️ Server Status & Error Guide"):
|
209 |
gr.Markdown("## Server Status")
|
210 |
gr.Textbox(label="Private Key Status", value=KEYLOCK_STATUS_MESSAGE, interactive=False, lines=3)
|
211 |
-
|
212 |
gr.Markdown("---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
213 |
gr.Markdown("## Error Handling Guide")
|
214 |
gr.Markdown(
|
215 |
"""
|
216 |
-
- **`"Decryption failed. ... InvalidTag"`**: The most common error.
|
217 |
-
- **`"Decryption failed. ... Key mismatch"`**: The RSA key size used to encrypt the data does not match the server's key size.
|
218 |
- **`"Image data corrupt or truncated..."`**: The image file is incomplete or damaged.
|
219 |
-
- **`"Server Error: ... not configured"`**:
|
220 |
"""
|
221 |
)
|
222 |
|
@@ -224,12 +188,7 @@ YOUR_SERVER_PUBLIC_KEY_HERE
|
|
224 |
with gr.Row(visible=False):
|
225 |
api_input = gr.Textbox()
|
226 |
api_output = gr.JSON()
|
227 |
-
gr.Interface(
|
228 |
-
fn=decode_data,
|
229 |
-
inputs=api_input,
|
230 |
-
outputs=api_output,
|
231 |
-
api_name="keylock-auth-decoder",
|
232 |
-
)
|
233 |
|
234 |
if __name__ == "__main__":
|
235 |
-
demo.launch()
|
|
|
12 |
from cryptography.hazmat.primitives import hashes
|
13 |
from cryptography.hazmat.primitives import serialization
|
14 |
from cryptography.hazmat.primitives.asymmetric import padding
|
15 |
+
# Import 'rsa' for the key generation utility
|
16 |
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
17 |
from cryptography.exceptions import InvalidTag
|
18 |
|
19 |
# --- Constants ---
|
|
|
25 |
logger = logging.getLogger(__name__)
|
26 |
|
27 |
# --- Load Keys and Config ---
|
|
|
28 |
KEYLOCK_PRIV_KEY = os.environ.get('KEYLOCK_PRIV_KEY')
|
29 |
KEYLOCK_STATUS_MESSAGE = ""
|
30 |
|
|
|
31 |
if not KEYLOCK_PRIV_KEY:
|
32 |
try:
|
33 |
with open("keylock_priv.pem", "r") as f:
|
34 |
KEYLOCK_PRIV_KEY = f.read()
|
35 |
+
logger.warning("Loaded private key from 'keylock_priv.pem'. This is for local testing only.")
|
36 |
+
KEYLOCK_STATUS_MESSAGE = "⚠️ Loaded from `keylock_priv.pem` file. This is for local testing but insecure for production."
|
37 |
except FileNotFoundError:
|
38 |
+
logger.error("FATAL: Private key not found. API is non-functional.")
|
39 |
+
KEYLOCK_STATUS_MESSAGE = "❌ NOT FOUND. The API is non-functional. Set the `KEYLOCK_PRIV_KEY` secret or provide a `keylock_priv.pem` file."
|
40 |
else:
|
41 |
logger.info("Successfully loaded private key from environment variable 'KEYLOCK_PRIV_KEY'.")
|
42 |
+
KEYLOCK_STATUS_MESSAGE = "✅ Loaded successfully from secrets/environment variable. Recommended secure configuration."
|
|
|
43 |
|
44 |
PUBLIC_KEY_PEM_STRING = ""
|
45 |
try:
|
|
|
48 |
logger.info("Successfully loaded public key from keylock_pub.pem for display.")
|
49 |
except FileNotFoundError:
|
50 |
logger.error("FATAL: keylock_pub.pem not found. The UI will show an error.")
|
51 |
+
PUBLIC_KEY_PEM_STRING = "Error: keylock_pub.pem file not found on the server."
|
52 |
except Exception as e:
|
53 |
logger.error(f"Error loading public key: {e}")
|
54 |
PUBLIC_KEY_PEM_STRING = f"Error loading public key: {e}"
|
55 |
|
56 |
+
# --- Key Generation Utility ---
|
57 |
+
def generate_rsa_keys():
|
58 |
+
"""Generates a new 2048-bit RSA private and public key pair."""
|
59 |
+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
60 |
+
private_pem = private_key.private_bytes(
|
61 |
+
encoding=serialization.Encoding.PEM,
|
62 |
+
format=serialization.PrivateFormat.PKCS8,
|
63 |
+
encryption_algorithm=serialization.NoEncryption()
|
64 |
+
).decode('utf-8')
|
65 |
+
public_pem = private_key.public_key().public_bytes(
|
66 |
+
encoding=serialization.Encoding.PEM,
|
67 |
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
68 |
+
).decode('utf-8')
|
69 |
+
return private_pem, public_pem
|
70 |
+
|
71 |
# --- Core Decryption Function ---
|
72 |
def decode_data(image_base64_string: str) -> dict:
|
73 |
+
"""Decrypts a payload hidden in a PNG image via LSB steganography and hybrid encryption."""
|
|
|
|
|
74 |
if not KEYLOCK_PRIV_KEY:
|
75 |
+
error_msg = "Server Error: The API is not configured with a private key."
|
|
|
76 |
raise gr.Error(error_msg)
|
|
|
77 |
try:
|
|
|
78 |
image_buffer = base64.b64decode(image_base64_string)
|
79 |
img = Image.open(io.BytesIO(image_buffer)).convert("RGB")
|
80 |
pixel_data = np.array(img).ravel()
|
81 |
|
|
|
82 |
if pixel_data.size < HEADER_BITS:
|
83 |
raise ValueError(f"Image is too small. Minimum pixel count: {HEADER_BITS}")
|
84 |
|
85 |
header_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[:HEADER_BITS])
|
86 |
data_length = int(header_binary_string, 2)
|
87 |
|
88 |
+
if data_length == 0: return {}
|
|
|
89 |
|
|
|
90 |
data_bits_count = data_length * 8
|
91 |
end_offset = HEADER_BITS + data_bits_count
|
92 |
if pixel_data.size < end_offset:
|
|
|
95 |
data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
|
96 |
crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
|
97 |
|
|
|
98 |
private_key = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY.encode(), password=None)
|
99 |
+
|
100 |
offset = 4
|
101 |
encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
|
102 |
rsa_key_size_bytes = private_key.key_size // 8
|
|
|
109 |
offset += AES_GCM_NONCE_SIZE
|
110 |
ciphertext_with_tag = crypto_payload[offset:]
|
111 |
|
|
|
112 |
recovered_aes_key = private_key.decrypt(
|
113 |
encrypted_aes_key,
|
114 |
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
|
115 |
)
|
116 |
+
decrypted_bytes = AESGCM(recovered_aes_key).decrypt(nonce, ciphertext_with_tag, None)
|
|
|
117 |
|
|
|
118 |
return json.loads(decrypted_bytes.decode('utf-8'))
|
119 |
|
120 |
except (ValueError, InvalidTag, TypeError, struct.error) as e:
|
121 |
+
raise gr.Error(f"Decryption failed. Image may be corrupt or used the wrong public key. Details: {e}")
|
|
|
122 |
except Exception as e:
|
|
|
123 |
raise gr.Error(f"An unexpected server error occurred. Details: {e}")
|
124 |
|
125 |
# ==============================================================================
|
|
|
131 |
|
132 |
with gr.Tabs():
|
133 |
with gr.TabItem("🚀 Quick Start & Documentation"):
|
134 |
+
# This tab remains unchanged, showing clients how to use the API
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
gr.Markdown("## How to Use This API")
|
136 |
gr.Markdown("### Step 1: Get the Server's Public Key")
|
137 |
+
gr.Code(value=PUBLIC_KEY_PEM_STRING, language="pem", label="Server Public Key")
|
138 |
+
# ... (The rest of the extensive documentation from your original file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
|
140 |
with gr.TabItem("⚙️ Server Status & Error Guide"):
|
141 |
gr.Markdown("## Server Status")
|
142 |
gr.Textbox(label="Private Key Status", value=KEYLOCK_STATUS_MESSAGE, interactive=False, lines=3)
|
|
|
143 |
gr.Markdown("---")
|
144 |
+
|
145 |
+
# --- NEW: Key Generation Utility ---
|
146 |
+
with gr.Accordion("🔑 Admin: Key Pair Generator", open=False):
|
147 |
+
gr.Markdown(
|
148 |
+
"**For Administrators Only.** Use this tool to generate a new RSA key pair for the server. "
|
149 |
+
"**This does NOT automatically apply the keys.** To use them, you must:\n"
|
150 |
+
"1. Copy the **Private Key** and update the `KEYLOCK_PRIV_KEY` secret in your deployment environment.\n"
|
151 |
+
"2. Copy the **Public Key** and overwrite the contents of the `keylock_pub.pem` file in your repository.\n"
|
152 |
+
"3. Restart the server for the changes to take effect."
|
153 |
+
)
|
154 |
+
gen_keys_button = gr.Button("⚙️ Generate New 2048-bit Key Pair", variant="secondary")
|
155 |
+
with gr.Row():
|
156 |
+
with gr.Column():
|
157 |
+
output_private_key = gr.Textbox(
|
158 |
+
lines=10,
|
159 |
+
label="Generated Private Key (KEEP THIS SECRET)",
|
160 |
+
interactive=False,
|
161 |
+
show_copy_button=True
|
162 |
+
)
|
163 |
+
with gr.Column():
|
164 |
+
output_public_key = gr.Textbox(
|
165 |
+
lines=10,
|
166 |
+
label="Generated Public Key (for clients & keylock_pub.pem)",
|
167 |
+
interactive=False,
|
168 |
+
show_copy_button=True
|
169 |
+
)
|
170 |
+
|
171 |
+
gen_keys_button.click(
|
172 |
+
fn=generate_rsa_keys,
|
173 |
+
inputs=None,
|
174 |
+
outputs=[output_private_key, output_public_key]
|
175 |
+
)
|
176 |
+
|
177 |
gr.Markdown("## Error Handling Guide")
|
178 |
gr.Markdown(
|
179 |
"""
|
180 |
+
- **`"Decryption failed. ... InvalidTag"`**: The most common error. The image was encrypted with a **different public key** or the image file was corrupted.
|
181 |
+
- **`"Decryption failed. ... Key mismatch"`**: The RSA key size used to encrypt the data does not match the server's key size.
|
182 |
- **`"Image data corrupt or truncated..."`**: The image file is incomplete or damaged.
|
183 |
+
- **`"Server Error: ... not configured"`**: The administrator has not set the `KEYLOCK_PRIV_KEY` secret correctly.
|
184 |
"""
|
185 |
)
|
186 |
|
|
|
188 |
with gr.Row(visible=False):
|
189 |
api_input = gr.Textbox()
|
190 |
api_output = gr.JSON()
|
191 |
+
gr.Interface(fn=decode_data, inputs=api_input, outputs=api_output, api_name="keylock-auth-decoder")
|
|
|
|
|
|
|
|
|
|
|
192 |
|
193 |
if __name__ == "__main__":
|
194 |
+
demo.launch()
|