broadfield-dev commited on
Commit
a64eda6
·
verified ·
1 Parent(s): 40b01e6

Rename server/app.py to server/server.py

Browse files
Files changed (1) hide show
  1. 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
- # 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."
@@ -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
- # --- 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
 
@@ -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
- 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}")
@@ -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
- 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="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