This commit is contained in:
sherlock 2025-10-28 16:51:29 +05:30
parent 9d70a80337
commit b18969362d
2 changed files with 296 additions and 433 deletions

View file

@ -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()

View file

@ -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()