Update server/app.py
Browse files- server/app.py +103 -106
server/app.py
CHANGED
@@ -11,60 +11,80 @@ import numpy as np
|
|
11 |
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.hazmat.primitives.asymmetric import rsa
|
16 |
from cryptography.exceptions import InvalidTag
|
17 |
|
|
|
18 |
HEADER_BITS = 32
|
19 |
AES_GCM_NONCE_SIZE = 12
|
20 |
|
|
|
21 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
22 |
logger = logging.getLogger(__name__)
|
23 |
|
24 |
-
|
|
|
|
|
|
|
25 |
KEYLOCK_STATUS_MESSAGE = ""
|
26 |
-
|
27 |
-
|
|
|
|
|
|
|
28 |
try:
|
29 |
-
with open(
|
30 |
-
|
31 |
-
logger.warning(f"Loaded private key from '{
|
32 |
-
KEYLOCK_STATUS_MESSAGE = f"β οΈ Loaded from `{
|
33 |
except FileNotFoundError:
|
34 |
-
logger.error("FATAL: Private key not found
|
35 |
-
KEYLOCK_STATUS_MESSAGE =
|
36 |
else:
|
37 |
logger.info("Successfully loaded private key from environment variable 'KEYLOCK_PRIV_KEY'.")
|
38 |
KEYLOCK_STATUS_MESSAGE = "β
Loaded successfully from secrets/environment variable. Recommended secure configuration."
|
39 |
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
def decode_data(image_base64_string: str) -> dict:
|
66 |
-
if not
|
67 |
error_msg = "Server Error: The API is not configured with a private key."
|
|
|
68 |
raise gr.Error(error_msg)
|
69 |
try:
|
70 |
image_buffer = base64.b64decode(image_base64_string)
|
@@ -87,13 +107,11 @@ def decode_data(image_base64_string: str) -> dict:
|
|
87 |
data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
|
88 |
crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
|
89 |
|
90 |
-
private_key = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY.encode(), password=None)
|
91 |
-
|
92 |
offset = 4
|
93 |
encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
|
94 |
-
rsa_key_size_bytes =
|
95 |
if encrypted_aes_key_len != rsa_key_size_bytes:
|
96 |
-
raise ValueError(f"Key mismatch: Encrypted with a {encrypted_aes_key_len*8}-bit key, server uses {
|
97 |
|
98 |
encrypted_aes_key = crypto_payload[offset : offset + encrypted_aes_key_len]
|
99 |
offset += encrypted_aes_key_len
|
@@ -101,7 +119,7 @@ def decode_data(image_base64_string: str) -> dict:
|
|
101 |
offset += AES_GCM_NONCE_SIZE
|
102 |
ciphertext_with_tag = crypto_payload[offset:]
|
103 |
|
104 |
-
recovered_aes_key =
|
105 |
encrypted_aes_key,
|
106 |
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
|
107 |
)
|
@@ -110,100 +128,79 @@ def decode_data(image_base64_string: str) -> dict:
|
|
110 |
return json.loads(decrypted_bytes.decode('utf-8'))
|
111 |
|
112 |
except (ValueError, InvalidTag, TypeError, struct.error) as e:
|
|
|
113 |
raise gr.Error(f"Decryption failed. Image may be corrupt or used the wrong public key. Details: {e}")
|
114 |
except Exception as e:
|
|
|
115 |
raise gr.Error(f"An unexpected server error occurred. Details: {e}")
|
116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
with gr.Blocks(title="Secure Decoder API", theme=gr.themes.Soft()) as demo:
|
118 |
gr.Markdown("# π Secure KeyLock Decoder API")
|
119 |
-
gr.Markdown("This application provides
|
120 |
|
121 |
with gr.Tabs():
|
122 |
-
with gr.TabItem("π
|
123 |
gr.Markdown("## How to Use This API")
|
124 |
-
gr.Markdown(
|
125 |
-
|
126 |
-
|
127 |
-
|
|
|
|
|
|
|
|
|
|
|
128 |
gr.Code(
|
129 |
language="python",
|
130 |
label="Client-Side Image Creation Script",
|
131 |
value="""
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
def create_keylock_image(public_key_pem: str, json_data: dict, template_image_path: str, output_path: str):
|
137 |
-
public_key = serialization.load_pem_public_key(public_key_pem.encode())
|
138 |
-
data_to_encrypt = json.dumps(json_data).encode('utf-8')
|
139 |
-
aes_key = AESGCM.generate_key(bit_length=256)
|
140 |
-
nonce = os.urandom(12)
|
141 |
-
ciphertext_with_tag = AESGCM(aes_key).encrypt(nonce, data_to_encrypt, None)
|
142 |
-
encrypted_aes_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
|
143 |
-
crypto_payload = struct.pack('>I', len(encrypted_aes_key)) + encrypted_aes_key + nonce + ciphertext_with_tag
|
144 |
-
header = struct.pack('>I', len(crypto_payload))
|
145 |
-
full_binary_string = ''.join(f'{byte:08b}' for byte in header + crypto_payload)
|
146 |
-
img = Image.open(template_image_path).convert("RGB")
|
147 |
-
pixel_data = np.array(img)
|
148 |
-
if len(full_binary_string) > pixel_data.size:
|
149 |
-
raise ValueError("Data is too large for the image.")
|
150 |
-
flat_pixels = pixel_data.ravel()
|
151 |
-
for i in range(len(full_binary_string)):
|
152 |
-
flat_pixels[i] = (flat_pixels[i] & 0b11111110) | int(full_binary_string[i])
|
153 |
-
Image.fromarray(pixel_data).save(output_path, "PNG")
|
154 |
"""
|
155 |
)
|
156 |
|
157 |
-
with gr.TabItem("βοΈ Server Status &
|
158 |
gr.Markdown("## Server Status")
|
159 |
gr.Textbox(label="Private Key Status", value=KEYLOCK_STATUS_MESSAGE, interactive=False, lines=3)
|
160 |
gr.Markdown("---")
|
161 |
-
|
162 |
with gr.Accordion("π Admin: Key Pair Generator", open=False):
|
163 |
gr.Markdown(
|
164 |
"**For Administrators Only.** Use this tool to generate a new RSA key pair for the server. "
|
165 |
"**This does NOT automatically apply the keys.** To use them, you must:\n"
|
166 |
"1. Copy the **Private Key** and update the `KEYLOCK_PRIV_KEY` secret in your deployment environment.\n"
|
167 |
-
"2.
|
168 |
-
"3. Restart the server for the changes to take effect."
|
169 |
)
|
170 |
gen_keys_button = gr.Button("βοΈ Generate New 2048-bit Key Pair", variant="secondary")
|
171 |
with gr.Row():
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
label="Generated Private Key (KEEP THIS SECRET)",
|
176 |
-
interactive=False,
|
177 |
-
show_copy_button=True
|
178 |
-
)
|
179 |
-
with gr.Column():
|
180 |
-
output_public_key = gr.Textbox(
|
181 |
-
lines=10,
|
182 |
-
label="Generated Public Key (for clients & keylock_pub.pem)",
|
183 |
-
interactive=False,
|
184 |
-
show_copy_button=True
|
185 |
-
)
|
186 |
-
|
187 |
-
gen_keys_button.click(
|
188 |
-
fn=generate_rsa_keys,
|
189 |
-
inputs=None,
|
190 |
-
outputs=[output_private_key, output_public_key]
|
191 |
-
)
|
192 |
-
|
193 |
-
gr.Markdown("## Error Handling Guide")
|
194 |
-
gr.Markdown(
|
195 |
-
"""
|
196 |
-
- **`"Decryption failed. ... InvalidTag"`**: The most common error. The image was encrypted with a **different public key** or the image file was corrupted.
|
197 |
-
- **`"Decryption failed. ... Key mismatch"`**: The RSA key size used to encrypt the data does not match the server's key size.
|
198 |
-
- **`"Image data corrupt or truncated..."`**: The image file is incomplete or damaged.
|
199 |
-
- **`"Server Error: ... not configured"`**: The administrator has not set the `KEYLOCK_PRIV_KEY` secret correctly.
|
200 |
-
"""
|
201 |
-
)
|
202 |
|
|
|
203 |
with gr.Row(visible=False):
|
204 |
-
|
205 |
-
|
206 |
-
|
|
|
|
|
|
|
207 |
|
208 |
if __name__ == "__main__":
|
209 |
demo.launch()
|
|
|
11 |
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, rsa
|
|
|
15 |
from cryptography.exceptions import InvalidTag
|
16 |
|
17 |
+
# --- Constants ---
|
18 |
HEADER_BITS = 32
|
19 |
AES_GCM_NONCE_SIZE = 12
|
20 |
|
21 |
+
# --- Setup Logging ---
|
22 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
23 |
logger = logging.getLogger(__name__)
|
24 |
|
25 |
+
# --- Key Loading and Management ---
|
26 |
+
KEYLOCK_PRIV_KEY_PEM = os.environ.get('KEYLOCK_PRIV_KEY')
|
27 |
+
PRIVATE_KEY_OBJECT = None
|
28 |
+
PUBLIC_KEY_PEM_STRING = ""
|
29 |
KEYLOCK_STATUS_MESSAGE = ""
|
30 |
+
|
31 |
+
# Prioritize environment variable, then fallback to local file for development
|
32 |
+
if not KEYLOCK_PRIV_KEY_PEM:
|
33 |
+
# This path is relative to server/app.py, assuming the Space structure is `server/keys/`
|
34 |
+
dev_key_path = 'keys/DEMO_ONLY_THIS _IS_SECRET_keylock_priv_key.pem'
|
35 |
try:
|
36 |
+
with open(dev_key_path, "r") as f:
|
37 |
+
KEYLOCK_PRIV_KEY_PEM = f.read()
|
38 |
+
logger.warning(f"Loaded private key from '{dev_key_path}'. This is for local testing only.")
|
39 |
+
KEYLOCK_STATUS_MESSAGE = f"β οΈ Loaded from `{dev_key_path}`. This is for local testing but insecure for production."
|
40 |
except FileNotFoundError:
|
41 |
+
logger.error(f"FATAL: Private key not found at '{dev_key_path}' and 'KEYLOCK_PRIV_KEY' secret is not set.")
|
42 |
+
KEYLOCK_STATUS_MESSAGE = "β NOT FOUND. The API is non-functional. Set the `KEYLOCK_PRIV_KEY` secret or provide the demo key file."
|
43 |
else:
|
44 |
logger.info("Successfully loaded private key from environment variable 'KEYLOCK_PRIV_KEY'.")
|
45 |
KEYLOCK_STATUS_MESSAGE = "β
Loaded successfully from secrets/environment variable. Recommended secure configuration."
|
46 |
|
47 |
+
# Derive public key from private key if available
|
48 |
+
if KEYLOCK_PRIV_KEY_PEM:
|
49 |
+
try:
|
50 |
+
PRIVATE_KEY_OBJECT = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY_PEM.encode(), password=None)
|
51 |
+
public_key = PRIVATE_KEY_OBJECT.public_key()
|
52 |
+
PUBLIC_KEY_PEM_STRING = public_key.public_bytes(
|
53 |
+
encoding=serialization.Encoding.PEM,
|
54 |
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
55 |
+
).decode('utf-8')
|
56 |
+
KEYLOCK_STATUS_MESSAGE += "\nβ
Public key derived successfully."
|
57 |
+
except Exception as e:
|
58 |
+
logger.error(f"Failed to load private key or derive public key: {e}", exc_info=True)
|
59 |
+
PRIVATE_KEY_OBJECT = None
|
60 |
+
PUBLIC_KEY_PEM_STRING = f"Error: Could not process the configured private key. Details: {e}"
|
61 |
+
KEYLOCK_STATUS_MESSAGE += f"\nβ Failed to parse key: {e}"
|
62 |
+
|
63 |
+
# --- API Functions ---
|
64 |
+
|
65 |
+
def get_public_key():
|
66 |
+
"""API endpoint to return the server's public key."""
|
67 |
+
if not PUBLIC_KEY_PEM_STRING or "Error" in PUBLIC_KEY_PEM_STRING:
|
68 |
+
raise gr.Error("Server key is not configured correctly.")
|
69 |
+
return PUBLIC_KEY_PEM_STRING
|
70 |
+
|
71 |
+
def get_server_info():
|
72 |
+
"""API endpoint to return server documentation and metadata."""
|
73 |
+
return {
|
74 |
+
"name": "KeyLock Auth Server",
|
75 |
+
"version": "1.2",
|
76 |
+
"documentation": "This server decrypts data hidden in KeyLock images. Use the /keylock-pub endpoint to get the public key for encryption, and POST to /keylock-server with a base64 encoded image string to decrypt.",
|
77 |
+
"endpoints": {
|
78 |
+
"/keylock-pub": "GET - Returns the server's public key.",
|
79 |
+
"/keylock-info": "GET - Returns this information object.",
|
80 |
+
"/keylock-server": "POST - Decrypts a KeyLock image."
|
81 |
+
}
|
82 |
+
}
|
83 |
|
84 |
def decode_data(image_base64_string: str) -> dict:
|
85 |
+
if not PRIVATE_KEY_OBJECT:
|
86 |
error_msg = "Server Error: The API is not configured with a private key."
|
87 |
+
logger.error(error_msg)
|
88 |
raise gr.Error(error_msg)
|
89 |
try:
|
90 |
image_buffer = base64.b64decode(image_base64_string)
|
|
|
107 |
data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
|
108 |
crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
|
109 |
|
|
|
|
|
110 |
offset = 4
|
111 |
encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
|
112 |
+
rsa_key_size_bytes = PRIVATE_KEY_OBJECT.key_size // 8
|
113 |
if encrypted_aes_key_len != rsa_key_size_bytes:
|
114 |
+
raise ValueError(f"Key mismatch: Encrypted with a {encrypted_aes_key_len*8}-bit key, server uses {PRIVATE_KEY_OBJECT.key_size}-bit key.")
|
115 |
|
116 |
encrypted_aes_key = crypto_payload[offset : offset + encrypted_aes_key_len]
|
117 |
offset += encrypted_aes_key_len
|
|
|
119 |
offset += AES_GCM_NONCE_SIZE
|
120 |
ciphertext_with_tag = crypto_payload[offset:]
|
121 |
|
122 |
+
recovered_aes_key = PRIVATE_KEY_OBJECT.decrypt(
|
123 |
encrypted_aes_key,
|
124 |
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
|
125 |
)
|
|
|
128 |
return json.loads(decrypted_bytes.decode('utf-8'))
|
129 |
|
130 |
except (ValueError, InvalidTag, TypeError, struct.error) as e:
|
131 |
+
logger.warning(f"Decryption failed: {e}")
|
132 |
raise gr.Error(f"Decryption failed. Image may be corrupt or used the wrong public key. Details: {e}")
|
133 |
except Exception as e:
|
134 |
+
logger.error(f"An unexpected server error occurred during decryption: {e}", exc_info=True)
|
135 |
raise gr.Error(f"An unexpected server error occurred. Details: {e}")
|
136 |
|
137 |
+
def generate_rsa_keys():
|
138 |
+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
139 |
+
private_pem = private_key.private_bytes(
|
140 |
+
encoding=serialization.Encoding.PEM,
|
141 |
+
format=serialization.PrivateFormat.PKCS8,
|
142 |
+
encryption_algorithm=serialization.NoEncryption()
|
143 |
+
).decode('utf-8')
|
144 |
+
public_pem = private_key.public_key().public_bytes(
|
145 |
+
encoding=serialization.Encoding.PEM,
|
146 |
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
147 |
+
).decode('utf-8')
|
148 |
+
return private_pem, public_pem
|
149 |
+
|
150 |
+
# --- Gradio UI ---
|
151 |
with gr.Blocks(title="Secure Decoder API", theme=gr.themes.Soft()) as demo:
|
152 |
gr.Markdown("# π Secure KeyLock Decoder API")
|
153 |
+
gr.Markdown("This application provides secure API endpoints to decrypt and extract JSON data hidden within PNG images.")
|
154 |
|
155 |
with gr.Tabs():
|
156 |
+
with gr.TabItem("π API Documentation"):
|
157 |
gr.Markdown("## How to Use This API")
|
158 |
+
gr.Markdown(
|
159 |
+
"This server exposes three main API endpoints for programmatic use:\n"
|
160 |
+
"1. **`/keylock-info`**: A `GET` request returns a JSON object with server metadata.\n"
|
161 |
+
"2. **`/keylock-pub`**: A `GET` request returns the server's public RSA key required for encryption.\n"
|
162 |
+
"3. **`/keylock-server`**: A `POST` request with a base64-encoded image string decrypts the data."
|
163 |
+
)
|
164 |
+
gr.Markdown("### Server's Public Key")
|
165 |
+
gr.Code(value=PUBLIC_KEY_PEM_STRING, language="pem", label="Server Public Key (from /keylock-pub)")
|
166 |
+
with gr.Accordion("Client-Side Python Example for Image Creation", open=False):
|
167 |
gr.Code(
|
168 |
language="python",
|
169 |
label="Client-Side Image Creation Script",
|
170 |
value="""
|
171 |
+
# See the KeyLock-Auth-Client repo for a full implementation.
|
172 |
+
# The core logic involves using the server's public key to encrypt
|
173 |
+
# a one-time AES key, which in turn encrypts your JSON payload.
|
174 |
+
# This encrypted bundle is then embedded into an image's pixel data.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
"""
|
176 |
)
|
177 |
|
178 |
+
with gr.TabItem("βοΈ Server Status & Admin"):
|
179 |
gr.Markdown("## Server Status")
|
180 |
gr.Textbox(label="Private Key Status", value=KEYLOCK_STATUS_MESSAGE, interactive=False, lines=3)
|
181 |
gr.Markdown("---")
|
182 |
+
|
183 |
with gr.Accordion("π Admin: Key Pair Generator", open=False):
|
184 |
gr.Markdown(
|
185 |
"**For Administrators Only.** Use this tool to generate a new RSA key pair for the server. "
|
186 |
"**This does NOT automatically apply the keys.** To use them, you must:\n"
|
187 |
"1. Copy the **Private Key** and update the `KEYLOCK_PRIV_KEY` secret in your deployment environment.\n"
|
188 |
+
"2. Restart the server for the changes to take effect. The public key will be derived automatically."
|
|
|
189 |
)
|
190 |
gen_keys_button = gr.Button("βοΈ Generate New 2048-bit Key Pair", variant="secondary")
|
191 |
with gr.Row():
|
192 |
+
output_private_key = gr.Textbox(lines=10, label="Generated Private Key (KEEP THIS SECRET)", interactive=False, show_copy_button=True)
|
193 |
+
output_public_key = gr.Textbox(lines=10, label="Generated Public Key (will be auto-derived)", interactive=False, show_copy_button=True)
|
194 |
+
gen_keys_button.click(fn=generate_rsa_keys, inputs=None, outputs=[output_private_key, output_public_key])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
|
196 |
+
# --- API Endpoints (Hidden from UI) ---
|
197 |
with gr.Row(visible=False):
|
198 |
+
# API Endpoint 1: Public Key
|
199 |
+
gr.Interface(fn=get_public_key, inputs=None, outputs=gr.Textbox(), api_name="keylock-pub")
|
200 |
+
# API Endpoint 2: Info
|
201 |
+
gr.Interface(fn=get_server_info, inputs=None, outputs=gr.JSON(), api_name="keylock-info")
|
202 |
+
# API Endpoint 3: Decrypt (renamed)
|
203 |
+
gr.Interface(fn=decode_data, inputs=gr.Textbox(), outputs=gr.JSON(), api_name="keylock-server")
|
204 |
|
205 |
if __name__ == "__main__":
|
206 |
demo.launch()
|