client
This commit is contained in:
		
							parent
							
								
									9d70a80337
								
							
						
					
					
						commit
						b18969362d
					
				
					 2 changed files with 296 additions and 433 deletions
				
			
		|  | @ -1,47 +1,317 @@ | |||
| import os | ||||
| import json | ||||
| import socket | ||||
| import base64 | ||||
| import hashlib | ||||
| from datetime import datetime | ||||
| import uuid | ||||
| from datetime import datetime, timezone | ||||
| from pathlib import Path | ||||
| 
 | ||||
| from Crypto.PublicKey import RSA, ElGamal | ||||
| from Crypto.Random import get_random_bytes | ||||
| from Crypto.Util.number import inverse, GCD | ||||
| from Crypto.Cipher import AES, PKCS1_OAEP | ||||
| from Crypto.Cipher import PKCS1_OAEP, AES | ||||
| from Crypto.Hash import MD5 | ||||
| from Crypto.Random import get_random_bytes, random | ||||
| from Crypto.Util.number import GCD, inverse | ||||
| from phe import paillier | ||||
| 
 | ||||
| # Configuration | ||||
| SERVER_HOST = "127.0.0.1" | ||||
| SERVER_PORT = 5000 | ||||
| CLIENT_STATE_FILE = "client_state.json" | ||||
| INPUT_DIR = "inputdata" | ||||
| KEYS_DIR = "client_keys" | ||||
| 
 | ||||
| server_info = {}  # filled by get_public_info | ||||
| cached_paillier_pub = None | ||||
| 
 | ||||
| def ensure_dirs(): | ||||
|     os.makedirs(INPUT_DIR, exist_ok=True) | ||||
|     os.makedirs(KEYS_DIR, exist_ok=True) | ||||
|     Path(INPUT_DIR).mkdir(exist_ok=True) | ||||
| 
 | ||||
| def b64e(b: bytes) -> str: | ||||
|     return base64.b64encode(b).decode() | ||||
| 
 | ||||
| def b64d(s: str) -> bytes: | ||||
|     return base64.b64decode(s.encode()) | ||||
| def load_client_state(): | ||||
|     if not os.path.exists(CLIENT_STATE_FILE): | ||||
|         return {"doctor_id": None, "elgamal": {}, "server_keys": {}} | ||||
|     with open(CLIENT_STATE_FILE, "r") as f: | ||||
|         return json.load(f) | ||||
| 
 | ||||
| 
 | ||||
| def save_client_state(state): | ||||
|     with open(CLIENT_STATE_FILE, "w") as f: | ||||
|         json.dump(state, f, indent=2) | ||||
| 
 | ||||
| 
 | ||||
| def send_request(action, role, body): | ||||
|     """Send JSON request to server and receive response.""" | ||||
|     req = {"action": action, "role": role, "body": body} | ||||
|     with socket.create_connection((SERVER_HOST, SERVER_PORT), timeout=5) as s: | ||||
|         s.sendall((json.dumps(req) + "\n").encode()) | ||||
|         data = s.recv(1024*1024) | ||||
|     resp = json.loads(data.decode()) | ||||
|     if resp.get("status") != "ok": | ||||
|         print("server error:", resp.get("error")) | ||||
|     try: | ||||
|         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||
|         sock.connect((SERVER_HOST, SERVER_PORT)) | ||||
|         sock.sendall((json.dumps(req) + "\n").encode()) | ||||
|         data = sock.recv(4096).decode() | ||||
|         sock.close() | ||||
|         return json.loads(data) | ||||
|     except Exception as e: | ||||
|         return {"status": "error", "error": f"Connection failed: {e}"} | ||||
| 
 | ||||
| 
 | ||||
| def b64e(b: bytes) -> str: | ||||
|     import base64 | ||||
| 
 | ||||
|     return base64.b64encode(b).decode() | ||||
| 
 | ||||
| 
 | ||||
| def b64d(s: str) -> bytes: | ||||
|     import base64 | ||||
| 
 | ||||
|     return base64.b64decode(s.encode()) | ||||
| 
 | ||||
| 
 | ||||
| def fetch_server_keys(state): | ||||
|     """Get server's public keys.""" | ||||
|     resp = send_request("get_public_info", "doctor", {}) | ||||
|     if resp.get("status") == "ok": | ||||
|         state["server_keys"] = resp.get("data", {}) | ||||
|         save_client_state(state) | ||||
|         print("Server keys fetched.") | ||||
|         return True | ||||
|     else: | ||||
|         print(f"Failed to fetch server keys: {resp.get('error')}") | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| def register_doctor_client(state): | ||||
|     """Register a new doctor with the server.""" | ||||
|     print("\n=== Doctor Registration ===") | ||||
|     doctor_id = input("Choose doctor ID (alphanumeric): ").strip() | ||||
|     if not doctor_id.isalnum(): | ||||
|         print("Invalid doctor ID.") | ||||
|         return | ||||
| 
 | ||||
|     name = input("Doctor name: ").strip() | ||||
|     department = input("Department: ").strip() | ||||
| 
 | ||||
|     if not state["server_keys"]: | ||||
|         print("Fetch server keys first.") | ||||
|         return | ||||
| 
 | ||||
|     # Generate ElGamal keypair | ||||
|     print("Generating ElGamal keypair...") | ||||
|     eg_key = ElGamal.generate(512, get_random_bytes) | ||||
|     p = int(eg_key.p) | ||||
|     g = int(eg_key.g) | ||||
|     y = int(eg_key.y) | ||||
|     x = int(eg_key.x) | ||||
| 
 | ||||
|     state["doctor_id"] = doctor_id | ||||
|     state["elgamal"] = {"p": p, "g": g, "y": y, "x": x} | ||||
| 
 | ||||
|     # Encrypt department using Paillier | ||||
|     paillier_n = int(state["server_keys"]["paillier_n"]) | ||||
|     paillier_pub = paillier.PaillierPublicKey(paillier_n) | ||||
| 
 | ||||
|     dept_hash = int.from_bytes(hashlib.md5(department.encode()).digest(), "big") | ||||
|     dept_enc = paillier_pub.encrypt(dept_hash) | ||||
| 
 | ||||
|     # Prepare request | ||||
|     body = { | ||||
|         "doctor_id": doctor_id, | ||||
|         "department_plain": department, | ||||
|         "dept_enc": { | ||||
|             "ciphertext": int(dept_enc.ciphertext()), | ||||
|             "exponent": dept_enc.exponent, | ||||
|         }, | ||||
|         "elgamal_pub": {"p": p, "g": g, "y": y}, | ||||
|     } | ||||
| 
 | ||||
|     resp = send_request("register_doctor", "doctor", body) | ||||
|     if resp.get("status") == "ok": | ||||
|         save_client_state(state) | ||||
|         print(f"✓ Doctor '{doctor_id}' registered successfully.") | ||||
|         print(f"  Name: {name}, Department: {department}") | ||||
|     else: | ||||
|         print(f"✗ Registration failed: {resp.get('error')}") | ||||
| 
 | ||||
| 
 | ||||
| def elgamal_sign(eg_private, msg_bytes): | ||||
|     """Sign message with ElGamal.""" | ||||
|     p = int(eg_private["p"]) | ||||
|     g = int(eg_private["g"]) | ||||
|     x = int(eg_private["x"]) | ||||
| 
 | ||||
|     H = int.from_bytes(MD5.new(msg_bytes).digest(), "big") % (p - 1) | ||||
|     while True: | ||||
|         k = random.randint(2, p - 2) | ||||
|         if GCD(k, p - 1) == 1: | ||||
|             break | ||||
| 
 | ||||
|     r = pow(g, k, p) | ||||
|     kinv = inverse(k, p - 1) | ||||
|     s = (kinv * (H - x * r)) % (p - 1) | ||||
|     return int(r), int(s) | ||||
| 
 | ||||
| 
 | ||||
| def submit_report(state): | ||||
|     """Submit a medical report (encrypted with AES, key encrypted with RSA-OAEP).""" | ||||
|     if not state["doctor_id"]: | ||||
|         print("Register as doctor first.") | ||||
|         return | ||||
| 
 | ||||
|     ensure_dirs() | ||||
|     files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith(".md")] | ||||
|     if not files: | ||||
|         print("Place markdown files in inputdata/") | ||||
|         return | ||||
| 
 | ||||
|     print("\nAvailable files:") | ||||
|     for i, f in enumerate(files, 1): | ||||
|         print(f"  {i}. {f}") | ||||
| 
 | ||||
|     try: | ||||
|         idx = int(input("Select file #: ").strip()) - 1 | ||||
|         filename = files[idx] | ||||
|     except (ValueError, IndexError): | ||||
|         print("Invalid selection.") | ||||
|         return | ||||
| 
 | ||||
|     filepath = os.path.join(INPUT_DIR, filename) | ||||
|     with open(filepath, "rb") as f: | ||||
|         report_bytes = f.read() | ||||
| 
 | ||||
|     timestamp = datetime.now(timezone.utc).isoformat() | ||||
|     md5_hex = hashlib.md5(report_bytes).hexdigest() | ||||
| 
 | ||||
|     # Sign report | ||||
|     msg_to_sign = report_bytes + timestamp.encode() | ||||
|     r, s = elgamal_sign(state["elgamal"], msg_to_sign) | ||||
| 
 | ||||
|     # Encrypt report with AES-256-EAX | ||||
|     aes_key = get_random_bytes(32) | ||||
|     cipher = AES.new(aes_key, AES.MODE_EAX) | ||||
|     ciphertext, tag = cipher.encrypt_and_digest(report_bytes) | ||||
| 
 | ||||
|     # Encrypt AES key with RSA-OAEP | ||||
|     rsa_pub_pem = state["server_keys"]["rsa_pub_pem_b64"] | ||||
|     rsa_pub = RSA.import_key(b64d(rsa_pub_pem)) | ||||
|     rsa_cipher = PKCS1_OAEP.new(rsa_pub) | ||||
|     encrypted_aes_key = rsa_cipher.encrypt(aes_key) | ||||
| 
 | ||||
|     # Prepare request | ||||
|     body = { | ||||
|         "doctor_id": state["doctor_id"], | ||||
|         "filename": filename, | ||||
|         "timestamp": timestamp, | ||||
|         "md5_hex": md5_hex, | ||||
|         "sig": {"r": r, "s": s}, | ||||
|         "aes": { | ||||
|             "key_rsa_oaep_b64": b64e(encrypted_aes_key), | ||||
|             "nonce_b64": b64e(cipher.nonce), | ||||
|             "tag_b64": b64e(tag), | ||||
|             "ct_b64": b64e(ciphertext), | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
|     resp = send_request("upload_report", "doctor", body) | ||||
|     if resp.get("status") == "ok": | ||||
|         print(f"✓ Report '{filename}' uploaded successfully.") | ||||
|         print(f"  MD5: {md5_hex}") | ||||
|         print(f"  Timestamp: {timestamp}") | ||||
|     else: | ||||
|         print(f"✗ Upload failed: {resp.get('error')}") | ||||
| 
 | ||||
| 
 | ||||
| def homo_rsa_encrypt_amount(state, amount): | ||||
|     """Encrypt amount using homomorphic RSA.""" | ||||
|     if amount < 0 or amount > 100000: | ||||
|         print("Amount must be 0-100000.") | ||||
|         return None | ||||
|     return resp.get("data") if "data" in resp else {} | ||||
| 
 | ||||
| def get_public_info(): | ||||
|     global server_info, cached_paillier_pub | ||||
|     data = send_request("get result:") | ||||
|     if data is None: | ||||
|     n = int(state["server_keys"]["rsa_n"]) | ||||
|     e = int(state["server_keys"]["rsa_e"]) | ||||
|     g = int(state["server_keys"]["rsa_homo_g"]) | ||||
| 
 | ||||
| def rs | ||||
|     # Encrypt: c = g^amount)^e mod n | ||||
|     m = pow(g, amount, n) | ||||
|     c = pow(m, e, n) | ||||
|     return int(c) | ||||
| 
 | ||||
| 
 | ||||
| def submit_expense(state): | ||||
|     """Submit an encrypted expense.""" | ||||
|     if not state["doctor_id"]: | ||||
|         print("Register as doctor first.") | ||||
|         return | ||||
| 
 | ||||
|     if not state["server_keys"]: | ||||
|         print("Fetch server keys first.") | ||||
|         return | ||||
| 
 | ||||
|     try: | ||||
|         amount = int(input("Expense amount (integer, 0-100000): ").strip()) | ||||
|     except ValueError: | ||||
|         print("Invalid amount.") | ||||
|         return | ||||
| 
 | ||||
|     ciphertext = homo_rsa_encrypt_amount(state, amount) | ||||
|     if ciphertext is None: | ||||
|         return | ||||
| 
 | ||||
|     body = {"doctor_id": state["doctor_id"], "amount_ciphertext": str(ciphertext)} | ||||
| 
 | ||||
|     resp = send_request("submit_expense", "doctor", body) | ||||
|     if resp.get("status") == "ok": | ||||
|         print(f"✓ Expense encrypted and submitted.") | ||||
|         print(f"  Amount: {amount}") | ||||
|         print(f"  Ciphertext: {ciphertext}") | ||||
|     else: | ||||
|         print(f"✗ Submission failed: {resp.get('error')}") | ||||
| 
 | ||||
| 
 | ||||
| def doctor_menu(state): | ||||
|     """Doctor submenu.""" | ||||
|     while True: | ||||
|         print("\n=== Doctor Menu ===") | ||||
|         print("1. Register with server") | ||||
|         print("2. Fetch server keys") | ||||
|         print("3. Submit report (encrypted)") | ||||
|         print("4. Submit expense (homomorphic RSA)") | ||||
|         print("5. Show current doctor ID") | ||||
|         print("0. Back") | ||||
| 
 | ||||
|         ch = input("Choice: ").strip() | ||||
|         if ch == "1": | ||||
|             register_doctor_client(state) | ||||
|         elif ch == "2": | ||||
|             fetch_server_keys(state) | ||||
|         elif ch == "3": | ||||
|             submit_report(state) | ||||
|         elif ch == "4": | ||||
|             submit_expense(state) | ||||
|         elif ch == "5": | ||||
|             doc_id = state.get("doctor_id") | ||||
|             if doc_id: | ||||
|                 print(f"Current doctor ID: {doc_id}") | ||||
|             else: | ||||
|                 print("Not registered.") | ||||
|         elif ch == "0": | ||||
|             break | ||||
|         else: | ||||
|             print("Invalid choice.") | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     ensure_dirs() | ||||
|     state = load_client_state() | ||||
| 
 | ||||
|     while True: | ||||
|         print("\n=== Medical Records Client ===") | ||||
|         print("1. Doctor operations") | ||||
|         print("0. Exit") | ||||
| 
 | ||||
|         ch = input("Choice: ").strip() | ||||
|         if ch == "1": | ||||
|             doctor_menu(state) | ||||
|         elif ch == "0": | ||||
|             print("Goodbye!") | ||||
|             break | ||||
|         else: | ||||
|             print("Invalid choice.") | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
|  |  | |||
|  | @ -1,407 +0,0 @@ | |||
| import os | ||||
| import json | ||||
| import threading | ||||
| import socketserver | ||||
| import base64 | ||||
| import time | ||||
| from datetime import datetime, timezone | ||||
| from Crypto.PublicKey import RSA | ||||
| from Crypto.Cipher import PKCS1_OAEP, AES | ||||
| from Crypto.Random import get_random_bytes | ||||
| from Crypto.Util.number import GCD | ||||
| from phe import paillier | ||||
| 
 | ||||
| DATA_DIR = server_data | ||||
| DOCTORS_FILE = os.path.join(DATA_DIR, doctors.json) | ||||
| EXPENSES_FILE = os.path.join(DATA_DIR, expenses.json) | ||||
| REPORTS_FILE = os.path.join(DATA_DIR, reports.json) | ||||
| CONF_FILE = os.path.join(DATA_DIR, config.json) | ||||
| RSA_PRIV_FILE = os.path.join(DATA_DIR, server_rsa_priv.pem) | ||||
| RSA_PUB_FILE = os.path.join(DATA_DIR, server_rsa_pub.pem) | ||||
| PORT = 5000 | ||||
| 
 | ||||
| lock = threading.Lock() | ||||
| 
 | ||||
| def ensure_dirs(): | ||||
|     os.makedirs(DATA_DIR, exist_ok=True) | ||||
| 
 | ||||
| def read_json(path, default): | ||||
|     if not os.path.exists(path): | ||||
|         return default | ||||
|     with open(path, r) as f: | ||||
|         return json.load(f) | ||||
| 
 | ||||
| def write_json(path, obj): | ||||
|     tmp = path + .tmp | ||||
|     with open(tmp, w) as f: | ||||
|         json.dump(obj, f, indent=2) | ||||
|     os.replace(tmp, path) | ||||
| 
 | ||||
| def load_or_create_rsa(): | ||||
|     if not os.path.exists(RSA_PRIV_FILE): | ||||
|         key = RSA.generate(2048) | ||||
|         with open(RSA_PRIV_FILE, wb) as f: | ||||
|             f.write(key.export_key()) | ||||
|         with open(RSA_PUB_FILE, wb) as f: | ||||
|             f.write(key.public_key().export_key()) | ||||
|     with open(RSA_PRIV_FILE, rb) as f: | ||||
|         priv = RSA.import_key(f.read()) | ||||
|     with open(RSA_PUB_FILE, rb) as f: | ||||
|         pub = RSA.import_key(f.read()) | ||||
|     return priv, pub | ||||
| 
 | ||||
| def load_or_create_paillier(): | ||||
|     conf = read_json(CONF_FILE, {}) | ||||
|     if paillier not in conf: | ||||
|         pubkey, privkey = paillier.generate_paillier_keypair() | ||||
|         conf[paillier] = { | ||||
|             n: str(pubkey.n), | ||||
|             p: str(privkey.p), | ||||
|             q: str(privkey.q), | ||||
|         } | ||||
|         write_json(CONF_FILE, conf) | ||||
|     conf = read_json(CONF_FILE, {}) | ||||
|     n = int(conf[paillier][n]) | ||||
|     p = int(conf[paillier][p]) | ||||
|     q = int(conf[paillier][q]) | ||||
|     pubkey = paillier.PaillierPublicKey(n) | ||||
|     privkey = paillier.PaillierPrivateKey(pubkey, p, q) | ||||
|     return pubkey, privkey | ||||
| 
 | ||||
| def load_or_create_config_rsa_homomorphic_base(rsa_pub): | ||||
|     conf = read_json(CONF_FILE, {}) | ||||
|     n = rsa_pub.n | ||||
|     if rsa_homomorphic not in conf: | ||||
|         # pick base g coprime to n | ||||
|         import random | ||||
|         while True: | ||||
|             g = random.randrange(2, n - 1) | ||||
|             if GCD(g, n) == 1: | ||||
|                 break | ||||
|         conf[rsa_homomorphic] = { | ||||
|             g: str(g) | ||||
|         } | ||||
|         write_json(CONF_FILE, conf) | ||||
|     conf = read_json(CONF_FILE, {}) | ||||
|     g = int(conf[rsa_homomorphic][g]) | ||||
|     return g | ||||
| 
 | ||||
| def b64e(b: bytes) -> str: | ||||
|     return base64.b64encode(b).decode() | ||||
| 
 | ||||
| def b64d(s: str) -> bytes: | ||||
|     return base64.b64decode(s.encode()) | ||||
| 
 | ||||
| def init_storage(): | ||||
|     ensure_dirs() | ||||
|     priv, pub = load_or_create_rsa() | ||||
|     _ = load_or_create_paillier() | ||||
|     if not os.path.exists(DOCTORS_FILE): | ||||
|         write_json(DOCTORS_FILE, {}) | ||||
|     if not os.path.exists(EXPENSES_FILE): | ||||
|         write_json(EXPENSES_FILE, []) | ||||
|     if not os.path.exists(REPORTS_FILE): | ||||
|         write_json(REPORTS_FILE, []) | ||||
|     return priv, pub | ||||
| 
 | ||||
| RSA_PRIV, RSA_PUB = init_storage() | ||||
| PAI_PUB, PAI_PRIV = load_or_create_paillier() | ||||
| RSA_HOMO_G = load_or_create_config_rsa_homomorphic_base(RSA_PUB) | ||||
| 
 | ||||
| def get_public_info(): | ||||
|     return { | ||||
|         rsa_pub_pem_b64: b64e(RSA_PUB.export_key()), | ||||
|         rsa_n: str(RSA_PUB.n), | ||||
|         rsa_e: str(RSA_PUB.e), | ||||
|         paillier_n: str(PAI_PUB.n), | ||||
|         rsa_homo_g: str(RSA_HOMO_G), | ||||
|     } | ||||
| 
 | ||||
| def handle_register_doctor(body): | ||||
|     # body: {doctor_id, department_plain, dept_enc: {ciphertext, exponent}, elgamal_pub: {p,g,y}} | ||||
|     doc_id = body.get(doctor_id,).strip() | ||||
|     dept_plain = body.get(department_plain,).strip() | ||||
|     dept_enc = body.get(dept_enc) | ||||
|     elgamal_pub = body.get(elgamal_pub) | ||||
|     if not doc_id or not doc_id.isalnum(): | ||||
|         return {status:error,error:invalid doctor_id} | ||||
|     if not dept_plain: | ||||
|         return {status:error,error:invalid department} | ||||
|     if not dept_enc or ciphertext not in dept_enc or exponent not in dept_enc: | ||||
|         return {status:error,error:invalid dept_enc} | ||||
|     if not elgamal_pub or not all(k in elgamal_pub for k in [p,g,y]): | ||||
|         return {status:error,error:missing elgamal_pub} | ||||
| 
 | ||||
|     with lock: | ||||
|         doctors = read_json(DOCTORS_FILE, {}) | ||||
|         doctors[doc_id] = { | ||||
|             department_plain: dept_plain, | ||||
|             dept_enc: { | ||||
|                 ciphertext: str(int(dept_enc[ciphertext])), | ||||
|                 exponent: int(dept_enc[exponent]) | ||||
|             }, | ||||
|             elgamal_pub: { | ||||
|                 p: str(int(elgamal_pub[p])), | ||||
|                 g: str(int(elgamal_pub[g])), | ||||
|                 y: str(int(elgamal_pub[y])) | ||||
|             } | ||||
|         } | ||||
|         write_json(DOCTORS_FILE, doctors) | ||||
|     print(f[server] registered doctor {doc_id} dept='{dept_plain}' (stored encrypted and plaintext)) | ||||
|     return {status:ok} | ||||
| 
 | ||||
| def handle_upload_report(body): | ||||
|     # body: {doctor_id, filename, timestamp, md5_hex, sig: {r,s}, aes: {key_rsa_oaep_b64, nonce_b64, tag_b64, ct_b64}} | ||||
|     doc_id = body.get(doctor_id,).strip() | ||||
|     filename = os.path.basename(body.get(filename,).strip()) | ||||
|     timestamp = body.get(timestamp,).strip() | ||||
|     md5_hex = body.get(md5_hex,).strip() | ||||
|     sig = body.get(sig) | ||||
|     aes = body.get(aes) | ||||
|     if not doc_id or not filename or not timestamp or not md5_hex or not sig or not aes: | ||||
|         return {status:error,error:missing fields} | ||||
| 
 | ||||
|     with lock: | ||||
|         doctors = read_json(DOCTORS_FILE, {}) | ||||
|     if doc_id not in doctors: | ||||
|         return {status:error,error:unknown doctor_id} | ||||
| 
 | ||||
|     # decrypt AES key | ||||
|     try: | ||||
|         rsa_cipher = PKCS1_OAEP.new(RSA_PRIV) | ||||
|         aes_key = rsa_cipher.decrypt(b64d(aes[key_rsa_oaep_b64])) | ||||
|         nonce = b64d(aes[nonce_b64]) | ||||
|         tag = b64d(aes[tag_b64]) | ||||
|         ct = b64d(aes[ct_b64]) | ||||
|         aes_cipher = AES.new(aes_key, AES.MODE_EAX, nonce=nonce) | ||||
|         report_bytes = aes_cipher.decrypt_and_verify(ct, tag) | ||||
|     except Exception as e: | ||||
|         return {status:error,error:faes/rsa decrypt failed: {e}} | ||||
| 
 | ||||
|     # verify MD5 | ||||
|     import hashlib | ||||
|     md5_check = hashlib.md5(report_bytes).hexdigest() | ||||
|     if md5_check != md5_hex: | ||||
|         print([server] md5 mismatch) | ||||
|     # store file | ||||
|     outdir = os.path.join(DATA_DIR, reports) | ||||
|     os.makedirs(outdir, exist_ok=True) | ||||
|     savepath = os.path.join(outdir, f{doc_id}_{int(time.time())}_{filename}) | ||||
|     with open(savepath, wb) as f: | ||||
|         f.write(report_bytes) | ||||
| 
 | ||||
|     # store record | ||||
|     rec = { | ||||
|         doctor_id: doc_id, | ||||
|         filename: filename, | ||||
|         saved_path: savepath, | ||||
|         timestamp: timestamp, | ||||
|         md5_hex: md5_hex, | ||||
|         sig: {r: str(int(sig[r])), s: str(int(sig[s]))} | ||||
|     } | ||||
|     with lock: | ||||
|         records = read_json(REPORTS_FILE, []) | ||||
|         records.append(rec) | ||||
|         write_json(REPORTS_FILE, records) | ||||
|     print(f[server] report uploaded by {doc_id}, stored {savepath}) | ||||
|     return {status:ok} | ||||
| 
 | ||||
| def handle_submit_expense(body): | ||||
|     # body: {doctor_id, amount_ciphertext} | ||||
|     doc_id = body.get(doctor_id,).strip() | ||||
|     c = body.get(amount_ciphertext) | ||||
|     if not doc_id or not doc_id.isalnum(): | ||||
|         return {status:error,error:invalid doctor_id} | ||||
|     try: | ||||
|         c_int = int(c) | ||||
|     except: | ||||
|         return {status:error,error:invalid ciphertext} | ||||
|     with lock: | ||||
|         doctors = read_json(DOCTORS_FILE, {}) | ||||
|     if doc_id not in doctors: | ||||
|         return {status:error,error:unknown doctor_id} | ||||
| 
 | ||||
|     with lock: | ||||
|         expenses = read_json(EXPENSES_FILE, []) | ||||
|         expenses.append({doctor_id: doc_id, ciphertext: str(c_int)}) | ||||
|         write_json(EXPENSES_FILE, expenses) | ||||
|     print(f[server] expense ciphertext stored for {doc_id}) | ||||
|     return {status:ok} | ||||
| 
 | ||||
| class RequestHandler(socketserver.StreamRequestHandler): | ||||
|     def handle(self): | ||||
|         try: | ||||
|             data = self.rfile.readline() | ||||
|             if not data: | ||||
|                 return | ||||
|             req = json.loads(data.decode()) | ||||
|             action = req.get(action) | ||||
|             role = req.get(role, ) | ||||
|             body = req.get(body, {}) | ||||
|             if action == get_public_info: | ||||
|                 resp = {status:ok,data: get_public_info()} | ||||
|             elif action == register_doctor: | ||||
|                 if role != doctor: | ||||
|                     resp = {status:error,error:unauthorized} | ||||
|                 else: | ||||
|                     resp = handle_register_doctor(body) | ||||
|             elif action == upload_report: | ||||
|                 if role != doctor: | ||||
|                     resp = {status:error,error:unauthorized} | ||||
|                 else: | ||||
|                     resp = handle_upload_report(body) | ||||
|             elif action == submit_expense: | ||||
|                 if role != doctor: | ||||
|                     resp = {status:error,error:unauthorized} | ||||
|                 else: | ||||
|                     resp = handle_submit_expense(body) | ||||
|             else: | ||||
|                 resp = {status:error,error:unknown action} | ||||
|         except Exception as e: | ||||
|             resp = {status:error,error:str(e)} | ||||
|         self.wfile.write((json.dumps(resp)+\n).encode()) | ||||
| 
 | ||||
| def start_server(): | ||||
|     server = socketserver.ThreadingTCPServer((127.0.0.1, PORT), RequestHandler) | ||||
|     t = threading.Thread(target=server.serve_forever, daemon=True) | ||||
|     t.start() | ||||
|     print(f[server] listening on 127.0.0.1:{PORT}) | ||||
|     return server | ||||
| 
 | ||||
| # Auditor utilities | ||||
| 
 | ||||
| def load_doctors(): | ||||
|     return read_json(DOCTORS_FILE, {}) | ||||
| 
 | ||||
| def load_expenses(): | ||||
|     return read_json(EXPENSES_FILE, []) | ||||
| 
 | ||||
| def load_reports(): | ||||
|     return read_json(REPORTS_FILE, []) | ||||
| 
 | ||||
| def audit_list_doctors(): | ||||
|     docs = load_doctors() | ||||
|     print(Doctors:) | ||||
|     for did, info in docs.items(): | ||||
|         enc = info[dept_enc] | ||||
|         print(f- {did} dept_plain='{info['department_plain']}' enc_ciphertext={enc['ciphertext']} exponent={enc['exponent']}) | ||||
| 
 | ||||
| def audit_keyword_search(): | ||||
|     docs = load_doctors() | ||||
|     if not docs: | ||||
|         print(no doctors) | ||||
|         return | ||||
|     q = input(Enter department keyword to search: ).strip() | ||||
|     if not q: | ||||
|         print(empty) | ||||
|         return | ||||
|     # hash to int | ||||
|     import hashlib | ||||
|     h = int.from_bytes(hashlib.sha256(q.encode()).digest(), big) | ||||
|     pub = PAI_PUB | ||||
|     priv = PAI_PRIV | ||||
|     enc_q = pub.encrypt(h) | ||||
|     print(Matching doctors (using Paillier equality on hashed dept):) | ||||
|     for did, info in docs.items(): | ||||
|         enc = info[dept_enc] | ||||
|         c = int(enc[ciphertext]) | ||||
|         exp = int(enc[exponent]) | ||||
|         enc_doc = paillier.EncryptedNumber(pub, c, exp) | ||||
|         diff = enc_doc - enc_q | ||||
|         dec = priv.decrypt(diff) | ||||
|         match = (dec == 0) | ||||
|         print(f  {did}: dept_plain='{info['department_plain']}' enc_ciphertext={c} match={match}) | ||||
| 
 | ||||
| def rsa_homo_decrypt_sum(c_prod_int): | ||||
|     n = RSA_PRIV.n | ||||
|     d = RSA_PRIV.d | ||||
|     g = RSA_HOMO_G | ||||
|     # decrypt to get g^sum mod n | ||||
|     m = pow(int(c_prod_int), d, n) | ||||
|     # brute force discrete log for small sums | ||||
|     max_iter = 500000 | ||||
|     acc = 1 | ||||
|     for k in range(0, max_iter+1): | ||||
|         if acc == m: | ||||
|             return k | ||||
|         acc = (acc * g) % n | ||||
|     return None | ||||
| 
 | ||||
| def audit_sum_expenses(): | ||||
|     exps = load_expenses() | ||||
|     if not exps: | ||||
|         print(no expenses) | ||||
|         return | ||||
|     # sum all | ||||
|     n = RSA_PUB.n | ||||
|     c_prod = 1 | ||||
|     for e in exps: | ||||
|         c_prod = (c_prod * int(e[ciphertext])) % n | ||||
|     print(fProduct ciphertext (represents sum under RSA-in-exponent): {c_prod}) | ||||
|     s = rsa_homo_decrypt_sum(c_prod) | ||||
|     if s is None: | ||||
|         print(sum decryption failed (exceeded search bound)) | ||||
|     else: | ||||
|         print(fDecrypted sum of expenses = {s}) | ||||
|     # by doctor | ||||
|     docs = load_doctors() | ||||
|     if docs: | ||||
|         print(Per-doctor sums:) | ||||
|         for did in docs.keys(): | ||||
|             c_prod_d = 1 | ||||
|             count = 0 | ||||
|             for e in exps: | ||||
|                 if e[doctor_id] == did: | ||||
|                     c_prod_d = (c_prod_d * int(e[ciphertext])) % n | ||||
|                     count += 1 | ||||
|             if count == 0: | ||||
|                 continue | ||||
|             s_d = rsa_homo_decrypt_sum(c_prod_d) | ||||
|             print(f  {did}: entries={count} product_ct={c_prod_d} sum={s_d}) | ||||
| 
 | ||||
| def elgamal_verify(p, g, y, H_int, r, s): | ||||
|     # verify: g^H ≡ y^r * r^s (mod p) | ||||
|     return pow(g, H_int, p) == (pow(y, r, p) * pow(r, s, p)) % p | ||||
| 
 | ||||
| def audit_verify_reports(): | ||||
|     records = load_reports() | ||||
|     if not records: | ||||
|         print(no reports) | ||||
|         return | ||||
|     doctors = load_doctors() | ||||
|     for rec in records: | ||||
|         did = rec[doctor_id] | ||||
|         docinfo = doctors.get(did) | ||||
|         ok_sig = False | ||||
|         ok_ts = False | ||||
|         if docinfo: | ||||
|             p = int(docinfo[elgamal_pub][p]) | ||||
|             g = int(docinfo[elgamal_pub][g]) | ||||
|             y = int(docinfo[elgamal_pub][y]) | ||||
|             r = int(rec[sig][r]) | ||||
|             s = int(rec[sig][s]) | ||||
|             try: | ||||
|                 with open(rec[saved_path], rb) as f: | ||||
|                     report_bytes = f.read() | ||||
|                 import hashlib | ||||
|                 H = int.from_bytes(hashlib.md5(report_bytes + rec[timestamp].encode()).digest(), big) % (p - 1) | ||||
|                 ok_sig = elgamal_verify(p, g, y, H, r, s) | ||||
|             except Exception as e: | ||||
|                 ok_sig = False | ||||
|         # timestamp check | ||||
|         try: | ||||
|             ts = datetime.fromisoformat(rec[timestamp]) | ||||
|         except: | ||||
|             try: | ||||
|                 ts = datetime.strptime(rec[timestamp], %Y-%m-%dT%H:%M:%S.%f) | ||||
|             except: | ||||
|                 ts = None | ||||
|         if ts: | ||||
|             now = datetime.utcnow().replace(tzinfo=None) | ||||
|             delta = (now - ts).total_seconds() | ||||
|             # simple rule: not in the future by more than 5 min | ||||
|             ok_ts = (delta >= -300) | ||||
|         print(f- report by {did} file={os.path.basename(rec['saved_path'])} sig_ok={ok_sig} ts_ok={ok_ts} ts={rec['timestamp']} md5={rec['md5_hex']}) | ||||
| 
 | ||||
| def auditor_menu(): while True: print(\n[Auditor Menu]) print(1) List doctors (show encrypted and plaintext dept)) print(2) Keyword search doctors by dept (Paillier)) print(3) Sum expenses (RSA-in-exponent demo)) print(4) Verify reports and timestamps) print(5) Show server public info) print(0) Exit) ch = input(Select: ).strip() if ch == 1: audit_list_doctors() elif ch == 2: audit_keyword_search() elif ch == 3: audit_sum_expenses() elif ch == 4: audit_verify_reports() elif ch == 5: info = get_public_info() print(json.dumps(info, indent=2)) elif ch == 0: print(bye) break else: print(invalid) if __name__ == __main__: start_server() auditor_menu() | ||||
| 
 | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue