From b18969362d3a0736fd5fa266fd96429a04f1bf78 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 28 Oct 2025 16:51:29 +0530 Subject: [PATCH] client --- IS/Lab/Eval-Endsem/client.py | 322 ++++++++++++++++++++++++--- IS/Lab/Eval-Endsem/rough.md | 407 ----------------------------------- 2 files changed, 296 insertions(+), 433 deletions(-) delete mode 100644 IS/Lab/Eval-Endsem/rough.md diff --git a/IS/Lab/Eval-Endsem/client.py b/IS/Lab/Eval-Endsem/client.py index 022bf6b..13e97b1 100644 --- a/IS/Lab/Eval-Endsem/client.py +++ b/IS/Lab/Eval-Endsem/client.py @@ -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() diff --git a/IS/Lab/Eval-Endsem/rough.md b/IS/Lab/Eval-Endsem/rough.md deleted file mode 100644 index 68b4826..0000000 --- a/IS/Lab/Eval-Endsem/rough.md +++ /dev/null @@ -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() -