broadfield-dev commited on
Commit
6a2d885
Β·
verified Β·
1 Parent(s): deab0d5

Update server/app.py

Browse files
Files changed (1) hide show
  1. 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
- KEYLOCK_PRIV_KEY = os.environ.get('KEYLOCK_PRIV_KEY','./DEMO_ONLY_THIS_IS_SECRET_keylock_priv_key.pem')
 
 
 
25
  KEYLOCK_STATUS_MESSAGE = ""
26
- KEY_DIR = "./keys/"
27
- if not KEYLOCK_PRIV_KEY:
 
 
 
28
  try:
29
- with open(f"{KEY_DIR}keylock_priv.pem", "r") as f:
30
- KEYLOCK_PRIV_KEY = f.read()
31
- logger.warning(f"Loaded private key from '{KEY_DIR}keylock_priv.pem'. This is for local testing only.")
32
- KEYLOCK_STATUS_MESSAGE = f"⚠️ Loaded from `{KEY_DIR}keylock_priv.pem` file. This is for local testing but insecure for production."
33
  except FileNotFoundError:
34
- logger.error("FATAL: Private key not found. API is non-functional.")
35
- KEYLOCK_STATUS_MESSAGE = f"❌ NOT FOUND. The API is non-functional. Set the `KEYLOCK_PRIV_KEY` secret or provide a `{KEY_DIR}keylock_priv.pem` file."
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
- PUBLIC_KEY_PEM_STRING = ""
41
- try:
42
- with open("keylock_pub.pem", "r") as f:
43
- PUBLIC_KEY_PEM_STRING = f.read()
44
- logger.info("Successfully loaded public key from keylock_pub.pem for display.")
45
- except FileNotFoundError:
46
- logger.error("FATAL: keylock_pub.pem not found. The UI will show an error.")
47
- PUBLIC_KEY_PEM_STRING = "Error: keylock_pub.pem file not found on the server."
48
- except Exception as e:
49
- logger.error(f"Error loading public key: {e}")
50
- PUBLIC_KEY_PEM_STRING = f"Error loading public key: {e}"
51
-
52
- def generate_rsa_keys():
53
- private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
54
- private_pem = private_key.private_bytes(
55
- encoding=serialization.Encoding.PEM,
56
- format=serialization.PrivateFormat.PKCS8,
57
- encryption_algorithm=serialization.NoEncryption()
58
- ).decode('utf-8')
59
- public_pem = private_key.public_key().public_bytes(
60
- encoding=serialization.Encoding.PEM,
61
- format=serialization.PublicFormat.SubjectPublicKeyInfo
62
- ).decode('utf-8')
63
- return private_pem, public_pem
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  def decode_data(image_base64_string: str) -> dict:
66
- if not KEYLOCK_PRIV_KEY:
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 = private_key.key_size // 8
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 {private_key.key_size}-bit key.")
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 = private_key.decrypt(
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 a secure API endpoint to decrypt and extract JSON data hidden within PNG images.")
120
 
121
  with gr.Tabs():
122
- with gr.TabItem("πŸš€ Quick Start & Documentation"):
123
  gr.Markdown("## How to Use This API")
124
- gr.Markdown("### Step 1: Get the Server's Public Key")
125
- gr.Code(value=PUBLIC_KEY_PEM_STRING, language="python", label="Server Public Key")
126
- with gr.Accordion("Step 2: Create the Encrypted Image (Client-Side Python Example)", open=False):
127
- gr.Markdown("Use the following Python code in your client application to create a KeyLock image.")
 
 
 
 
 
128
  gr.Code(
129
  language="python",
130
  label="Client-Side Image Creation Script",
131
  value="""
132
- import json; import os; import struct; from PIL import Image; import numpy as np
133
- from cryptography.hazmat.primitives.ciphers.aead import AESGCM
134
- from cryptography.hazmat.primitives import hashes, serialization
135
- from cryptography.hazmat.primitives.asymmetric import padding
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 & Error Guide"):
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. Copy the **Public Key** and overwrite the contents of the `keylock_pub.pem` file in your repository.\n"
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
- with gr.Column():
173
- output_private_key = gr.Textbox(
174
- lines=10,
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
- api_input = gr.Textbox()
205
- api_output = gr.JSON()
206
- gr.Interface(fn=decode_data, inputs=api_input, outputs=api_output, api_name="keylock-auth-decoder")
 
 
 
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()