feat: harden auth and study workflows

- replace plaintext login and unlock requests with RSA-OAEP/AES-GCM encrypted payloads

- add login challenge replay protection, production RSA key validation, and auth tests

- wire compose to environment-driven dev/prod settings without committing local secrets

- update setup-config smoke scripts and Postman docs for encrypted login

- add visit schedule migrations/tests and update study/subject setup workflows
This commit is contained in:
Cheng Zhou
2026-05-08 22:13:12 +08:00
parent a7bbcaa5dc
commit 74feca4467
47 changed files with 2423 additions and 534 deletions
+59 -4
View File
@@ -7,10 +7,65 @@ PASSWORD="${PASSWORD:-admin123}"
STUDY_ID="${STUDY_ID:-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa}"
echo "[1/6] login: $EMAIL"
TOKEN=$(curl -sS -X POST "$BASE_URL/api/v1/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" | \
python3 -c 'import json,sys; print(json.load(sys.stdin).get("access_token",""))')
TOKEN=$(BASE_URL="$BASE_URL" EMAIL="$EMAIL" PASSWORD="$PASSWORD" python3 - <<'PY'
import base64
import json
import os
import urllib.request
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
base_url = os.environ["BASE_URL"]
email = os.environ["EMAIL"]
password = os.environ["PASSWORD"]
with urllib.request.urlopen(f"{base_url}/api/v1/auth/login-key", timeout=20) as resp:
login_key = json.loads(resp.read().decode())
public_key = serialization.load_pem_public_key(login_key["public_key"].encode("utf-8"))
plaintext = json.dumps(
{"email": email, "password": password, "challenge": login_key["challenge"]},
separators=(",", ":"),
).encode("utf-8")
aes_key = AESGCM.generate_key(bit_length=256)
iv = os.urandom(12)
encrypted_data = AESGCM(aes_key).encrypt(iv, plaintext, None)
encrypted_key = public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
payload = json.dumps(
{
"key_id": login_key["key_id"],
"challenge": login_key["challenge"],
"ciphertext": base64.b64encode(
json.dumps(
{
"encrypted_key": base64.b64encode(encrypted_key).decode("ascii"),
"iv": base64.b64encode(iv).decode("ascii"),
"data": base64.b64encode(encrypted_data).decode("ascii"),
},
separators=(",", ":"),
).encode("utf-8")
).decode("ascii"),
}
).encode()
req = urllib.request.Request(
f"{base_url}/api/v1/auth/login",
method="POST",
headers={"Content-Type": "application/json"},
data=payload,
)
with urllib.request.urlopen(req, timeout=20) as resp:
print(json.loads(resp.read().decode()).get("access_token", ""))
PY
)
if [[ -z "$TOKEN" ]]; then
echo "login failed: access_token empty"