client
This commit is contained in:
parent
9d70a80337
commit
b18969362d
2 changed files with 296 additions and 433 deletions
|
|
@ -1,47 +1,317 @@
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
import base64
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA, ElGamal
|
from Crypto.PublicKey import RSA, ElGamal
|
||||||
from Crypto.Random import get_random_bytes
|
from Crypto.Cipher import PKCS1_OAEP, AES
|
||||||
from Crypto.Util.number import inverse, GCD
|
from Crypto.Hash import MD5
|
||||||
from Crypto.Cipher import AES, PKCS1_OAEP
|
from Crypto.Random import get_random_bytes, random
|
||||||
|
from Crypto.Util.number import GCD, inverse
|
||||||
from phe import paillier
|
from phe import paillier
|
||||||
|
|
||||||
|
# Configuration
|
||||||
SERVER_HOST = "127.0.0.1"
|
SERVER_HOST = "127.0.0.1"
|
||||||
SERVER_PORT = 5000
|
SERVER_PORT = 5000
|
||||||
|
CLIENT_STATE_FILE = "client_state.json"
|
||||||
INPUT_DIR = "inputdata"
|
INPUT_DIR = "inputdata"
|
||||||
KEYS_DIR = "client_keys"
|
|
||||||
|
|
||||||
server_info = {} # filled by get_public_info
|
|
||||||
cached_paillier_pub = None
|
|
||||||
|
|
||||||
def ensure_dirs():
|
def ensure_dirs():
|
||||||
os.makedirs(INPUT_DIR, exist_ok=True)
|
Path(INPUT_DIR).mkdir(exist_ok=True)
|
||||||
os.makedirs(KEYS_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
def b64e(b: bytes) -> str:
|
|
||||||
return base64.b64encode(b).decode()
|
|
||||||
|
|
||||||
def b64d(s: str) -> bytes:
|
def load_client_state():
|
||||||
return base64.b64decode(s.encode())
|
if not os.path.exists(CLIENT_STATE_FILE):
|
||||||
|
return {"doctor_id": None, "elgamal": {}, "server_keys": {}}
|
||||||
|
with open(CLIENT_STATE_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def save_client_state(state):
|
||||||
|
with open(CLIENT_STATE_FILE, "w") as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def send_request(action, role, body):
|
def send_request(action, role, body):
|
||||||
|
"""Send JSON request to server and receive response."""
|
||||||
req = {"action": action, "role": role, "body": body}
|
req = {"action": action, "role": role, "body": body}
|
||||||
with socket.create_connection((SERVER_HOST, SERVER_PORT), timeout=5) as s:
|
try:
|
||||||
s.sendall((json.dumps(req) + "\n").encode())
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
data = s.recv(1024*1024)
|
sock.connect((SERVER_HOST, SERVER_PORT))
|
||||||
resp = json.loads(data.decode())
|
sock.sendall((json.dumps(req) + "\n").encode())
|
||||||
if resp.get("status") != "ok":
|
data = sock.recv(4096).decode()
|
||||||
print("server error:", resp.get("error"))
|
sock.close()
|
||||||
|
return json.loads(data)
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "error": f"Connection failed: {e}"}
|
||||||
|
|
||||||
|
|
||||||
|
def b64e(b: bytes) -> str:
|
||||||
|
import base64
|
||||||
|
|
||||||
|
return base64.b64encode(b).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def b64d(s: str) -> bytes:
|
||||||
|
import base64
|
||||||
|
|
||||||
|
return base64.b64decode(s.encode())
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_server_keys(state):
|
||||||
|
"""Get server's public keys."""
|
||||||
|
resp = send_request("get_public_info", "doctor", {})
|
||||||
|
if resp.get("status") == "ok":
|
||||||
|
state["server_keys"] = resp.get("data", {})
|
||||||
|
save_client_state(state)
|
||||||
|
print("Server keys fetched.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Failed to fetch server keys: {resp.get('error')}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def register_doctor_client(state):
|
||||||
|
"""Register a new doctor with the server."""
|
||||||
|
print("\n=== Doctor Registration ===")
|
||||||
|
doctor_id = input("Choose doctor ID (alphanumeric): ").strip()
|
||||||
|
if not doctor_id.isalnum():
|
||||||
|
print("Invalid doctor ID.")
|
||||||
|
return
|
||||||
|
|
||||||
|
name = input("Doctor name: ").strip()
|
||||||
|
department = input("Department: ").strip()
|
||||||
|
|
||||||
|
if not state["server_keys"]:
|
||||||
|
print("Fetch server keys first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate ElGamal keypair
|
||||||
|
print("Generating ElGamal keypair...")
|
||||||
|
eg_key = ElGamal.generate(512, get_random_bytes)
|
||||||
|
p = int(eg_key.p)
|
||||||
|
g = int(eg_key.g)
|
||||||
|
y = int(eg_key.y)
|
||||||
|
x = int(eg_key.x)
|
||||||
|
|
||||||
|
state["doctor_id"] = doctor_id
|
||||||
|
state["elgamal"] = {"p": p, "g": g, "y": y, "x": x}
|
||||||
|
|
||||||
|
# Encrypt department using Paillier
|
||||||
|
paillier_n = int(state["server_keys"]["paillier_n"])
|
||||||
|
paillier_pub = paillier.PaillierPublicKey(paillier_n)
|
||||||
|
|
||||||
|
dept_hash = int.from_bytes(hashlib.md5(department.encode()).digest(), "big")
|
||||||
|
dept_enc = paillier_pub.encrypt(dept_hash)
|
||||||
|
|
||||||
|
# Prepare request
|
||||||
|
body = {
|
||||||
|
"doctor_id": doctor_id,
|
||||||
|
"department_plain": department,
|
||||||
|
"dept_enc": {
|
||||||
|
"ciphertext": int(dept_enc.ciphertext()),
|
||||||
|
"exponent": dept_enc.exponent,
|
||||||
|
},
|
||||||
|
"elgamal_pub": {"p": p, "g": g, "y": y},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = send_request("register_doctor", "doctor", body)
|
||||||
|
if resp.get("status") == "ok":
|
||||||
|
save_client_state(state)
|
||||||
|
print(f"✓ Doctor '{doctor_id}' registered successfully.")
|
||||||
|
print(f" Name: {name}, Department: {department}")
|
||||||
|
else:
|
||||||
|
print(f"✗ Registration failed: {resp.get('error')}")
|
||||||
|
|
||||||
|
|
||||||
|
def elgamal_sign(eg_private, msg_bytes):
|
||||||
|
"""Sign message with ElGamal."""
|
||||||
|
p = int(eg_private["p"])
|
||||||
|
g = int(eg_private["g"])
|
||||||
|
x = int(eg_private["x"])
|
||||||
|
|
||||||
|
H = int.from_bytes(MD5.new(msg_bytes).digest(), "big") % (p - 1)
|
||||||
|
while True:
|
||||||
|
k = random.randint(2, p - 2)
|
||||||
|
if GCD(k, p - 1) == 1:
|
||||||
|
break
|
||||||
|
|
||||||
|
r = pow(g, k, p)
|
||||||
|
kinv = inverse(k, p - 1)
|
||||||
|
s = (kinv * (H - x * r)) % (p - 1)
|
||||||
|
return int(r), int(s)
|
||||||
|
|
||||||
|
|
||||||
|
def submit_report(state):
|
||||||
|
"""Submit a medical report (encrypted with AES, key encrypted with RSA-OAEP)."""
|
||||||
|
if not state["doctor_id"]:
|
||||||
|
print("Register as doctor first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
ensure_dirs()
|
||||||
|
files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith(".md")]
|
||||||
|
if not files:
|
||||||
|
print("Place markdown files in inputdata/")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\nAvailable files:")
|
||||||
|
for i, f in enumerate(files, 1):
|
||||||
|
print(f" {i}. {f}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
idx = int(input("Select file #: ").strip()) - 1
|
||||||
|
filename = files[idx]
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
print("Invalid selection.")
|
||||||
|
return
|
||||||
|
|
||||||
|
filepath = os.path.join(INPUT_DIR, filename)
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
report_bytes = f.read()
|
||||||
|
|
||||||
|
timestamp = datetime.now(timezone.utc).isoformat()
|
||||||
|
md5_hex = hashlib.md5(report_bytes).hexdigest()
|
||||||
|
|
||||||
|
# Sign report
|
||||||
|
msg_to_sign = report_bytes + timestamp.encode()
|
||||||
|
r, s = elgamal_sign(state["elgamal"], msg_to_sign)
|
||||||
|
|
||||||
|
# Encrypt report with AES-256-EAX
|
||||||
|
aes_key = get_random_bytes(32)
|
||||||
|
cipher = AES.new(aes_key, AES.MODE_EAX)
|
||||||
|
ciphertext, tag = cipher.encrypt_and_digest(report_bytes)
|
||||||
|
|
||||||
|
# Encrypt AES key with RSA-OAEP
|
||||||
|
rsa_pub_pem = state["server_keys"]["rsa_pub_pem_b64"]
|
||||||
|
rsa_pub = RSA.import_key(b64d(rsa_pub_pem))
|
||||||
|
rsa_cipher = PKCS1_OAEP.new(rsa_pub)
|
||||||
|
encrypted_aes_key = rsa_cipher.encrypt(aes_key)
|
||||||
|
|
||||||
|
# Prepare request
|
||||||
|
body = {
|
||||||
|
"doctor_id": state["doctor_id"],
|
||||||
|
"filename": filename,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"md5_hex": md5_hex,
|
||||||
|
"sig": {"r": r, "s": s},
|
||||||
|
"aes": {
|
||||||
|
"key_rsa_oaep_b64": b64e(encrypted_aes_key),
|
||||||
|
"nonce_b64": b64e(cipher.nonce),
|
||||||
|
"tag_b64": b64e(tag),
|
||||||
|
"ct_b64": b64e(ciphertext),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = send_request("upload_report", "doctor", body)
|
||||||
|
if resp.get("status") == "ok":
|
||||||
|
print(f"✓ Report '{filename}' uploaded successfully.")
|
||||||
|
print(f" MD5: {md5_hex}")
|
||||||
|
print(f" Timestamp: {timestamp}")
|
||||||
|
else:
|
||||||
|
print(f"✗ Upload failed: {resp.get('error')}")
|
||||||
|
|
||||||
|
|
||||||
|
def homo_rsa_encrypt_amount(state, amount):
|
||||||
|
"""Encrypt amount using homomorphic RSA."""
|
||||||
|
if amount < 0 or amount > 100000:
|
||||||
|
print("Amount must be 0-100000.")
|
||||||
return None
|
return None
|
||||||
return resp.get("data") if "data" in resp else {}
|
|
||||||
|
|
||||||
def get_public_info():
|
n = int(state["server_keys"]["rsa_n"])
|
||||||
global server_info, cached_paillier_pub
|
e = int(state["server_keys"]["rsa_e"])
|
||||||
data = send_request("get result:")
|
g = int(state["server_keys"]["rsa_homo_g"])
|
||||||
if data is None:
|
|
||||||
|
|
||||||
def rs
|
# Encrypt: c = g^amount)^e mod n
|
||||||
|
m = pow(g, amount, n)
|
||||||
|
c = pow(m, e, n)
|
||||||
|
return int(c)
|
||||||
|
|
||||||
|
|
||||||
|
def submit_expense(state):
|
||||||
|
"""Submit an encrypted expense."""
|
||||||
|
if not state["doctor_id"]:
|
||||||
|
print("Register as doctor first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not state["server_keys"]:
|
||||||
|
print("Fetch server keys first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
amount = int(input("Expense amount (integer, 0-100000): ").strip())
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid amount.")
|
||||||
|
return
|
||||||
|
|
||||||
|
ciphertext = homo_rsa_encrypt_amount(state, amount)
|
||||||
|
if ciphertext is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
body = {"doctor_id": state["doctor_id"], "amount_ciphertext": str(ciphertext)}
|
||||||
|
|
||||||
|
resp = send_request("submit_expense", "doctor", body)
|
||||||
|
if resp.get("status") == "ok":
|
||||||
|
print(f"✓ Expense encrypted and submitted.")
|
||||||
|
print(f" Amount: {amount}")
|
||||||
|
print(f" Ciphertext: {ciphertext}")
|
||||||
|
else:
|
||||||
|
print(f"✗ Submission failed: {resp.get('error')}")
|
||||||
|
|
||||||
|
|
||||||
|
def doctor_menu(state):
|
||||||
|
"""Doctor submenu."""
|
||||||
|
while True:
|
||||||
|
print("\n=== Doctor Menu ===")
|
||||||
|
print("1. Register with server")
|
||||||
|
print("2. Fetch server keys")
|
||||||
|
print("3. Submit report (encrypted)")
|
||||||
|
print("4. Submit expense (homomorphic RSA)")
|
||||||
|
print("5. Show current doctor ID")
|
||||||
|
print("0. Back")
|
||||||
|
|
||||||
|
ch = input("Choice: ").strip()
|
||||||
|
if ch == "1":
|
||||||
|
register_doctor_client(state)
|
||||||
|
elif ch == "2":
|
||||||
|
fetch_server_keys(state)
|
||||||
|
elif ch == "3":
|
||||||
|
submit_report(state)
|
||||||
|
elif ch == "4":
|
||||||
|
submit_expense(state)
|
||||||
|
elif ch == "5":
|
||||||
|
doc_id = state.get("doctor_id")
|
||||||
|
if doc_id:
|
||||||
|
print(f"Current doctor ID: {doc_id}")
|
||||||
|
else:
|
||||||
|
print("Not registered.")
|
||||||
|
elif ch == "0":
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Invalid choice.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ensure_dirs()
|
||||||
|
state = load_client_state()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n=== Medical Records Client ===")
|
||||||
|
print("1. Doctor operations")
|
||||||
|
print("0. Exit")
|
||||||
|
|
||||||
|
ch = input("Choice: ").strip()
|
||||||
|
if ch == "1":
|
||||||
|
doctor_menu(state)
|
||||||
|
elif ch == "0":
|
||||||
|
print("Goodbye!")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Invalid choice.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
|
||||||
|
|
@ -1,407 +0,0 @@
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
import socketserver
|
|
||||||
import base64
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from Crypto.Cipher import PKCS1_OAEP, AES
|
|
||||||
from Crypto.Random import get_random_bytes
|
|
||||||
from Crypto.Util.number import GCD
|
|
||||||
from phe import paillier
|
|
||||||
|
|
||||||
DATA_DIR = server_data
|
|
||||||
DOCTORS_FILE = os.path.join(DATA_DIR, doctors.json)
|
|
||||||
EXPENSES_FILE = os.path.join(DATA_DIR, expenses.json)
|
|
||||||
REPORTS_FILE = os.path.join(DATA_DIR, reports.json)
|
|
||||||
CONF_FILE = os.path.join(DATA_DIR, config.json)
|
|
||||||
RSA_PRIV_FILE = os.path.join(DATA_DIR, server_rsa_priv.pem)
|
|
||||||
RSA_PUB_FILE = os.path.join(DATA_DIR, server_rsa_pub.pem)
|
|
||||||
PORT = 5000
|
|
||||||
|
|
||||||
lock = threading.Lock()
|
|
||||||
|
|
||||||
def ensure_dirs():
|
|
||||||
os.makedirs(DATA_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
def read_json(path, default):
|
|
||||||
if not os.path.exists(path):
|
|
||||||
return default
|
|
||||||
with open(path, r) as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def write_json(path, obj):
|
|
||||||
tmp = path + .tmp
|
|
||||||
with open(tmp, w) as f:
|
|
||||||
json.dump(obj, f, indent=2)
|
|
||||||
os.replace(tmp, path)
|
|
||||||
|
|
||||||
def load_or_create_rsa():
|
|
||||||
if not os.path.exists(RSA_PRIV_FILE):
|
|
||||||
key = RSA.generate(2048)
|
|
||||||
with open(RSA_PRIV_FILE, wb) as f:
|
|
||||||
f.write(key.export_key())
|
|
||||||
with open(RSA_PUB_FILE, wb) as f:
|
|
||||||
f.write(key.public_key().export_key())
|
|
||||||
with open(RSA_PRIV_FILE, rb) as f:
|
|
||||||
priv = RSA.import_key(f.read())
|
|
||||||
with open(RSA_PUB_FILE, rb) as f:
|
|
||||||
pub = RSA.import_key(f.read())
|
|
||||||
return priv, pub
|
|
||||||
|
|
||||||
def load_or_create_paillier():
|
|
||||||
conf = read_json(CONF_FILE, {})
|
|
||||||
if paillier not in conf:
|
|
||||||
pubkey, privkey = paillier.generate_paillier_keypair()
|
|
||||||
conf[paillier] = {
|
|
||||||
n: str(pubkey.n),
|
|
||||||
p: str(privkey.p),
|
|
||||||
q: str(privkey.q),
|
|
||||||
}
|
|
||||||
write_json(CONF_FILE, conf)
|
|
||||||
conf = read_json(CONF_FILE, {})
|
|
||||||
n = int(conf[paillier][n])
|
|
||||||
p = int(conf[paillier][p])
|
|
||||||
q = int(conf[paillier][q])
|
|
||||||
pubkey = paillier.PaillierPublicKey(n)
|
|
||||||
privkey = paillier.PaillierPrivateKey(pubkey, p, q)
|
|
||||||
return pubkey, privkey
|
|
||||||
|
|
||||||
def load_or_create_config_rsa_homomorphic_base(rsa_pub):
|
|
||||||
conf = read_json(CONF_FILE, {})
|
|
||||||
n = rsa_pub.n
|
|
||||||
if rsa_homomorphic not in conf:
|
|
||||||
# pick base g coprime to n
|
|
||||||
import random
|
|
||||||
while True:
|
|
||||||
g = random.randrange(2, n - 1)
|
|
||||||
if GCD(g, n) == 1:
|
|
||||||
break
|
|
||||||
conf[rsa_homomorphic] = {
|
|
||||||
g: str(g)
|
|
||||||
}
|
|
||||||
write_json(CONF_FILE, conf)
|
|
||||||
conf = read_json(CONF_FILE, {})
|
|
||||||
g = int(conf[rsa_homomorphic][g])
|
|
||||||
return g
|
|
||||||
|
|
||||||
def b64e(b: bytes) -> str:
|
|
||||||
return base64.b64encode(b).decode()
|
|
||||||
|
|
||||||
def b64d(s: str) -> bytes:
|
|
||||||
return base64.b64decode(s.encode())
|
|
||||||
|
|
||||||
def init_storage():
|
|
||||||
ensure_dirs()
|
|
||||||
priv, pub = load_or_create_rsa()
|
|
||||||
_ = load_or_create_paillier()
|
|
||||||
if not os.path.exists(DOCTORS_FILE):
|
|
||||||
write_json(DOCTORS_FILE, {})
|
|
||||||
if not os.path.exists(EXPENSES_FILE):
|
|
||||||
write_json(EXPENSES_FILE, [])
|
|
||||||
if not os.path.exists(REPORTS_FILE):
|
|
||||||
write_json(REPORTS_FILE, [])
|
|
||||||
return priv, pub
|
|
||||||
|
|
||||||
RSA_PRIV, RSA_PUB = init_storage()
|
|
||||||
PAI_PUB, PAI_PRIV = load_or_create_paillier()
|
|
||||||
RSA_HOMO_G = load_or_create_config_rsa_homomorphic_base(RSA_PUB)
|
|
||||||
|
|
||||||
def get_public_info():
|
|
||||||
return {
|
|
||||||
rsa_pub_pem_b64: b64e(RSA_PUB.export_key()),
|
|
||||||
rsa_n: str(RSA_PUB.n),
|
|
||||||
rsa_e: str(RSA_PUB.e),
|
|
||||||
paillier_n: str(PAI_PUB.n),
|
|
||||||
rsa_homo_g: str(RSA_HOMO_G),
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_register_doctor(body):
|
|
||||||
# body: {doctor_id, department_plain, dept_enc: {ciphertext, exponent}, elgamal_pub: {p,g,y}}
|
|
||||||
doc_id = body.get(doctor_id,).strip()
|
|
||||||
dept_plain = body.get(department_plain,).strip()
|
|
||||||
dept_enc = body.get(dept_enc)
|
|
||||||
elgamal_pub = body.get(elgamal_pub)
|
|
||||||
if not doc_id or not doc_id.isalnum():
|
|
||||||
return {status:error,error:invalid doctor_id}
|
|
||||||
if not dept_plain:
|
|
||||||
return {status:error,error:invalid department}
|
|
||||||
if not dept_enc or ciphertext not in dept_enc or exponent not in dept_enc:
|
|
||||||
return {status:error,error:invalid dept_enc}
|
|
||||||
if not elgamal_pub or not all(k in elgamal_pub for k in [p,g,y]):
|
|
||||||
return {status:error,error:missing elgamal_pub}
|
|
||||||
|
|
||||||
with lock:
|
|
||||||
doctors = read_json(DOCTORS_FILE, {})
|
|
||||||
doctors[doc_id] = {
|
|
||||||
department_plain: dept_plain,
|
|
||||||
dept_enc: {
|
|
||||||
ciphertext: str(int(dept_enc[ciphertext])),
|
|
||||||
exponent: int(dept_enc[exponent])
|
|
||||||
},
|
|
||||||
elgamal_pub: {
|
|
||||||
p: str(int(elgamal_pub[p])),
|
|
||||||
g: str(int(elgamal_pub[g])),
|
|
||||||
y: str(int(elgamal_pub[y]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
write_json(DOCTORS_FILE, doctors)
|
|
||||||
print(f[server] registered doctor {doc_id} dept='{dept_plain}' (stored encrypted and plaintext))
|
|
||||||
return {status:ok}
|
|
||||||
|
|
||||||
def handle_upload_report(body):
|
|
||||||
# body: {doctor_id, filename, timestamp, md5_hex, sig: {r,s}, aes: {key_rsa_oaep_b64, nonce_b64, tag_b64, ct_b64}}
|
|
||||||
doc_id = body.get(doctor_id,).strip()
|
|
||||||
filename = os.path.basename(body.get(filename,).strip())
|
|
||||||
timestamp = body.get(timestamp,).strip()
|
|
||||||
md5_hex = body.get(md5_hex,).strip()
|
|
||||||
sig = body.get(sig)
|
|
||||||
aes = body.get(aes)
|
|
||||||
if not doc_id or not filename or not timestamp or not md5_hex or not sig or not aes:
|
|
||||||
return {status:error,error:missing fields}
|
|
||||||
|
|
||||||
with lock:
|
|
||||||
doctors = read_json(DOCTORS_FILE, {})
|
|
||||||
if doc_id not in doctors:
|
|
||||||
return {status:error,error:unknown doctor_id}
|
|
||||||
|
|
||||||
# decrypt AES key
|
|
||||||
try:
|
|
||||||
rsa_cipher = PKCS1_OAEP.new(RSA_PRIV)
|
|
||||||
aes_key = rsa_cipher.decrypt(b64d(aes[key_rsa_oaep_b64]))
|
|
||||||
nonce = b64d(aes[nonce_b64])
|
|
||||||
tag = b64d(aes[tag_b64])
|
|
||||||
ct = b64d(aes[ct_b64])
|
|
||||||
aes_cipher = AES.new(aes_key, AES.MODE_EAX, nonce=nonce)
|
|
||||||
report_bytes = aes_cipher.decrypt_and_verify(ct, tag)
|
|
||||||
except Exception as e:
|
|
||||||
return {status:error,error:faes/rsa decrypt failed: {e}}
|
|
||||||
|
|
||||||
# verify MD5
|
|
||||||
import hashlib
|
|
||||||
md5_check = hashlib.md5(report_bytes).hexdigest()
|
|
||||||
if md5_check != md5_hex:
|
|
||||||
print([server] md5 mismatch)
|
|
||||||
# store file
|
|
||||||
outdir = os.path.join(DATA_DIR, reports)
|
|
||||||
os.makedirs(outdir, exist_ok=True)
|
|
||||||
savepath = os.path.join(outdir, f{doc_id}_{int(time.time())}_{filename})
|
|
||||||
with open(savepath, wb) as f:
|
|
||||||
f.write(report_bytes)
|
|
||||||
|
|
||||||
# store record
|
|
||||||
rec = {
|
|
||||||
doctor_id: doc_id,
|
|
||||||
filename: filename,
|
|
||||||
saved_path: savepath,
|
|
||||||
timestamp: timestamp,
|
|
||||||
md5_hex: md5_hex,
|
|
||||||
sig: {r: str(int(sig[r])), s: str(int(sig[s]))}
|
|
||||||
}
|
|
||||||
with lock:
|
|
||||||
records = read_json(REPORTS_FILE, [])
|
|
||||||
records.append(rec)
|
|
||||||
write_json(REPORTS_FILE, records)
|
|
||||||
print(f[server] report uploaded by {doc_id}, stored {savepath})
|
|
||||||
return {status:ok}
|
|
||||||
|
|
||||||
def handle_submit_expense(body):
|
|
||||||
# body: {doctor_id, amount_ciphertext}
|
|
||||||
doc_id = body.get(doctor_id,).strip()
|
|
||||||
c = body.get(amount_ciphertext)
|
|
||||||
if not doc_id or not doc_id.isalnum():
|
|
||||||
return {status:error,error:invalid doctor_id}
|
|
||||||
try:
|
|
||||||
c_int = int(c)
|
|
||||||
except:
|
|
||||||
return {status:error,error:invalid ciphertext}
|
|
||||||
with lock:
|
|
||||||
doctors = read_json(DOCTORS_FILE, {})
|
|
||||||
if doc_id not in doctors:
|
|
||||||
return {status:error,error:unknown doctor_id}
|
|
||||||
|
|
||||||
with lock:
|
|
||||||
expenses = read_json(EXPENSES_FILE, [])
|
|
||||||
expenses.append({doctor_id: doc_id, ciphertext: str(c_int)})
|
|
||||||
write_json(EXPENSES_FILE, expenses)
|
|
||||||
print(f[server] expense ciphertext stored for {doc_id})
|
|
||||||
return {status:ok}
|
|
||||||
|
|
||||||
class RequestHandler(socketserver.StreamRequestHandler):
|
|
||||||
def handle(self):
|
|
||||||
try:
|
|
||||||
data = self.rfile.readline()
|
|
||||||
if not data:
|
|
||||||
return
|
|
||||||
req = json.loads(data.decode())
|
|
||||||
action = req.get(action)
|
|
||||||
role = req.get(role, )
|
|
||||||
body = req.get(body, {})
|
|
||||||
if action == get_public_info:
|
|
||||||
resp = {status:ok,data: get_public_info()}
|
|
||||||
elif action == register_doctor:
|
|
||||||
if role != doctor:
|
|
||||||
resp = {status:error,error:unauthorized}
|
|
||||||
else:
|
|
||||||
resp = handle_register_doctor(body)
|
|
||||||
elif action == upload_report:
|
|
||||||
if role != doctor:
|
|
||||||
resp = {status:error,error:unauthorized}
|
|
||||||
else:
|
|
||||||
resp = handle_upload_report(body)
|
|
||||||
elif action == submit_expense:
|
|
||||||
if role != doctor:
|
|
||||||
resp = {status:error,error:unauthorized}
|
|
||||||
else:
|
|
||||||
resp = handle_submit_expense(body)
|
|
||||||
else:
|
|
||||||
resp = {status:error,error:unknown action}
|
|
||||||
except Exception as e:
|
|
||||||
resp = {status:error,error:str(e)}
|
|
||||||
self.wfile.write((json.dumps(resp)+\n).encode())
|
|
||||||
|
|
||||||
def start_server():
|
|
||||||
server = socketserver.ThreadingTCPServer((127.0.0.1, PORT), RequestHandler)
|
|
||||||
t = threading.Thread(target=server.serve_forever, daemon=True)
|
|
||||||
t.start()
|
|
||||||
print(f[server] listening on 127.0.0.1:{PORT})
|
|
||||||
return server
|
|
||||||
|
|
||||||
# Auditor utilities
|
|
||||||
|
|
||||||
def load_doctors():
|
|
||||||
return read_json(DOCTORS_FILE, {})
|
|
||||||
|
|
||||||
def load_expenses():
|
|
||||||
return read_json(EXPENSES_FILE, [])
|
|
||||||
|
|
||||||
def load_reports():
|
|
||||||
return read_json(REPORTS_FILE, [])
|
|
||||||
|
|
||||||
def audit_list_doctors():
|
|
||||||
docs = load_doctors()
|
|
||||||
print(Doctors:)
|
|
||||||
for did, info in docs.items():
|
|
||||||
enc = info[dept_enc]
|
|
||||||
print(f- {did} dept_plain='{info['department_plain']}' enc_ciphertext={enc['ciphertext']} exponent={enc['exponent']})
|
|
||||||
|
|
||||||
def audit_keyword_search():
|
|
||||||
docs = load_doctors()
|
|
||||||
if not docs:
|
|
||||||
print(no doctors)
|
|
||||||
return
|
|
||||||
q = input(Enter department keyword to search: ).strip()
|
|
||||||
if not q:
|
|
||||||
print(empty)
|
|
||||||
return
|
|
||||||
# hash to int
|
|
||||||
import hashlib
|
|
||||||
h = int.from_bytes(hashlib.sha256(q.encode()).digest(), big)
|
|
||||||
pub = PAI_PUB
|
|
||||||
priv = PAI_PRIV
|
|
||||||
enc_q = pub.encrypt(h)
|
|
||||||
print(Matching doctors (using Paillier equality on hashed dept):)
|
|
||||||
for did, info in docs.items():
|
|
||||||
enc = info[dept_enc]
|
|
||||||
c = int(enc[ciphertext])
|
|
||||||
exp = int(enc[exponent])
|
|
||||||
enc_doc = paillier.EncryptedNumber(pub, c, exp)
|
|
||||||
diff = enc_doc - enc_q
|
|
||||||
dec = priv.decrypt(diff)
|
|
||||||
match = (dec == 0)
|
|
||||||
print(f {did}: dept_plain='{info['department_plain']}' enc_ciphertext={c} match={match})
|
|
||||||
|
|
||||||
def rsa_homo_decrypt_sum(c_prod_int):
|
|
||||||
n = RSA_PRIV.n
|
|
||||||
d = RSA_PRIV.d
|
|
||||||
g = RSA_HOMO_G
|
|
||||||
# decrypt to get g^sum mod n
|
|
||||||
m = pow(int(c_prod_int), d, n)
|
|
||||||
# brute force discrete log for small sums
|
|
||||||
max_iter = 500000
|
|
||||||
acc = 1
|
|
||||||
for k in range(0, max_iter+1):
|
|
||||||
if acc == m:
|
|
||||||
return k
|
|
||||||
acc = (acc * g) % n
|
|
||||||
return None
|
|
||||||
|
|
||||||
def audit_sum_expenses():
|
|
||||||
exps = load_expenses()
|
|
||||||
if not exps:
|
|
||||||
print(no expenses)
|
|
||||||
return
|
|
||||||
# sum all
|
|
||||||
n = RSA_PUB.n
|
|
||||||
c_prod = 1
|
|
||||||
for e in exps:
|
|
||||||
c_prod = (c_prod * int(e[ciphertext])) % n
|
|
||||||
print(fProduct ciphertext (represents sum under RSA-in-exponent): {c_prod})
|
|
||||||
s = rsa_homo_decrypt_sum(c_prod)
|
|
||||||
if s is None:
|
|
||||||
print(sum decryption failed (exceeded search bound))
|
|
||||||
else:
|
|
||||||
print(fDecrypted sum of expenses = {s})
|
|
||||||
# by doctor
|
|
||||||
docs = load_doctors()
|
|
||||||
if docs:
|
|
||||||
print(Per-doctor sums:)
|
|
||||||
for did in docs.keys():
|
|
||||||
c_prod_d = 1
|
|
||||||
count = 0
|
|
||||||
for e in exps:
|
|
||||||
if e[doctor_id] == did:
|
|
||||||
c_prod_d = (c_prod_d * int(e[ciphertext])) % n
|
|
||||||
count += 1
|
|
||||||
if count == 0:
|
|
||||||
continue
|
|
||||||
s_d = rsa_homo_decrypt_sum(c_prod_d)
|
|
||||||
print(f {did}: entries={count} product_ct={c_prod_d} sum={s_d})
|
|
||||||
|
|
||||||
def elgamal_verify(p, g, y, H_int, r, s):
|
|
||||||
# verify: g^H ≡ y^r * r^s (mod p)
|
|
||||||
return pow(g, H_int, p) == (pow(y, r, p) * pow(r, s, p)) % p
|
|
||||||
|
|
||||||
def audit_verify_reports():
|
|
||||||
records = load_reports()
|
|
||||||
if not records:
|
|
||||||
print(no reports)
|
|
||||||
return
|
|
||||||
doctors = load_doctors()
|
|
||||||
for rec in records:
|
|
||||||
did = rec[doctor_id]
|
|
||||||
docinfo = doctors.get(did)
|
|
||||||
ok_sig = False
|
|
||||||
ok_ts = False
|
|
||||||
if docinfo:
|
|
||||||
p = int(docinfo[elgamal_pub][p])
|
|
||||||
g = int(docinfo[elgamal_pub][g])
|
|
||||||
y = int(docinfo[elgamal_pub][y])
|
|
||||||
r = int(rec[sig][r])
|
|
||||||
s = int(rec[sig][s])
|
|
||||||
try:
|
|
||||||
with open(rec[saved_path], rb) as f:
|
|
||||||
report_bytes = f.read()
|
|
||||||
import hashlib
|
|
||||||
H = int.from_bytes(hashlib.md5(report_bytes + rec[timestamp].encode()).digest(), big) % (p - 1)
|
|
||||||
ok_sig = elgamal_verify(p, g, y, H, r, s)
|
|
||||||
except Exception as e:
|
|
||||||
ok_sig = False
|
|
||||||
# timestamp check
|
|
||||||
try:
|
|
||||||
ts = datetime.fromisoformat(rec[timestamp])
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
ts = datetime.strptime(rec[timestamp], %Y-%m-%dT%H:%M:%S.%f)
|
|
||||||
except:
|
|
||||||
ts = None
|
|
||||||
if ts:
|
|
||||||
now = datetime.utcnow().replace(tzinfo=None)
|
|
||||||
delta = (now - ts).total_seconds()
|
|
||||||
# simple rule: not in the future by more than 5 min
|
|
||||||
ok_ts = (delta >= -300)
|
|
||||||
print(f- report by {did} file={os.path.basename(rec['saved_path'])} sig_ok={ok_sig} ts_ok={ok_ts} ts={rec['timestamp']} md5={rec['md5_hex']})
|
|
||||||
|
|
||||||
def auditor_menu(): while True: print(\n[Auditor Menu]) print(1) List doctors (show encrypted and plaintext dept)) print(2) Keyword search doctors by dept (Paillier)) print(3) Sum expenses (RSA-in-exponent demo)) print(4) Verify reports and timestamps) print(5) Show server public info) print(0) Exit) ch = input(Select: ).strip() if ch == 1: audit_list_doctors() elif ch == 2: audit_keyword_search() elif ch == 3: audit_sum_expenses() elif ch == 4: audit_verify_reports() elif ch == 5: info = get_public_info() print(json.dumps(info, indent=2)) elif ch == 0: print(bye) break else: print(invalid) if __name__ == __main__: start_server() auditor_menu()
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue