474 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			474 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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(1024)
 | |
|     pub_pem = rsa_oaep_key.publickey().export_key().decode()
 | |
|     priv_pem = rsa_oaep_key.export_key().decode()
 | |
| 
 | |
|     homo_rsa = RSA.generate(1024)
 | |
|     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(1024, 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()
 |