mike23415 commited on
Commit
3d367a5
·
verified ·
1 Parent(s): 79b702c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -712
app.py CHANGED
@@ -1,10 +1,8 @@
1
  import sys
2
  import os
 
3
  from flask import Flask, request, jsonify, send_file
4
  from flask_cors import CORS
5
- from flask_limiter import Limiter
6
- from flask_limiter.util import get_remote_address
7
- from collections import defaultdict
8
  from werkzeug.utils import secure_filename
9
  import tempfile
10
  import uuid
@@ -23,32 +21,10 @@ import threading
23
  from database import init_supabase, get_supabase
24
  from auth import signup_user, login_user, check_username_exists, verify_token
25
  from functools import wraps
26
- from datetime import datetime, timedelta
27
- from cryptography.hazmat.primitives import hashes, serialization
28
- from cryptography.hazmat.primitives.asymmetric import ec
29
- from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers, SECP256R1
30
- from cryptography.hazmat.backends import default_backend
31
- from cryptography.exceptions import InvalidSignature
32
- import secrets
33
 
34
  app = Flask(__name__)
35
  CORS(app)
36
 
37
- @app.after_request
38
- def add_security_headers(response):
39
- """Add security headers"""
40
- response.headers['X-Content-Type-Options'] = 'nosniff'
41
- response.headers['X-Frame-Options'] = 'DENY'
42
- response.headers['X-XSS-Protection'] = '1; mode=block'
43
- response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
44
-
45
- # Cache control for sensitive data
46
- if '/api/' in request.path:
47
- response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, private'
48
- response.headers['Pragma'] = 'no-cache'
49
-
50
- return response
51
-
52
  print("Initializing Supabase...")
53
  init_supabase()
54
 
@@ -71,6 +47,9 @@ socketio = SocketIO(
71
  transports=['websocket', 'polling'] # Explicitly allow both
72
  )
73
 
 
 
 
74
  # In-memory storage
75
  SECRETS = {} # { id: { data, file_data, file_type, expire_at, view_once, theme, analytics, etc. } }
76
  SHORT_LINKS = {} # { short_id: full_id }
@@ -79,11 +58,6 @@ SCREEN_SHARE_ROOMS = {} # { room_id: { host_hash, participants, settings, creat
79
  CURSOR_STATES = {}
80
  CHAT_ROOMS = {}
81
 
82
- # NEW: Rate limiting storage
83
- SESSION_REQUESTS = defaultdict(list) # {uuid: [timestamp1, timestamp2, ...]}
84
- SEEN_NONCES = {} # {nonce: timestamp}
85
- MESSAGE_VALIDITY_WINDOW = 300 # 5 minutes
86
-
87
  # Configuration
88
  MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
89
  ALLOWED_EXTENSIONS = {
@@ -96,102 +70,6 @@ ALLOWED_EXTENSIONS = {
96
  ONLINE_USERS = {}
97
  FILE_TRANSFERS = {}
98
 
99
- def import_ecdsa_public_key(base64_key):
100
- """Import ECDSA P-256 public key from base64 (RAW format from Web Crypto API)"""
101
- try:
102
- # Web Crypto API exports ECDSA public keys in RAW format (65 bytes for P-256)
103
- # Format: 0x04 || X (32 bytes) || Y (32 bytes)
104
- key_bytes = base64.b64decode(base64_key)
105
-
106
- print(f"📊 Public key bytes length: {len(key_bytes)}")
107
-
108
- # Expected: 65 bytes for uncompressed P-256 point
109
- if len(key_bytes) != 65:
110
- print(f"❌ Invalid key length: expected 65, got {len(key_bytes)}")
111
- return None
112
-
113
- # Check format marker (should be 0x04 for uncompressed point)
114
- if key_bytes[0] != 0x04:
115
- print(f"❌ Invalid key format marker: {hex(key_bytes[0])}")
116
- return None
117
-
118
- # Extract X and Y coordinates (32 bytes each)
119
- x = int.from_bytes(key_bytes[1:33], byteorder='big')
120
- y = int.from_bytes(key_bytes[33:65], byteorder='big')
121
-
122
- # Create ECDSA public key from coordinates
123
- from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers, SECP256R1
124
-
125
- public_numbers = EllipticCurvePublicNumbers(x, y, SECP256R1())
126
- public_key = public_numbers.public_key(default_backend())
127
-
128
- print(f"✅ Successfully imported ECDSA public key")
129
- return public_key
130
-
131
- except Exception as e:
132
- print(f"❌ Failed to import ECDSA public key: {e}")
133
- import traceback
134
- traceback.print_exc()
135
- return None
136
-
137
- def verify_ecdsa_signature(public_key_base64, challenge, signature_base64):
138
- """Verify ECDSA signature for challenge-response auth"""
139
- try:
140
- print(f"\n=== ECDSA SIGNATURE VERIFICATION ===")
141
- print(f"Challenge (base64): {challenge[:30]}...")
142
- print(f"Signature (base64): {signature_base64[:30]}...")
143
- print(f"Public Key (base64): {public_key_base64[:50]}...")
144
-
145
- # Import public key (RAW format from Web Crypto API)
146
- public_key = import_ecdsa_public_key(public_key_base64)
147
- if not public_key:
148
- print("❌ Failed to import public key")
149
- print("=====================================\n")
150
- return False
151
-
152
- # Decode signature from base64
153
- signature_bytes = base64.b64decode(signature_base64)
154
-
155
- print(f"📊 Signature bytes length: {len(signature_bytes)}")
156
-
157
- # ✅ FIX: Decode the base64 challenge to bytes (match frontend)
158
- # Frontend does: atob(challenge) before signing
159
- challenge_bytes = base64.b64decode(challenge)
160
-
161
- print(f"📊 Challenge bytes length: {len(challenge_bytes)}")
162
-
163
- # Verify signature using ECDSA with SHA-256
164
- public_key.verify(
165
- signature_bytes,
166
- challenge_bytes,
167
- ec.ECDSA(hashes.SHA256())
168
- )
169
-
170
- print("✅ Signature verification successful!")
171
- print("=====================================\n")
172
- return True
173
-
174
- except InvalidSignature:
175
- print("❌ Invalid ECDSA signature - cryptographic verification failed")
176
- print("=====================================\n")
177
- return False
178
- except Exception as e:
179
- print(f"❌ ECDSA verification error: {e}")
180
- import traceback
181
- traceback.print_exc()
182
- print("=====================================\n")
183
- return False
184
-
185
- # Challenge storage for ECDSA auth (temporary, expires in 60s)
186
- CHALLENGE_STORE = {} # {room_id: {challenge, created_at}}
187
-
188
- def cleanup_old_challenges():
189
- """Remove expired challenges"""
190
- now = time.time()
191
- expired = [k for k, v in CHALLENGE_STORE.items() if now - v['created_at'] > 60]
192
- for key in expired:
193
- del CHALLENGE_STORE[key]
194
-
195
  def assign_cursor_color(existing_participants):
196
  """Assign unique cursor color to new participant"""
197
  import random
@@ -268,68 +146,6 @@ def get_location_info(ip):
268
  'timezone': 'Unknown'
269
  }
270
 
271
- def check_rate_limit(session_uuid, limit, window_seconds):
272
- """
273
- Check if session_uuid exceeded rate limit
274
- Returns: (is_allowed: bool, message: str)
275
- """
276
- now = time.time()
277
-
278
- if not session_uuid:
279
- return False, "Session UUID required"
280
-
281
- # Get request history for this session
282
- requests = SESSION_REQUESTS[session_uuid]
283
-
284
- # Remove old requests outside window
285
- requests = [ts for ts in requests if now - ts < window_seconds]
286
-
287
- # Check limit
288
- if len(requests) >= limit:
289
- return False, f"Rate limit exceeded: {limit} requests per {window_seconds}s"
290
-
291
- # Add current request
292
- requests.append(now)
293
- SESSION_REQUESTS[session_uuid] = requests
294
-
295
- return True, "OK"
296
-
297
- def verify_nonce_and_timestamp(nonce, timestamp):
298
- """
299
- Prevent replay attacks
300
- Returns: (is_valid: bool, message: str)
301
- """
302
- current_time = time.time()
303
-
304
- if not nonce:
305
- return False, "Nonce required"
306
-
307
- # Check if message is too old
308
- if current_time - timestamp > MESSAGE_VALIDITY_WINDOW:
309
- return False, "Message expired (older than 5 minutes)"
310
-
311
- # Check if message is from the future
312
- if timestamp > current_time + 60:
313
- return False, "Invalid timestamp"
314
-
315
- # Check if nonce was already used
316
- if nonce in SEEN_NONCES:
317
- return False, "Replay attack detected"
318
-
319
- # Store nonce
320
- SEEN_NONCES[nonce] = timestamp
321
-
322
- # Cleanup old nonces
323
- cleanup_old_nonces(current_time)
324
-
325
- return True, "OK"
326
-
327
- def cleanup_old_nonces(current_time):
328
- """Remove expired nonces"""
329
- expired = [k for k, v in SEEN_NONCES.items() if current_time - v > MESSAGE_VALIDITY_WINDOW]
330
- for key in expired:
331
- del SEEN_NONCES[key]
332
-
333
  def generate_qr_code(data):
334
  """Generate QR code for the given data"""
335
  qr = qrcode.QRCode(
@@ -423,40 +239,10 @@ def store():
423
  try:
424
  form = request.form
425
  data = form.get("data")
426
- session_uuid = form.get("session_uuid")
427
- nonce = form.get("nonce")
428
- timestamp_str = form.get("timestamp")
429
 
430
- # Validate required fields
431
  if not data:
432
  return jsonify({"error": "Data is required"}), 400
433
 
434
- if not session_uuid:
435
- return jsonify({"error": "Session UUID required"}), 400
436
-
437
- if not nonce:
438
- return jsonify({"error": "Nonce required"}), 400
439
-
440
- if not timestamp_str:
441
- return jsonify({"error": "Timestamp required"}), 400
442
-
443
- try:
444
- timestamp = float(timestamp_str)
445
- except ValueError:
446
- return jsonify({"error": "Invalid timestamp"}), 400
447
-
448
- # Check rate limit (10 secrets per minute)
449
- allowed, message = check_rate_limit(session_uuid, limit=10, window_seconds=60)
450
- if not allowed:
451
- print(f"[RateLimit] Blocked {session_uuid[:8]}... - {message}")
452
- return jsonify({"error": message}), 429
453
-
454
- # Verify nonce and timestamp (prevent replay attacks)
455
- valid, message = verify_nonce_and_timestamp(nonce, timestamp)
456
- if not valid:
457
- print(f"[Security] Blocked - {message}")
458
- return jsonify({"error": message}), 400
459
-
460
  # Parse parameters
461
  ttl = int(form.get("ttl", 300))
462
  view_once = form.get("view_once", "false").lower() == "true"
@@ -539,14 +325,6 @@ def store():
539
  def fetch(secret_id):
540
  """Fetch and decrypt secret with analytics - MODIFIED TO HANDLE verify_only"""
541
  try:
542
- # ADD THIS SECTION FOR RATE LIMITING:
543
- session_uuid = request.args.get("session_uuid")
544
-
545
- if session_uuid:
546
- allowed, message = check_rate_limit(session_uuid, limit=20, window_seconds=60)
547
- if not allowed:
548
- print(f"[RateLimit] Fetch blocked {session_uuid[:8]}... - {message}")
549
- return jsonify({"error": message}), 429
550
  # Check if it's a short link
551
  if secret_id in SHORT_LINKS:
552
  secret_id = SHORT_LINKS[secret_id]
@@ -781,16 +559,6 @@ def get_stats():
781
  except Exception as e:
782
  return jsonify({"error": str(e)}), 500
783
 
784
- # NEW ENDPOINT: Ping endpoint for uptime monitoring
785
- @app.route('/ping', methods=['GET'])
786
- def ping():
787
- """Simple ping endpoint to keep the service alive"""
788
- return jsonify({
789
- "status": "alive",
790
- "timestamp": datetime.utcnow().isoformat(),
791
- "message": "Pong! Service is awake."
792
- }), 200
793
-
794
  @app.route("/api/cleanup", methods=["POST"])
795
  def cleanup_expired():
796
  """Clean up expired secrets"""
@@ -833,68 +601,41 @@ def create_chat_room():
833
  max_receivers = int(form.get("max_receivers", 5))
834
  password = form.get("password", "")
835
  allow_files = form.get("allow_files", "true").lower() == "true"
836
- admin_public_key = form.get("admin_public_key", "") # NEW: ECDSA public key
837
-
838
- if not admin_public_key:
839
- return jsonify({"error": "Admin public key required"}), 400
840
 
841
  room_id = str(uuid.uuid4())
842
- admin_session = str(uuid.uuid4()) # Still used for admin link
843
-
844
- # Generate individual receiver tokens
845
- receiver_tokens = []
846
- for i in range(max_receivers):
847
- receiver_tokens.append({
848
- "token": str(uuid.uuid4()),
849
- "receiver_number": i + 1,
850
- "claimed": False,
851
- "public_key": None, # NEW: Will be set when receiver joins
852
- "claimed_at": None
853
- })
854
-
855
- expires_at = time.time() + ttl
856
 
857
  CHAT_ROOMS[room_id] = {
858
  "admin_session": admin_session,
859
- "admin_ecdsa_public_key": admin_public_key, # NEW: Store admin's ECDSA public key
860
  "created_at": time.time(),
861
- "expires_at": expires_at,
862
  "settings": {
863
  "max_receivers": max_receivers,
864
  "password": password,
865
  "allow_files": allow_files,
866
  "burn_on_admin_exit": True
867
  },
868
- "receiver_tokens": receiver_tokens,
869
  "active_sessions": {},
870
  "receiver_counter": 0
871
  }
872
 
873
- print(f"✅ Room created: {room_id[:8]}... (TTL: {ttl}s, Expires: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_at))})")
874
- print(f"🔑 Admin public key stored (length: {len(admin_public_key)})")
875
 
876
- # Return IDs + receiver tokens
 
877
  return jsonify({
878
  "room_id": room_id,
879
  "admin_session": admin_session,
880
- "receiver_tokens": [{"token": t["token"], "receiver_number": t["receiver_number"]} for t in receiver_tokens],
881
- "expires_at": expires_at # Return as Unix timestamp (frontend converts to local time)
882
  })
883
 
884
  except Exception as e:
885
- print(f"❌ Error creating chat room: {e}")
886
- import traceback
887
- traceback.print_exc()
888
  return jsonify({"error": str(e)}), 500
889
 
890
  @app.route("/api/chat/join/<room_id>")
891
  def join_chat_room(room_id):
892
- """Join chat room - FIRST TIME ONLY (registration)"""
893
  try:
894
  password = request.args.get("password", "")
895
  admin_session = request.args.get("admin", "")
896
- receiver_token = request.args.get("receiver", "")
897
- public_key = request.args.get("public_key", "")
898
 
899
  if room_id not in CHAT_ROOMS:
900
  return jsonify({"error": "Chat room not found"}), 404
@@ -908,324 +649,25 @@ def join_chat_room(room_id):
908
  # Check password
909
  if room["settings"]["password"] and password != room["settings"]["password"]:
910
  return jsonify({"error": "Wrong password"}), 403
911
-
912
- # ADMIN FIRST JOIN (Registration)
913
- if admin_session and admin_session == room["admin_session"]:
914
- if not public_key:
915
- return jsonify({"error": "Public key required for admin join"}), 400
916
-
917
- # Check if this is first join or rejoin
918
- if public_key == room["admin_ecdsa_public_key"]:
919
- # This is first join (keys match from creation)
920
- print(f"✅ Admin first join with registered ECDSA key")
921
-
922
- role = "admin"
923
- session_id = admin_session
924
- receiver_number = None
925
-
926
- return jsonify({
927
- "session_id": session_id,
928
- "role": role,
929
- "receiver_number": receiver_number,
930
- "room_settings": room["settings"],
931
- "expires_at": room["expires_at"]
932
- })
933
- else:
934
- # Different key - this should use challenge-response
935
- return jsonify({
936
- "error": "Use challenge-response for rejoin (/api/chat/challenge)"
937
- }), 400
938
-
939
- # RECEIVER FIRST JOIN (Registration)
940
- if not receiver_token or not public_key:
941
- return jsonify({"error": "Receiver token and public key required"}), 400
942
-
943
- # Find the token
944
- token_data = None
945
- for token in room["receiver_tokens"]:
946
- if token["token"] == receiver_token:
947
- token_data = token
948
- break
949
-
950
- if not token_data:
951
- return jsonify({"error": "Invalid receiver token"}), 403
952
-
953
- # Check if token already claimed
954
- if token_data["claimed"]:
955
- # Already claimed - should use challenge-response
956
- return jsonify({
957
- "error": "Token already claimed. Use challenge-response for rejoin (/api/chat/challenge)"
958
- }), 400
959
-
960
- # First time claim - BIND the ECDSA public key to token
961
- token_data["claimed"] = True
962
- token_data["ecdsa_public_key"] = public_key
963
- token_data["claimed_at"] = time.time()
964
- print(f"🔑 Receiver #{token_data['receiver_number']} registered with ECDSA key")
965
-
966
- # ✅ ADD THIS DETAILED LOGGING:
967
- print(f"\n{'='*60}")
968
- print(f"🔑 ECDSA KEY REGISTRATION")
969
- print(f"{'='*60}")
970
- print(f"Room ID: {room_id[:8]}...")
971
- print(f"Receiver #: {token_data['receiver_number']}")
972
- print(f"Token: {receiver_token[:8]}...")
973
- print(f"ECDSA Public Key (stored):")
974
- print(f" - Length: {len(public_key)} characters")
975
- print(f" - First 50 chars: {public_key[:50]}")
976
- print(f" - Last 20 chars: {public_key[-20:]}")
977
- print(f" - Full key: {public_key}")
978
- print(f"{'='*60}\n")
979
-
980
- # Verify storage
981
- print(f"🔍 VERIFICATION: Reading back stored key...")
982
- stored_key = token_data.get("ecdsa_public_key")
983
- print(f" - Stored key exists: {stored_key is not None}")
984
- if stored_key:
985
- print(f" - Stored key length: {len(stored_key)}")
986
- print(f" - Keys match: {stored_key == public_key}")
987
- print(f" - Stored key: {stored_key}")
988
- print(f"{'='*60}\n")
989
-
990
- role = "receiver"
991
- session_id = str(uuid.uuid4())
992
- receiver_number = token_data["receiver_number"]
993
-
994
- return jsonify({
995
- "session_id": session_id,
996
- "role": role,
997
- "receiver_number": receiver_number,
998
- "room_settings": room["settings"],
999
- "expires_at": room["expires_at"]
1000
- })
1001
-
1002
- except Exception as e:
1003
- print(f"❌ Error joining chat room: {e}")
1004
- import traceback
1005
- traceback.print_exc()
1006
- return jsonify({"error": str(e)}), 500
1007
-
1008
- @socketio.on('request_challenge')
1009
- def handle_request_challenge(data):
1010
- """Generate challenge for ECDSA authentication (rejoin) - via WebSocket"""
1011
- try:
1012
- room_id = data.get("room_id")
1013
- role = data.get("role")
1014
- identifier = data.get("identifier") # admin_session or receiver_token
1015
-
1016
- print(f"\n{'='*60}")
1017
- print(f"🎲 CHALLENGE REQUEST")
1018
- print(f"{'='*60}")
1019
- print(f"Room: {room_id[:8] if room_id else 'None'}...")
1020
- print(f"Role: {role}")
1021
- print(f"Identifier: {identifier[:8] if identifier else 'None'}...")
1022
-
1023
- if not room_id or not role or not identifier:
1024
- emit('challenge_error', {"error": "Missing required fields"})
1025
- return
1026
-
1027
- if room_id not in CHAT_ROOMS:
1028
- emit('challenge_error', {"error": "Chat room not found"})
1029
- return
1030
-
1031
- room = CHAT_ROOMS[room_id]
1032
-
1033
- # Check if expired
1034
- if time.time() > room["expires_at"]:
1035
- emit('challenge_error', {"error": "Chat room has expired"})
1036
- return
1037
-
1038
- # Verify identifier exists
1039
- if role == "admin":
1040
- if identifier != room["admin_session"]:
1041
- emit('challenge_error', {"error": "Invalid admin session"})
1042
- return
1043
- print(f"✅ Admin session valid")
1044
-
1045
- elif role == "receiver":
1046
- # Find token
1047
- token_found = False
1048
- for token in room["receiver_tokens"]:
1049
- if token["token"] == identifier:
1050
- if not token["claimed"]:
1051
- emit('challenge_error', {"error": "Token not yet registered"})
1052
- return
1053
-
1054
- if not token.get("ecdsa_public_key"):
1055
- emit('challenge_error', {"error": "No public key registered"})
1056
- return
1057
-
1058
- # ✅ ADD THIS DETAILED LOGGING:
1059
- print(f"✅ Found receiver token")
1060
- print(f"Token data:")
1061
- print(f" - Receiver #: {token['receiver_number']}")
1062
- print(f" - Claimed: {token['claimed']}")
1063
- print(f" - Claimed at: {token.get('claimed_at')}")
1064
- print(f" - ECDSA key exists: {token.get('ecdsa_public_key') is not None}")
1065
- if token.get('ecdsa_public_key'):
1066
- ecdsa_key = token['ecdsa_public_key']
1067
- print(f" - ECDSA key length: {len(ecdsa_key)}")
1068
- print(f" - ECDSA key (first 50): {ecdsa_key[:50]}")
1069
- print(f" - ECDSA key (last 20): {ecdsa_key[-20:]}")
1070
- print(f" - Full ECDSA key: {ecdsa_key}")
1071
-
1072
- print(f"✅ Found receiver token, public_key exists")
1073
- token_found = True
1074
- break
1075
 
1076
- if not token_found:
1077
- emit('challenge_error', {"error": "Invalid receiver token"})
1078
- return
 
 
 
1079
  else:
1080
- emit('challenge_error', {"error": "Invalid role"})
1081
- return
1082
-
1083
- # Generate random challenge (32 bytes)
1084
- challenge_bytes = secrets.token_bytes(32)
1085
- challenge_b64 = base64.b64encode(challenge_bytes).decode()
1086
-
1087
- # Store challenge temporarily (expires in 60 seconds)
1088
- challenge_key = f"{room_id}_{identifier}"
1089
- CHALLENGE_STORE[challenge_key] = {
1090
- "challenge": challenge_b64,
1091
- "created_at": time.time(),
1092
- "role": role
1093
- }
1094
-
1095
- print(f"✅ Generated challenge")
1096
- print(f" - Challenge (base64): {challenge_b64}")
1097
- print(f" - Challenge length: {len(challenge_b64)} chars")
1098
- print(f" - Challenge bytes: {len(challenge_bytes)} bytes")
1099
- print(f"{'='*60}\n")
1100
-
1101
- emit('challenge_response', {"challenge": challenge_b64})
1102
-
1103
- except Exception as e:
1104
- print(f"❌ Error generating challenge: {e}")
1105
- import traceback
1106
- traceback.print_exc()
1107
- emit('challenge_error', {"error": str(e)})
1108
-
1109
-
1110
- @socketio.on('verify_signature')
1111
- def handle_verify_signature(data):
1112
- """Verify ECDSA signature and authenticate - via WebSocket"""
1113
- try:
1114
- room_id = data.get("room_id")
1115
- role = data.get("role")
1116
- identifier = data.get("identifier")
1117
- signature_b64 = data.get("signature")
1118
-
1119
- print(f"\n{'='*60}")
1120
- print(f"🔐 SIGNATURE VERIFICATION")
1121
- print(f"{'='*60}")
1122
- print(f"Room: {room_id[:8] if room_id else 'None'}...")
1123
- print(f"Role: {role}")
1124
- print(f"Identifier: {identifier[:8] if identifier else 'None'}...")
1125
- print(f"Signature (first 30): {signature_b64[:30] if signature_b64 else 'None'}...")
1126
- print(f"Signature (full): {signature_b64}")
1127
-
1128
- if not room_id or not role or not identifier or not signature_b64:
1129
- emit('verification_error', {"error": "Missing required fields"})
1130
- return
1131
-
1132
- if room_id not in CHAT_ROOMS:
1133
- emit('verification_error', {"error": "Chat room not found"})
1134
- return
1135
-
1136
- room = CHAT_ROOMS[room_id]
1137
-
1138
- # Check if expired
1139
- if time.time() > room["expires_at"]:
1140
- emit('verification_error', {"error": "Chat room has expired"})
1141
- return
1142
-
1143
- # Retrieve challenge
1144
- challenge_key = f"{room_id}_{identifier}"
1145
- print(f"Looking for challenge key: {challenge_key[:50]}...")
1146
-
1147
- if challenge_key not in CHALLENGE_STORE:
1148
- print(f"❌ Challenge not found!")
1149
- emit('verification_error', {"error": "No challenge found or challenge expired"})
1150
- return
1151
-
1152
- challenge_data = CHALLENGE_STORE[challenge_key]
1153
-
1154
- # Check challenge expiry (60 seconds)
1155
- if time.time() - challenge_data["created_at"] > 60:
1156
- del CHALLENGE_STORE[challenge_key]
1157
- emit('verification_error', {"error": "Challenge expired"})
1158
- return
1159
-
1160
- challenge = challenge_data["challenge"]
1161
- print(f"✅ Found challenge: {challenge[:30]}...")
1162
-
1163
- # Get ECDSA public key based on role
1164
- ecdsa_public_key_b64 = None
1165
- receiver_number = None
1166
-
1167
- if role == "admin":
1168
- if identifier != room["admin_session"]:
1169
- emit('verification_error', {"error": "Invalid admin session"})
1170
- return
1171
- ecdsa_public_key_b64 = room["admin_ecdsa_public_key"]
1172
- print(f"✅ Using admin ECDSA public key")
1173
- print(f" Public key (first 50): {ecdsa_public_key_b64[:50]}...")
1174
-
1175
- elif role == "receiver":
1176
- token_found = False
1177
- for token in room["receiver_tokens"]:
1178
- if token["token"] == identifier:
1179
- if not token["claimed"]:
1180
- emit('verification_error', {"error": "Token not claimed"})
1181
- return
1182
- ecdsa_public_key_b64 = token["ecdsa_public_key"]
1183
- receiver_number = token["receiver_number"]
1184
- # ✅ ADD THIS DETAILED LOGGING:
1185
- print(f"\n✅ Found receiver token for verification")
1186
- print(f"Token data:")
1187
- print(f" - Receiver #: {receiver_number}")
1188
- print(f" - ECDSA key exists: {ecdsa_public_key_b64 is not None}")
1189
- if ecdsa_public_key_b64:
1190
- print(f" - ECDSA key length: {len(ecdsa_public_key_b64)}")
1191
- print(f" - ECDSA key (first 50): {ecdsa_public_key_b64[:50]}")
1192
- print(f" - ECDSA key (last 20): {ecdsa_public_key_b64[-20:]}")
1193
- print(f" - Full ECDSA key: {ecdsa_public_key_b64}")
1194
- token_found = True
1195
- break
1196
-
1197
- if not token_found:
1198
- emit('verification_error', {"error": "Invalid receiver token"})
1199
- return
1200
 
1201
- if not ecdsa_public_key_b64:
1202
- emit('verification_error', {"error": "No ECDSA public key found"})
1203
- return
1204
- else:
1205
- emit('verification_error', {"error": "Invalid role"})
1206
- return
1207
-
1208
- # Verify ECDSA signature
1209
- print(f"🔐 Calling verify_ecdsa_signature...")
1210
- print(f" Challenge: {challenge}")
1211
- print(f" Signature: {signature_b64}")
1212
-
1213
- is_valid = verify_ecdsa_signature(ecdsa_public_key_b64, challenge, signature_b64)
1214
-
1215
- # Delete challenge (prevent replay)
1216
- del CHALLENGE_STORE[challenge_key]
1217
-
1218
- if not is_valid:
1219
- print(f"❌ Signature verification FAILED")
1220
- emit('verification_error', {"error": "Invalid signature"})
1221
- return
1222
-
1223
- print(f"✅ Signature verified successfully!")
1224
-
1225
- # Generate session
1226
- session_id = str(uuid.uuid4())
1227
-
1228
- emit('verification_success', {
1229
  "session_id": session_id,
1230
  "role": role,
1231
  "receiver_number": receiver_number,
@@ -1234,48 +676,6 @@ def handle_verify_signature(data):
1234
  })
1235
 
1236
  except Exception as e:
1237
- print(f"❌ Error verifying signature: {e}")
1238
- import traceback
1239
- traceback.print_exc()
1240
- emit('verification_error', {"error": str(e)})
1241
-
1242
- @app.route("/api/chat/delete/<room_id>", methods=["DELETE"])
1243
- def delete_chat_room(room_id):
1244
- """Admin manually deletes a chat room"""
1245
- try:
1246
- # Get admin session from Authorization header
1247
- auth_header = request.headers.get('Authorization', '')
1248
- admin_session = auth_header.replace('Bearer ', '')
1249
-
1250
- if not admin_session:
1251
- return jsonify({"error": "Admin session required"}), 401
1252
-
1253
- if room_id not in CHAT_ROOMS:
1254
- return jsonify({"error": "Chat room not found"}), 404
1255
-
1256
- room = CHAT_ROOMS[room_id]
1257
-
1258
- # Verify admin session
1259
- if room["admin_session"] != admin_session:
1260
- return jsonify({"error": "Unauthorized - not admin"}), 403
1261
-
1262
- # Notify all participants that room is closing
1263
- socketio.emit('room_closing', {
1264
- 'room_id': room_id,
1265
- 'reason': 'Admin deleted the room'
1266
- }, room=room_id)
1267
-
1268
- # Delete room
1269
- del CHAT_ROOMS[room_id]
1270
-
1271
- print(f"🗑️ Admin deleted room: {room_id[:8]}...")
1272
-
1273
- return jsonify({"message": "Room deleted successfully"})
1274
-
1275
- except Exception as e:
1276
- print(f"❌ Error deleting chat room: {e}")
1277
- import traceback
1278
- traceback.print_exc()
1279
  return jsonify({"error": str(e)}), 500
1280
 
1281
  # ADD WEBSOCKET EVENTS:
@@ -1284,7 +684,7 @@ def handle_join_chat(data):
1284
  room_id = data['room_id']
1285
  session_id = data['session_id']
1286
  role = data['role']
1287
- rsa_public_key = data.get('public_key')
1288
 
1289
  if room_id not in CHAT_ROOMS:
1290
  return
@@ -1295,8 +695,7 @@ def handle_join_chat(data):
1295
  CHAT_ROOMS[room_id]["active_sessions"][session_id] = {
1296
  "role": role,
1297
  "receiver_number": data.get('receiver_number'),
1298
- "rsa_public_key": rsa_public_key,
1299
- "socket_id": request.sid, # NEW: Store socket ID
1300
  "joined_at": time.time(),
1301
  "last_seen": time.time()
1302
  }
@@ -1309,7 +708,7 @@ def handle_join_chat(data):
1309
  'session_id': sid,
1310
  'role': session['role'],
1311
  'receiver_number': session.get('receiver_number'),
1312
- 'public_key': session.get('rsa_public_key')
1313
  })
1314
 
1315
  emit('peer_list', {'peers': peer_list})
@@ -1319,7 +718,7 @@ def handle_join_chat(data):
1319
  'session_id': session_id,
1320
  'role': role,
1321
  'receiver_number': data.get('receiver_number'),
1322
- 'public_key': rsa_public_key,
1323
  'active_count': len(CHAT_ROOMS[room_id]["active_sessions"])
1324
  }, room=room_id, include_self=False) # ← CRITICAL: include_self=False
1325
 
@@ -1407,35 +806,24 @@ def handle_leave_chat(data):
1407
  room_id = data['room_id']
1408
  session_id = data['session_id']
1409
 
1410
- if room_id not in CHAT_ROOMS:
1411
- print(f"⚠️ Room {room_id[:8]}... already deleted")
1412
- return
1413
-
1414
- if session_id not in CHAT_ROOMS[room_id]["active_sessions"]:
1415
- print(f"⚠️ Session {session_id[:8]}... not in room")
1416
- return
1417
-
1418
- session = CHAT_ROOMS[room_id]["active_sessions"][session_id]
1419
-
1420
- # If admin left and burn_on_admin_exit is true
1421
- if session["role"] == "admin" and CHAT_ROOMS[room_id]["settings"]["burn_on_admin_exit"]:
1422
- emit('room_closing', {'reason': 'Admin left the room'}, room=room_id)
1423
- del CHAT_ROOMS[room_id] # ✅ Delete entire room
1424
- leave_room(room_id)
1425
- print(f"🔥 Room {room_id[:8]}... burned (admin left)")
1426
- else:
1427
- # Emit BEFORE deleting
1428
- emit('user_left', {
1429
- 'session_id': session_id,
1430
- 'role': session["role"],
1431
- 'receiver_number': session.get("receiver_number"),
1432
- 'active_count': len(CHAT_ROOMS[room_id]["active_sessions"]) - 1
1433
- }, room=room_id)
1434
-
1435
- # Delete session
1436
  del CHAT_ROOMS[room_id]["active_sessions"][session_id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1437
  leave_room(room_id)
1438
- print(f"👋 Session {session_id[:8]}... left room {room_id[:8]}...")
1439
 
1440
  @app.route("/api/auth/signup", methods=["POST"])
1441
  def api_signup():
@@ -1939,30 +1327,26 @@ def handle_disconnect():
1939
  print(f"👤 User disconnected: {username_hash[:8]}...")
1940
 
1941
  # Remove from chat rooms
1942
- # Remove from chat rooms and notify others
1943
  for room_id, room in list(CHAT_ROOMS.items()):
1944
- for session_id, session in list(room['active_sessions'].items()):
1945
- # Check if this session matches the disconnected socket
1946
- if session.get('socket_id') == request.sid:
1947
- print(f"[Disconnect] User {session_id} disconnected from room {room_id}")
1948
-
1949
- # CRITICAL: Notify BEFORE deleting
1950
- socketio.emit('user_left', {
1951
- 'session_id': session_id,
1952
- 'role': session['role'],
1953
- 'receiver_number': session.get('receiver_number'),
1954
- 'active_count': len(room['active_sessions']) - 1
1955
- }, room=room_id)
1956
-
1957
- # If admin disconnected, close room
1958
- if session['role'] == 'admin' and room['settings'].get('burn_on_admin_exit', True):
1959
- print(f"[Disconnect] Admin disconnected, closing room {room_id}")
1960
- socketio.emit('room_closing', {'reason': 'Admin disconnected'}, room=room_id)
1961
- del CHAT_ROOMS[room_id]
1962
- else:
1963
- del room['active_sessions'][session_id]
1964
-
1965
- break
1966
 
1967
  # Remove from screen share rooms
1968
  for room_id, room in list(SCREEN_SHARE_ROOMS.items()):
@@ -2170,38 +1554,6 @@ def periodic_cleanup():
2170
 
2171
  try:
2172
  now = time.time()
2173
-
2174
- # ADD THIS: Cleanup old nonces
2175
- cleanup_old_nonces(now)
2176
-
2177
- # ADD THIS: Cleanup old session requests
2178
- for uuid in list(SESSION_REQUESTS.keys()):
2179
- requests = [ts for ts in SESSION_REQUESTS[uuid] if now - ts < 300]
2180
- if requests:
2181
- SESSION_REQUESTS[uuid] = requests
2182
- else:
2183
- del SESSION_REQUESTS[uuid]
2184
-
2185
- # 0. Cleanup old challenges
2186
- cleanup_old_challenges()
2187
-
2188
- # 1. Cleanup expired chat rooms
2189
- expired_chat_rooms = []
2190
- for room_id, room in list(CHAT_ROOMS.items()):
2191
- if now > room['expires_at']:
2192
- expired_chat_rooms.append(room_id)
2193
-
2194
- # Notify participants
2195
- socketio.emit('room_closing', {
2196
- 'room_id': room_id,
2197
- 'reason': 'Room expired'
2198
- }, room=room_id)
2199
-
2200
- # Delete room
2201
- del CHAT_ROOMS[room_id]
2202
-
2203
- if expired_chat_rooms:
2204
- print(f"[Cleanup] Expired {len(expired_chat_rooms)} chat room(s)")
2205
 
2206
  # 1. Cleanup expired screen share rooms
2207
  expired_rooms = []
 
1
  import sys
2
  import os
3
+
4
  from flask import Flask, request, jsonify, send_file
5
  from flask_cors import CORS
 
 
 
6
  from werkzeug.utils import secure_filename
7
  import tempfile
8
  import uuid
 
21
  from database import init_supabase, get_supabase
22
  from auth import signup_user, login_user, check_username_exists, verify_token
23
  from functools import wraps
 
 
 
 
 
 
 
24
 
25
  app = Flask(__name__)
26
  CORS(app)
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  print("Initializing Supabase...")
29
  init_supabase()
30
 
 
47
  transports=['websocket', 'polling'] # Explicitly allow both
48
  )
49
 
50
+
51
+
52
+
53
  # In-memory storage
54
  SECRETS = {} # { id: { data, file_data, file_type, expire_at, view_once, theme, analytics, etc. } }
55
  SHORT_LINKS = {} # { short_id: full_id }
 
58
  CURSOR_STATES = {}
59
  CHAT_ROOMS = {}
60
 
 
 
 
 
 
61
  # Configuration
62
  MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
63
  ALLOWED_EXTENSIONS = {
 
70
  ONLINE_USERS = {}
71
  FILE_TRANSFERS = {}
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def assign_cursor_color(existing_participants):
74
  """Assign unique cursor color to new participant"""
75
  import random
 
146
  'timezone': 'Unknown'
147
  }
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  def generate_qr_code(data):
150
  """Generate QR code for the given data"""
151
  qr = qrcode.QRCode(
 
239
  try:
240
  form = request.form
241
  data = form.get("data")
 
 
 
242
 
 
243
  if not data:
244
  return jsonify({"error": "Data is required"}), 400
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  # Parse parameters
247
  ttl = int(form.get("ttl", 300))
248
  view_once = form.get("view_once", "false").lower() == "true"
 
325
  def fetch(secret_id):
326
  """Fetch and decrypt secret with analytics - MODIFIED TO HANDLE verify_only"""
327
  try:
 
 
 
 
 
 
 
 
328
  # Check if it's a short link
329
  if secret_id in SHORT_LINKS:
330
  secret_id = SHORT_LINKS[secret_id]
 
559
  except Exception as e:
560
  return jsonify({"error": str(e)}), 500
561
 
 
 
 
 
 
 
 
 
 
 
562
  @app.route("/api/cleanup", methods=["POST"])
563
  def cleanup_expired():
564
  """Clean up expired secrets"""
 
601
  max_receivers = int(form.get("max_receivers", 5))
602
  password = form.get("password", "")
603
  allow_files = form.get("allow_files", "true").lower() == "true"
 
 
 
 
604
 
605
  room_id = str(uuid.uuid4())
606
+ admin_session = str(uuid.uuid4())
 
 
 
 
 
 
 
 
 
 
 
 
 
607
 
608
  CHAT_ROOMS[room_id] = {
609
  "admin_session": admin_session,
 
610
  "created_at": time.time(),
611
+ "expires_at": time.time() + ttl,
612
  "settings": {
613
  "max_receivers": max_receivers,
614
  "password": password,
615
  "allow_files": allow_files,
616
  "burn_on_admin_exit": True
617
  },
 
618
  "active_sessions": {},
619
  "receiver_counter": 0
620
  }
621
 
 
 
622
 
623
+
624
+ # Return only IDs - let frontend create URLs
625
  return jsonify({
626
  "room_id": room_id,
627
  "admin_session": admin_session,
628
+ "expires_at": CHAT_ROOMS[room_id]["expires_at"]
 
629
  })
630
 
631
  except Exception as e:
 
 
 
632
  return jsonify({"error": str(e)}), 500
633
 
634
  @app.route("/api/chat/join/<room_id>")
635
  def join_chat_room(room_id):
 
636
  try:
637
  password = request.args.get("password", "")
638
  admin_session = request.args.get("admin", "")
 
 
639
 
640
  if room_id not in CHAT_ROOMS:
641
  return jsonify({"error": "Chat room not found"}), 404
 
649
  # Check password
650
  if room["settings"]["password"] and password != room["settings"]["password"]:
651
  return jsonify({"error": "Wrong password"}), 403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
+ # FIXED: Proper role assignment
654
+ if admin_session and admin_session == room["admin_session"]:
655
+ # Only admin if the session matches the room's admin session
656
+ role = "admin"
657
+ session_id = admin_session
658
+ receiver_number = None
659
  else:
660
+ # Everyone else is a receiver
661
+ active_receivers = sum(1 for s in room["active_sessions"].values() if s["role"] == "receiver")
662
+ if active_receivers >= room["settings"]["max_receivers"]:
663
+ return jsonify({"error": "Chat room is full"}), 403
664
+
665
+ role = "receiver"
666
+ session_id = str(uuid.uuid4())
667
+ room["receiver_counter"] += 1
668
+ receiver_number = room["receiver_counter"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
 
670
+ return jsonify({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  "session_id": session_id,
672
  "role": role,
673
  "receiver_number": receiver_number,
 
676
  })
677
 
678
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  return jsonify({"error": str(e)}), 500
680
 
681
  # ADD WEBSOCKET EVENTS:
 
684
  room_id = data['room_id']
685
  session_id = data['session_id']
686
  role = data['role']
687
+ public_key = data.get('public_key')
688
 
689
  if room_id not in CHAT_ROOMS:
690
  return
 
695
  CHAT_ROOMS[room_id]["active_sessions"][session_id] = {
696
  "role": role,
697
  "receiver_number": data.get('receiver_number'),
698
+ "public_key": public_key,
 
699
  "joined_at": time.time(),
700
  "last_seen": time.time()
701
  }
 
708
  'session_id': sid,
709
  'role': session['role'],
710
  'receiver_number': session.get('receiver_number'),
711
+ 'public_key': session.get('public_key')
712
  })
713
 
714
  emit('peer_list', {'peers': peer_list})
 
718
  'session_id': session_id,
719
  'role': role,
720
  'receiver_number': data.get('receiver_number'),
721
+ 'public_key': public_key,
722
  'active_count': len(CHAT_ROOMS[room_id]["active_sessions"])
723
  }, room=room_id, include_self=False) # ← CRITICAL: include_self=False
724
 
 
806
  room_id = data['room_id']
807
  session_id = data['session_id']
808
 
809
+ if room_id in CHAT_ROOMS and session_id in CHAT_ROOMS[room_id]["active_sessions"]:
810
+ session = CHAT_ROOMS[room_id]["active_sessions"][session_id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  del CHAT_ROOMS[room_id]["active_sessions"][session_id]
812
+
813
+ # If admin left and burn_on_admin_exit is true
814
+ if session["role"] == "admin" and CHAT_ROOMS[room_id]["settings"]["burn_on_admin_exit"]:
815
+ emit('room_closing', {'reason': 'Admin left the room'}, room=room_id)
816
+ del CHAT_ROOMS[room_id]
817
+ if room_id in CHAT_MESSAGES:
818
+ del CHAT_MESSAGES[room_id]
819
+ else:
820
+ emit('user_left', {
821
+ 'role': session["role"],
822
+ 'receiver_number': session.get("receiver_number"),
823
+ 'active_count': len(CHAT_ROOMS[room_id]["active_sessions"])
824
+ }, room=room_id)
825
+
826
  leave_room(room_id)
 
827
 
828
  @app.route("/api/auth/signup", methods=["POST"])
829
  def api_signup():
 
1327
  print(f"👤 User disconnected: {username_hash[:8]}...")
1328
 
1329
  # Remove from chat rooms
 
1330
  for room_id, room in list(CHAT_ROOMS.items()):
1331
+ if username_hash in [s for s_id, s in room['active_sessions'].items()]:
1332
+ # Find and remove session
1333
+ for session_id, session in list(room['active_sessions'].items()):
1334
+ if session.get('username_hash') == username_hash or \
1335
+ (session['role'] == 'admin' and room['admin_session'] == session_id):
1336
+
1337
+ # If admin left, close room
1338
+ if session['role'] == 'admin' and room['settings'].get('burn_on_admin_exit', True):
1339
+ socketio.emit('room_closing', {'reason': 'Admin disconnected'}, room=room_id)
1340
+ if room_id in CHAT_MESSAGES:
1341
+ del CHAT_MESSAGES[room_id]
1342
+ del CHAT_ROOMS[room_id]
1343
+ else:
1344
+ del room['active_sessions'][session_id]
1345
+ socketio.emit('user_left', {
1346
+ 'role': session['role'],
1347
+ 'receiver_number': session.get('receiver_number'),
1348
+ 'active_count': len(room['active_sessions'])
1349
+ }, room=room_id)
 
 
 
1350
 
1351
  # Remove from screen share rooms
1352
  for room_id, room in list(SCREEN_SHARE_ROOMS.items()):
 
1554
 
1555
  try:
1556
  now = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1557
 
1558
  # 1. Cleanup expired screen share rooms
1559
  expired_rooms = []