"""YahBible split-key auth -- sign in without Supabase. Model (Elan'iel's design): the PC is the authoritative SOURCE; Hugging Face and a GitHub repo each hold ONE half of an encrypted credential, so login still works when the laptop is offline. The password is the third factor and is never stored or sent in the clear. signup(user, password) -> account Ed25519 key (the user's Otaviel identity) + profile are AES-GCM encrypted under K = PBKDF2-HMAC-SHA256(password, salt, 600000). The ciphertext is split 2-of-2 by a one-time pad: half_HF = random pad (published to Hugging Face) half_GH = ciphertext XOR pad (published to the GitHub repo) Neither half alone reveals anything (OTP); BOTH are required to reconstruct the ciphertext, and the PASSWORD is still required to decrypt it. A verifier = HMAC(K, "yahbible-verify") lets a wrong password fail fast without touching the account key. login(password, hf_record, gh_record) -> reconstruct ciphertext = half_HF XOR half_GH, derive K, check the verifier, decrypt -> the account key + profile. No PC and no Supabase required. Cross-compatible with the browser: PBKDF2-HMAC-SHA256 + AES-GCM are both WebCrypto primitives, so shim/auth.js can run the identical scheme client-side. """ from __future__ import annotations import hashlib import hmac import json import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey PBKDF2_ITERS = 600_000 KDF_HASH = "sha256" def _kdf(password: str, salt: bytes) -> bytes: return hashlib.pbkdf2_hmac(KDF_HASH, password.encode("utf-8"), salt, PBKDF2_ITERS, 32) def _verifier(K: bytes) -> str: return hmac.new(K, b"yahbible-verify", hashlib.sha256).hexdigest() def _xor(a: bytes, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b)) def _uhash(user: str) -> str: """Public, stable record id -- the username is not stored in the clear at rest.""" return hashlib.sha256(("yahbible:user:" + user.strip().lower()).encode()).hexdigest()[:32] def signup(user: str, password: str, profile: dict | None = None) -> dict: """Create the split credential. Returns {hf, gh} records to publish to each host.""" salt = os.urandom(16) K = _kdf(password, salt) sk = Ed25519PrivateKey.generate() account = {"priv": sk.private_bytes_raw().hex(), "pub": sk.public_key().public_bytes_raw().hex(), "user": user, "profile": profile or {}} plaintext = json.dumps(account, separators=(",", ":")).encode() nonce = os.urandom(12) ct = nonce + AESGCM(K).encrypt(nonce, plaintext, _uhash(user).encode()) pad = os.urandom(len(ct)) # one-time pad -> 2-of-2 split half_hf, half_gh = pad, _xor(ct, pad) uh = _uhash(user) return { "uhash": uh, "pub": account["pub"], # Hugging Face: login info + salt + verifier + HF half (readable when PC is offline) "hf": {"uhash": uh, "salt": salt.hex(), "verifier": _verifier(K), "pub": account["pub"], "half": half_hf.hex(), "v": 1}, # GitHub: the encrypted other half (kept in the passwords repo) "gh": {"uhash": uh, "half": half_gh.hex(), "v": 1}, } def login(password: str, hf_record: dict, gh_record: dict) -> dict | None: """Reconstruct + decrypt the credential from the two halves. None on any failure.""" try: if not hf_record or not gh_record: return None # need BOTH halves (2-of-2) salt = bytes.fromhex(hf_record["salt"]) K = _kdf(password, salt) if not hmac.compare_digest(_verifier(K), hf_record.get("verifier", "")): return None # wrong password -> fail fast ct = _xor(bytes.fromhex(hf_record["half"]), bytes.fromhex(gh_record["half"])) nonce, body = ct[:12], ct[12:] pt = AESGCM(K).decrypt(nonce, body, hf_record["uhash"].encode()) acc = json.loads(pt) return {"user": acc["user"], "pub": acc["pub"], "priv": acc["priv"], "profile": acc.get("profile", {})} except Exception: return None def change_password(old_pw: str, new_pw: str, hf_record: dict, gh_record: dict) -> dict | None: """Re-wrap the same account key under a new password (keeps the identity).""" acc = login(old_pw, hf_record, gh_record) if not acc: return None salt = os.urandom(16) K = _kdf(new_pw, salt) plaintext = json.dumps( {"priv": acc["priv"], "pub": acc["pub"], "user": acc["user"], "profile": acc["profile"]}, separators=(",", ":")).encode() nonce = os.urandom(12) ct = nonce + AESGCM(K).encrypt(nonce, plaintext, hf_record["uhash"].encode()) pad = os.urandom(len(ct)) return { "uhash": hf_record["uhash"], "pub": acc["pub"], "hf": {"uhash": hf_record["uhash"], "salt": salt.hex(), "verifier": _verifier(K), "pub": acc["pub"], "half": pad.hex(), "v": 1}, "gh": {"uhash": hf_record["uhash"], "half": _xor(ct, pad).hex(), "v": 1}, }