From 9d70a803376cfce56cc2d7288861541cb8878ff1 Mon Sep 17 00:00:00 2001 From: student Date: Tue, 28 Oct 2025 10:08:36 +0530 Subject: [PATCH] labendsem --- IS/Lab/Eval-Endsem/ans.py | 434 +++++++++++++++++++++ IS/Lab/Eval-Endsem/client.py | 47 +++ IS/Lab/Eval-Endsem/inputdata/diag2.md | 5 + IS/Lab/Eval-Endsem/inputdata/diagnosis.md | 19 + IS/Lab/Eval-Endsem/q.md | 0 IS/Lab/Eval-Endsem/rough.md | 407 ++++++++++++++++++++ IS/Lab/Eval-Endsem/server.py | 436 ++++++++++++++++++++++ IS/Lab/Lab6/socket_rsa/client.py | 1 + IS/Lab/Lab8/PKSE/encrypted_index.pkl | Bin 18227 -> 18226 bytes 9 files changed, 1349 insertions(+) create mode 100644 IS/Lab/Eval-Endsem/ans.py create mode 100644 IS/Lab/Eval-Endsem/client.py create mode 100644 IS/Lab/Eval-Endsem/inputdata/diag2.md create mode 100644 IS/Lab/Eval-Endsem/inputdata/diagnosis.md create mode 100644 IS/Lab/Eval-Endsem/q.md create mode 100644 IS/Lab/Eval-Endsem/rough.md create mode 100644 IS/Lab/Eval-Endsem/server.py diff --git a/IS/Lab/Eval-Endsem/ans.py b/IS/Lab/Eval-Endsem/ans.py new file mode 100644 index 0000000..4b7a362 --- /dev/null +++ b/IS/Lab/Eval-Endsem/ans.py @@ -0,0 +1,434 @@ + +import os +import json +import base64 +import uuid +from datetime import datetime, timezone + +from Crypto.PublicKey import RSA, ElGamal +from Crypto.Cipher import PKCS1_OAEP +from Crypto.Hash import MD5 +from Crypto.Random import random, get_random_bytes +from Crypto.Util.number import GCD, inverse + +try: + from phe import paillier +except ImportError: + print("Install dependency: pip install phe") + raise + +STATE_FILE = "server_state.json" +INPUT_DIR = "inputdata" + +def ensure_dirs(): + if not os.path.exists(INPUT_DIR): + os.makedirs(INPUT_DIR) + +def load_state(): + if not os.path.exists(STATE_FILE): + return { + "server": {}, + "doctors": {}, + "reports": [], + "expenses": {} + } + with open(STATE_FILE, "r") as f: + return json.load(f) + +def save_state(state): + with open(STATE_FILE, "w") as f: + json.dump(state, f, indent=2) + +def gen_server_keys(state): + if "rsa_oaep" in state["server"]: + print("Server keys already exist.") + return + rsa_oaep_key = RSA.generate(512) + pub_pem = rsa_oaep_key.publickey().export_key().decode() + priv_pem = rsa_oaep_key.export_key().decode() + + homo_rsa = RSA.generate(512) + n = int(homo_rsa.n) + e = int(homo_rsa.e) + d = int(homo_rsa.d) + # base for exponent-trick homomorphic addition + while True: + base = random.randint(2, n - 2) + if GCD(base, n) == 1: + break + max_exp = 10000 + + pub, priv = paillier.generate_paillier_keypair(n_length=1024) + + state["server"]["rsa_oaep"] = {"pub_pem": pub_pem, "priv_pem": priv_pem} + state["server"]["homo_rsa"] = {"n": n, "e": e, "d": d, "base": base, "max_exp": max_exp} + state["server"]["paillier"] = {"n": pub.n, "p": priv.p, "q": priv.q} + save_state(state) + print("Server RSA-OAEP, Homo-RSA, and Paillier keys generated.") + +def get_paillier_keys(state): + n = state["server"]["paillier"]["n"] + p = state["server"]["paillier"]["p"] + q = state["server"]["paillier"]["q"] + pub = paillier.PaillierPublicKey(n) + priv = paillier.PaillierPrivateKey(pub, p, q) + return pub, priv + +def register_doctor(state): + name = input("Doctor name: ").strip() + dept = input("Department: ").strip() + doc_id = "doc_" + uuid.uuid4().hex[:8] + + eg_key = ElGamal.generate(512, get_random_bytes) + # ElGamal object has p,g,y,x attributes + p = int(eg_key.p) + g = int(eg_key.g) + y = int(eg_key.y) + x = int(eg_key.x) + + # Paillier encrypt department hash + pub, _ = get_paillier_keys(state) + dept_md5_int = int.from_bytes(MD5.new(dept.encode()).digest(), "big") + dept_enc = pub.encrypt(dept_md5_int) + + state["doctors"][doc_id] = { + "name": name, + "department": dept, + "department_md5": dept_md5_int, + "department_paillier": { + "ciphertext": int(dept_enc.ciphertext()), + "exponent": dept_enc.exponent + }, + "elgamal": {"p": p, "g": g, "y": y, "x": x} + } + save_state(state) + print(f"Registered doctor {name} with id {doc_id} in dept {dept}.") + print(f"ElGamal pub (p,g,y) set. Department stored both plaintext and Paillier-encrypted.") + +def list_markdown_files(): + ensure_dirs() + files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith(".md")] + for i, f in enumerate(files, 1): + print(f"{i}. {f}") + return files + +def rsa_oaep_encrypt_large(data_bytes, pub_pem): + pub = RSA.import_key(pub_pem) + cipher = PKCS1_OAEP.new(pub) # SHA1 by default + k = pub.size_in_bytes() + hlen = 20 + max_pt = k - 2*hlen - 2 + out = b"" + for i in range(0, len(data_bytes), max_pt): + block = data_bytes[i:i+max_pt] + out += cipher.encrypt(block) + return base64.b64encode(out).decode() + +def rsa_oaep_decrypt_large(b64, priv_pem): + data = base64.b64decode(b64.encode()) + priv = RSA.import_key(priv_pem) + cipher = PKCS1_OAEP.new(priv) + k = priv.size_in_bytes() + out = b"" + for i in range(0, len(data), k): + block = data[i:i+k] + out += cipher.decrypt(block) + return out + +def elgamal_sign(doc_eg, msg_bytes): + p = int(doc_eg["p"]) + g = int(doc_eg["g"]) + x = int(doc_eg["x"]) + H = int(MD5.new(msg_bytes).hexdigest(), 16) % (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 elgamal_verify(pub_eg, msg_bytes, sig): + p = int(pub_eg["p"]) + g = int(pub_eg["g"]) + y = int(pub_eg["y"]) + r, s = sig + if not (1 < r < p): + return False + H = int(MD5.new(msg_bytes).hexdigest(), 16) % (p - 1) + v1 = (pow(y, r, p) * pow(r, s, p)) % p + v2 = pow(g, H, p) + return v1 == v2 + +def doctor_submit_report(state): + if not state["doctors"]: + print("No doctors. Register first.") + return + doc_id = input("Enter your doctor id: ").strip() + if doc_id not in state["doctors"]: + print("Unknown doctor.") + return + files = list_markdown_files() + if not files: + print("Place a markdown file in inputdata/") + return + idx = int(input("Select file #: ").strip()) + filename = files[idx - 1] + path = os.path.join(INPUT_DIR, filename) + with open(path, "rb") as f: + report_bytes = f.read() + md5_hex = MD5.new(report_bytes).hexdigest() + ts = datetime.now(timezone.utc).isoformat() + + msg_to_sign = report_bytes + ts.encode() + r, s = elgamal_sign(state["doctors"][doc_id]["elgamal"], msg_to_sign) + + pub_pem = state["server"]["rsa_oaep"]["pub_pem"] + ct_b64 = rsa_oaep_encrypt_large(report_bytes, pub_pem) + + rep_id = "rep_" + uuid.uuid4().hex[:8] + rec = { + "report_id": rep_id, + "doctor_id": doc_id, + "doctor_name": state["doctors"][doc_id]["name"], + "filename": filename, + "timestamp_utc": ts, + "md5_hex": md5_hex, + "elgamal_sig": {"r": r, "s": s}, + "rsa_oaep_b64": ct_b64 + } + state["reports"].append(rec) + save_state(state) + print("Report submitted.") + print(f"id: {rep_id}") + print(f"md5: {md5_hex}") + print(f"sig: r={r} s={s}") + print(f"enc blocks (base64 len): {len(ct_b64)}") + +def homo_rsa_encrypt_amount(state, amount): + n = int(state["server"]["homo_rsa"]["n"]) + e = int(state["server"]["homo_rsa"]["e"]) + base = int(state["server"]["homo_rsa"]["base"]) + max_exp = int(state["server"]["homo_rsa"]["max_exp"]) + if amount < 0 or amount > max_exp: + raise ValueError("amount out of allowed range") + m = pow(base, amount, n) + c = pow(m, e, n) + return int(c) + +def homo_rsa_discrete_log(m, base, mod, max_k): + val = 1 % mod + if m == 1 % mod: + return 0 + for k in range(1, max_k + 1): + val = (val * base) % mod + if val == m: + return k + return None + +def doctor_submit_expense(state): + if not state["doctors"]: + print("No doctors. Register first.") + return + doc_id = input("Enter your doctor id: ").strip() + if doc_id not in state["doctors"]: + print("Unknown doctor.") + return + amt = int(input("Expense integer (<=10000): ").strip()) + c = homo_rsa_encrypt_amount(state, amt) + state["expenses"].setdefault(doc_id, []).append(c) + save_state(state) + print(f"Encrypted expense stored for {doc_id}.") + print(f"ciphertext: {c}") + +def auditor_list_reports(state): + if not state["reports"]: + print("No reports.") + return + for r in state["reports"]: + print(f"{r['report_id']} by {r['doctor_id']} at {r['timestamp_utc']} file={r['filename']} md5={r['md5_hex']}") + +def auditor_verify_report(state): + if not state["reports"]: + print("No reports.") + return + rep_id = input("Report id: ").strip() + rec = next((r for r in state["reports"] if r["report_id"] == rep_id), None) + if not rec: + print("Not found.") + return + priv_pem = state["server"]["rsa_oaep"]["priv_pem"] + pt = rsa_oaep_decrypt_large(rec["rsa_oaep_b64"], priv_pem) + md5_calc = MD5.new(pt).hexdigest() + ok_md5 = (md5_calc == rec["md5_hex"]) + + doc = state["doctors"][rec["doctor_id"]] + pub_eg = {"p": doc["elgamal"]["p"], "g": doc["elgamal"]["g"], "y": doc["elgamal"]["y"]} + msg = pt + rec["timestamp_utc"].encode() + ok_sig = elgamal_verify(pub_eg, msg, (rec["elgamal_sig"]["r"], rec["elgamal_sig"]["s"])) + + ts = datetime.fromisoformat(rec["timestamp_utc"]) + now = datetime.now(timezone.utc) + skew_sec = (now - ts).total_seconds() + + print("Verification results:") + print(f"md5 match: {ok_md5}") + print(f"signature valid: {ok_sig}") + print(f"timestamp: {rec['timestamp_utc']}") + print(f"server now: {now.isoformat()}") + print(f"age seconds: {int(skew_sec)} (future? {skew_sec < 0})") + +def auditor_keyword_search(state): + if not state["doctors"]: + print("No doctors.") + return + dept_q = input("Search department: ").strip() + pub, priv = get_paillier_keys(state) + q_int = int.from_bytes(MD5.new(dept_q.encode()).digest(), "big") + q_enc = pub.encrypt(q_int) + + print("Records:") + found = [] + for doc_id, doc in state["doctors"].items(): + enc_info = doc["department_paillier"] + enc_doc = paillier.EncryptedNumber(pub, int(enc_info["ciphertext"]), int(enc_info["exponent"])) + diff = enc_doc - q_enc + val = priv.decrypt(diff) + is_match = (val == 0) + if is_match: + found.append(doc_id) + print(f"{doc_id}: dept='{doc['department']}' enc_ct={enc_info['ciphertext']} match={is_match}") + print(f"Matches: {found}") + +def auditor_sum_expenses(state): + homo = state["server"]["homo_rsa"] + n = int(homo["n"]); d = int(homo["d"]); base = int(homo["base"]); max_exp = int(homo["max_exp"]) + if not state["expenses"]: + print("No expenses.") + return + choice = input("Sum for 'all' or specific doc_id: ").strip() + c_list = [] + if choice.lower() == "all": + for doc_id, lst in state["expenses"].items(): + c_list += lst + else: + if choice not in state["expenses"]: + print("No expenses for given id.") + return + c_list = state["expenses"][choice] + if not c_list: + print("No expenses to sum.") + return + prod = 1 + for c in c_list: + prod = (prod * int(c)) % n + m = pow(prod, d, n) + s = homo_rsa_discrete_log(m, base, n, max_exp) + print("Homomorphic sum result:") + print(f"combined ciphertext (mod n): {prod}") + if s is None: + print("decrypted sum: could not recover (out of range)") + else: + print(f"decrypted sum: {s}") + +def list_doctors(state): + if not state["doctors"]: + print("No doctors.") + return + for doc_id, d in state["doctors"].items(): + print(f"{doc_id}: {d['name']} dept='{d['department']}'") + +def doctor_list_my_data(state): + doc_id = input("Enter your doctor id: ").strip() + if doc_id not in state["doctors"]: + print("Unknown doctor.") + return + print("Reports:") + for r in state["reports"]: + if r["doctor_id"] == doc_id: + print(f"{r['report_id']} {r['filename']} {r['timestamp_utc']}") + print("Expenses (ciphertexts):") + for c in state["expenses"].get(doc_id, []): + print(c) + +def main(): + ensure_dirs() + state = load_state() + while True: + print("\nMain Menu") + print("1. Setup server keys") + print("2. Register doctor") + print("3. Doctor menu") + print("4. Auditor menu") + print("5. List doctors") + print("0. Exit") + ch = input("Choice: ").strip() + if ch == "1": + gen_server_keys(state) + elif ch == "2": + if "rsa_oaep" not in state["server"]: + print("Setup server keys first.") + else: + register_doctor(state) + elif ch == "3": + while True: + print("\nDoctor Menu") + print("1. Submit report (md file in inputdata/)") + print("2. Submit expense (homomorphic RSA)") + print("3. List my reports/expenses") + print("0. Back") + dch = input("Choice: ").strip() + if dch == "1": + if "rsa_oaep" not in state["server"]: + print("Setup server keys first.") + else: + doctor_submit_report(state) + elif dch == "2": + if "homo_rsa" not in state["server"]: + print("Setup server keys first.") + else: + doctor_submit_expense(state) + elif dch == "3": + doctor_list_my_data(state) + elif dch == "0": + break + else: + print("Invalid.") + elif ch == "4": + while True: + print("\nAuditor Menu") + print("1. List reports") + print("2. Verify a report (sig + timestamp)") + print("3. Dept keyword search (Paillier)") + print("4. Sum expenses (homomorphic RSA)") + print("0. Back") + ach = input("Choice: ").strip() + if ach == "1": + auditor_list_reports(state) + elif ach == "2": + auditor_verify_report(state) + elif ach == "3": + if "paillier" not in state["server"]: + print("Setup server keys first.") + else: + auditor_keyword_search(state) + elif ach == "4": + if "homo_rsa" not in state["server"]: + print("Setup server keys first.") + else: + auditor_sum_expenses(state) + elif ach == "0": + break + else: + print("Invalid.") + elif ch == "5": + list_doctors(state) + elif ch == "0": + print("Bye.") + break + else: + print("Invalid.") + +if __name__ == "__main__": + main() diff --git a/IS/Lab/Eval-Endsem/client.py b/IS/Lab/Eval-Endsem/client.py new file mode 100644 index 0000000..022bf6b --- /dev/null +++ b/IS/Lab/Eval-Endsem/client.py @@ -0,0 +1,47 @@ +import os +import json +import socket +import base64 +import hashlib +from datetime import datetime +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 phe import paillier + +SERVER_HOST = "127.0.0.1" +SERVER_PORT = 5000 +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) + +def b64e(b: bytes) -> str: + return base64.b64encode(b).decode() + +def b64d(s: str) -> bytes: + return base64.b64decode(s.encode()) + +def send_request(action, role, body): + 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")) + 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: + +def rs diff --git a/IS/Lab/Eval-Endsem/inputdata/diag2.md b/IS/Lab/Eval-Endsem/inputdata/diag2.md new file mode 100644 index 0000000..c52e20f --- /dev/null +++ b/IS/Lab/Eval-Endsem/inputdata/diag2.md @@ -0,0 +1,5 @@ +hello +world +test +medical +doc \ No newline at end of file diff --git a/IS/Lab/Eval-Endsem/inputdata/diagnosis.md b/IS/Lab/Eval-Endsem/inputdata/diagnosis.md new file mode 100644 index 0000000..f10c38c --- /dev/null +++ b/IS/Lab/Eval-Endsem/inputdata/diagnosis.md @@ -0,0 +1,19 @@ +Patient - Aadit + +Patient No - 230953344 + +Blood Grp - A Positive + +Org - MIT Manipal + +Ailments +-------- +Allergic Rhinitis +Pancreatic Rest +Migraine + +Medicine +-------- +Pantoprazole +SOS inhaler +Omeprazole \ No newline at end of file diff --git a/IS/Lab/Eval-Endsem/q.md b/IS/Lab/Eval-Endsem/q.md new file mode 100644 index 0000000..e69de29 diff --git a/IS/Lab/Eval-Endsem/rough.md b/IS/Lab/Eval-Endsem/rough.md new file mode 100644 index 0000000..68b4826 --- /dev/null +++ b/IS/Lab/Eval-Endsem/rough.md @@ -0,0 +1,407 @@ +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() + diff --git a/IS/Lab/Eval-Endsem/server.py b/IS/Lab/Eval-Endsem/server.py new file mode 100644 index 0000000..c93a87a --- /dev/null +++ b/IS/Lab/Eval-Endsem/server.py @@ -0,0 +1,436 @@ +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":f"aes/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(f"Product 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(f"Decrypted 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() + diff --git a/IS/Lab/Lab6/socket_rsa/client.py b/IS/Lab/Lab6/socket_rsa/client.py index 08460b3..542b463 100644 --- a/IS/Lab/Lab6/socket_rsa/client.py +++ b/IS/Lab/Lab6/socket_rsa/client.py @@ -46,4 +46,5 @@ def main() -> None: if __name__ == "__main__": main() +curl diff --git a/IS/Lab/Lab8/PKSE/encrypted_index.pkl b/IS/Lab/Lab8/PKSE/encrypted_index.pkl index 5082e65a82bea6b4d79dec1628e7f879890c2f5f..de1e38e80d3796c17030bb9d351f5cd8b2f3be50 100644 GIT binary patch literal 18226 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 literal 18227 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