Rename server/app.py to server/server.py
Browse files- server/{app.py → server.py} +45 -106
server/{app.py → server.py}
RENAMED
@@ -14,29 +14,24 @@ 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 |
-
|
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
|
39 |
-
KEYLOCK_STATUS_MESSAGE = f"⚠️ Loaded from
|
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."
|
@@ -44,7 +39,6 @@ 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)
|
@@ -60,24 +54,36 @@ if KEYLOCK_PRIV_KEY_PEM:
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
76 |
-
"documentation": "This server decrypts data hidden in KeyLock images. Use
|
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 |
|
@@ -90,43 +96,41 @@ def decode_data(image_base64_string: str) -> dict:
|
|
90 |
image_buffer = base64.b64decode(image_base64_string)
|
91 |
img = Image.open(io.BytesIO(image_buffer)).convert("RGB")
|
92 |
pixel_data = np.array(img).ravel()
|
93 |
-
|
94 |
-
if pixel_data.size < HEADER_BITS:
|
95 |
-
raise ValueError(f"Image is too small. Minimum pixel count: {HEADER_BITS}")
|
96 |
-
|
97 |
header_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[:HEADER_BITS])
|
98 |
data_length = int(header_binary_string, 2)
|
99 |
-
|
100 |
-
if data_length == 0: return {}
|
101 |
-
|
102 |
data_bits_count = data_length * 8
|
103 |
end_offset = HEADER_BITS + data_bits_count
|
104 |
-
if pixel_data.size < end_offset:
|
105 |
-
raise ValueError("Image data corrupt or truncated.")
|
106 |
-
|
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
|
118 |
nonce = crypto_payload[offset : offset + AES_GCM_NONCE_SIZE]
|
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 |
-
)
|
126 |
decrypted_bytes = AESGCM(recovered_aes_key).decrypt(nonce, ciphertext_with_tag, None)
|
127 |
-
|
128 |
-
|
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}")
|
@@ -136,71 +140,6 @@ def decode_data(image_base64_string: str) -> dict:
|
|
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 |
-
|
141 |
-
|
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="python", 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()
|
|
|
14 |
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
15 |
from cryptography.exceptions import InvalidTag
|
16 |
|
|
|
17 |
HEADER_BITS = 32
|
18 |
AES_GCM_NONCE_SIZE = 12
|
19 |
|
|
|
20 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
21 |
logger = logging.getLogger(__name__)
|
22 |
|
|
|
23 |
KEYLOCK_PRIV_KEY_PEM = os.environ.get('KEYLOCK_PRIV_KEY')
|
24 |
PRIVATE_KEY_OBJECT = None
|
25 |
PUBLIC_KEY_PEM_STRING = ""
|
26 |
KEYLOCK_STATUS_MESSAGE = ""
|
27 |
|
|
|
28 |
if not KEYLOCK_PRIV_KEY_PEM:
|
29 |
+
dev_key_path = os.path.join(os.path.dirname(__file__), '..', 'keys', 'DEMO_ONLY_THIS _IS_SECRET_keylock_priv_key.pem')
|
|
|
30 |
try:
|
31 |
with open(dev_key_path, "r") as f:
|
32 |
KEYLOCK_PRIV_KEY_PEM = f.read()
|
33 |
+
logger.warning(f"Loaded private key from dev path. This is for local testing only.")
|
34 |
+
KEYLOCK_STATUS_MESSAGE = f"⚠️ Loaded from development key file. This is for local testing but insecure for production."
|
35 |
except FileNotFoundError:
|
36 |
logger.error(f"FATAL: Private key not found at '{dev_key_path}' and 'KEYLOCK_PRIV_KEY' secret is not set.")
|
37 |
KEYLOCK_STATUS_MESSAGE = "❌ NOT FOUND. The API is non-functional. Set the `KEYLOCK_PRIV_KEY` secret or provide the demo key file."
|
|
|
39 |
logger.info("Successfully loaded private key from environment variable 'KEYLOCK_PRIV_KEY'.")
|
40 |
KEYLOCK_STATUS_MESSAGE = "✅ Loaded successfully from secrets/environment variable. Recommended secure configuration."
|
41 |
|
|
|
42 |
if KEYLOCK_PRIV_KEY_PEM:
|
43 |
try:
|
44 |
PRIVATE_KEY_OBJECT = serialization.load_pem_private_key(KEYLOCK_PRIV_KEY_PEM.encode(), password=None)
|
|
|
54 |
PUBLIC_KEY_PEM_STRING = f"Error: Could not process the configured private key. Details: {e}"
|
55 |
KEYLOCK_STATUS_MESSAGE += f"\n❌ Failed to parse key: {e}"
|
56 |
|
57 |
+
MOCK_USER_DATABASE = {
|
58 |
+
"sk-12345-abcde": {"user": "demo-user", "permissions": "read"},
|
59 |
+
"sk-67890-fghij": {"user": "admin-user", "permissions": "read,write,delete"}
|
60 |
+
}
|
61 |
+
|
62 |
+
def example_authenticate(api_key: str, user_id: str) -> bool:
|
63 |
+
if not api_key or not user_id:
|
64 |
+
return False
|
65 |
+
db_entry = MOCK_USER_DATABASE.get(api_key)
|
66 |
+
if db_entry and db_entry.get("user") == user_id:
|
67 |
+
logger.info(f"Authentication successful for user '{user_id}'.")
|
68 |
+
return True
|
69 |
+
else:
|
70 |
+
logger.warning(f"Authentication failed for user '{user_id}' with key '{api_key[:8]}...'.")
|
71 |
+
return False
|
72 |
|
73 |
def get_public_key():
|
|
|
74 |
if not PUBLIC_KEY_PEM_STRING or "Error" in PUBLIC_KEY_PEM_STRING:
|
75 |
raise gr.Error("Server key is not configured correctly.")
|
76 |
return PUBLIC_KEY_PEM_STRING
|
77 |
|
78 |
def get_server_info():
|
|
|
79 |
return {
|
80 |
"name": "KeyLock Auth Server",
|
81 |
+
"version": "1.3",
|
82 |
+
"documentation": "This server decrypts data hidden in KeyLock images and performs a mock authentication. Use /keylock-pub to get the public key, and POST to /keylock-server with a base64 image string to decrypt and authenticate.",
|
83 |
"endpoints": {
|
84 |
"/keylock-pub": "GET - Returns the server's public key.",
|
85 |
"/keylock-info": "GET - Returns this information object.",
|
86 |
+
"/keylock-server": "POST - Decrypts a KeyLock image and attempts authentication."
|
87 |
}
|
88 |
}
|
89 |
|
|
|
96 |
image_buffer = base64.b64decode(image_base64_string)
|
97 |
img = Image.open(io.BytesIO(image_buffer)).convert("RGB")
|
98 |
pixel_data = np.array(img).ravel()
|
|
|
|
|
|
|
|
|
99 |
header_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[:HEADER_BITS])
|
100 |
data_length = int(header_binary_string, 2)
|
101 |
+
if data_length == 0: raise ValueError("No data found in image.")
|
|
|
|
|
102 |
data_bits_count = data_length * 8
|
103 |
end_offset = HEADER_BITS + data_bits_count
|
104 |
+
if pixel_data.size < end_offset: raise ValueError("Image data corrupt or truncated.")
|
|
|
|
|
105 |
data_binary_string = "".join(str(pixel & 1) for pixel in pixel_data[HEADER_BITS:end_offset])
|
106 |
crypto_payload = int(data_binary_string, 2).to_bytes(data_length, byteorder='big')
|
|
|
107 |
offset = 4
|
108 |
encrypted_aes_key_len = struct.unpack('>I', crypto_payload[:offset])[0]
|
|
|
|
|
|
|
|
|
109 |
encrypted_aes_key = crypto_payload[offset : offset + encrypted_aes_key_len]
|
110 |
offset += encrypted_aes_key_len
|
111 |
nonce = crypto_payload[offset : offset + AES_GCM_NONCE_SIZE]
|
112 |
offset += AES_GCM_NONCE_SIZE
|
113 |
ciphertext_with_tag = crypto_payload[offset:]
|
114 |
+
recovered_aes_key = PRIVATE_KEY_OBJECT.decrypt(encrypted_aes_key, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
|
|
|
|
|
|
|
|
|
115 |
decrypted_bytes = AESGCM(recovered_aes_key).decrypt(nonce, ciphertext_with_tag, None)
|
116 |
+
decrypted_payload = json.loads(decrypted_bytes.decode('utf-8'))
|
117 |
+
logger.info(f"Successfully decoded payload: {decrypted_payload}")
|
118 |
+
api_key = decrypted_payload.get('API_KEY')
|
119 |
+
user_id = decrypted_payload.get('USER')
|
120 |
+
is_authenticated = example_authenticate(api_key=api_key, user_id=user_id)
|
121 |
+
if is_authenticated:
|
122 |
+
return {
|
123 |
+
"authentication_status": "Success",
|
124 |
+
"message": f"User '{user_id}' successfully authenticated.",
|
125 |
+
"granted_permissions": MOCK_USER_DATABASE[api_key]['permissions'],
|
126 |
+
"decoded_payload": decrypted_payload
|
127 |
+
}
|
128 |
+
else:
|
129 |
+
return {
|
130 |
+
"authentication_status": "Failed",
|
131 |
+
"message": "Authentication failed. Invalid credentials provided in the image.",
|
132 |
+
"decoded_payload": decrypted_payload
|
133 |
+
}
|
134 |
except (ValueError, InvalidTag, TypeError, struct.error) as e:
|
135 |
logger.warning(f"Decryption failed: {e}")
|
136 |
raise gr.Error(f"Decryption failed. Image may be corrupt or used the wrong public key. Details: {e}")
|
|
|
140 |
|
141 |
def generate_rsa_keys():
|
142 |
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
143 |
+
private_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()).decode('utf-8')
|
144 |
+
public_pem = private_key.public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode('utf-8')
|
145 |
+
return private_pem, public_pem
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|