From a55d40b74bda77dff6ec565abdd551607fc665d6 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 30 Sep 2026 13:55:59 +0200 Subject: feat(hub): a bundle pepper per account, handed only to a proven session Sealed at rest and bound to the account; returned by sign-in, device sign-in, a passphrase change and GET /me/bundle-pepper, never by a refresh, to a node token, in a token or in a log. Erasure clears it. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-hub/src/meshbay_hub/auth.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/auth.py') diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index 0d0fd95..1d09581 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -324,6 +324,28 @@ def decrypt_email(stored: str) -> str: return AESGCM(_email_key).decrypt(nonce, ct, None).decode() +def seal_pepper(raw: bytes, user_id: str) -> str: + """Seal a bundle pepper for storage, bound to its account. + + The same at-rest key as the e-mail, with the account id and a purpose as + associated data: a sealed value copied into another row, or into the e-mail + column, does not open. + """ + if _email_key is None: + raise RuntimeError("Hub keypair not loaded") + nonce = os.urandom(12) + aad = f"meshbay:bundle_pepper:{user_id}".encode() + return base64.b64encode(nonce + AESGCM(_email_key).encrypt(nonce, raw, aad)).decode() + + +def open_pepper(stored: str, user_id: str) -> bytes: + if _email_key is None: + raise RuntimeError("Hub keypair not loaded") + raw = base64.b64decode(stored) + aad = f"meshbay:bundle_pepper:{user_id}".encode() + return AESGCM(_email_key).decrypt(raw[:12], raw[12:], aad) + + def hash_email_blind(email: str) -> str: """Deterministic HMAC-SHA256 of the lowercased email for uniqueness checks. -- cgit v1.2.3