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 os | ||||||
| import json | import json | ||||||
| import socket | import socket | ||||||
| import base64 |  | ||||||
| import hashlib | 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.PublicKey import RSA, ElGamal | ||||||
| from Crypto.Random import get_random_bytes | from Crypto.Cipher import PKCS1_OAEP, AES | ||||||
| from Crypto.Util.number import inverse, GCD | from Crypto.Hash import MD5 | ||||||
| from Crypto.Cipher import AES, PKCS1_OAEP | from Crypto.Random import get_random_bytes, random | ||||||
|  | from Crypto.Util.number import GCD, inverse | ||||||
| from phe import paillier | from phe import paillier | ||||||
| 
 | 
 | ||||||
|  | # Configuration | ||||||
| SERVER_HOST = "127.0.0.1" | SERVER_HOST = "127.0.0.1" | ||||||
| SERVER_PORT = 5000 | SERVER_PORT = 5000 | ||||||
|  | CLIENT_STATE_FILE = "client_state.json" | ||||||
| INPUT_DIR = "inputdata" | INPUT_DIR = "inputdata" | ||||||
| KEYS_DIR = "client_keys" |  | ||||||
| 
 | 
 | ||||||
| server_info = {}  # filled by get_public_info |  | ||||||
| cached_paillier_pub = None |  | ||||||
| 
 | 
 | ||||||
| def ensure_dirs(): | def ensure_dirs(): | ||||||
|     os.makedirs(INPUT_DIR, exist_ok=True) |     Path(INPUT_DIR).mkdir(exist_ok=True) | ||||||
|     os.makedirs(KEYS_DIR, exist_ok=True) |  | ||||||
| 
 | 
 | ||||||
| def b64e(b: bytes) -> str: |  | ||||||
|     return base64.b64encode(b).decode() |  | ||||||
| 
 | 
 | ||||||
| def b64d(s: str) -> bytes: | def load_client_state(): | ||||||
|     return base64.b64decode(s.encode()) |     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): | def send_request(action, role, body): | ||||||
|  |     """Send JSON request to server and receive response.""" | ||||||
|     req = {"action": action, "role": role, "body": body} |     req = {"action": action, "role": role, "body": body} | ||||||
|     with socket.create_connection((SERVER_HOST, SERVER_PORT), timeout=5) as s: |     try: | ||||||
|         s.sendall((json.dumps(req) + "\n").encode()) |         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|         data = s.recv(1024*1024) |         sock.connect((SERVER_HOST, SERVER_PORT)) | ||||||
|     resp = json.loads(data.decode()) |         sock.sendall((json.dumps(req) + "\n").encode()) | ||||||
|     if resp.get("status") != "ok": |         data = sock.recv(4096).decode() | ||||||
|         print("server error:", resp.get("error")) |         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 None | ||||||
|     return resp.get("data") if "data" in resp else {} |  | ||||||
| 
 | 
 | ||||||
| def get_public_info(): |     n = int(state["server_keys"]["rsa_n"]) | ||||||
|     global server_info, cached_paillier_pub |     e = int(state["server_keys"]["rsa_e"]) | ||||||
|     data = send_request("get result:") |     g = int(state["server_keys"]["rsa_homo_g"]) | ||||||
|     if data is None: |  | ||||||
| 
 | 
 | ||||||
| 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