"""Short-lived Wormhole sender tokens for DM metadata reduction. These tokens let the client send a sealed DM without placing the long-term sender id and public key directly into the DM send request body. The token is single-use, recipient-bound, and kept in memory only. """ from __future__ import annotations import hashlib import secrets import time from typing import Any from cachetools import TTLCache from services.mesh.mesh_wormhole_identity import bootstrap_wormhole_identity, read_wormhole_identity from services.mesh.mesh_protocol import PROTOCOL_VERSION _SENDER_TOKEN_TTL_S = 2 * 60 _sender_tokens: TTLCache[str, dict[str, Any]] = TTLCache(maxsize=2048, ttl=_SENDER_TOKEN_TTL_S) def _sender_token_hash(token: str) -> str: return hashlib.sha256((token or "").encode("utf-8")).hexdigest() def _token_binding_hash(recipient_token: str) -> str: return hashlib.sha256((recipient_token or "").encode("utf-8")).hexdigest() def _sender_token_ttl_seconds(delivery_class: str, ttl_seconds: int) -> int: requested = int(ttl_seconds or _SENDER_TOKEN_TTL_S) delivery = str(delivery_class or "").strip().lower() maximum = _SENDER_TOKEN_TTL_S if delivery == "request": maximum = min(maximum, 90) return max(30, min(requested, maximum)) def issue_wormhole_dm_sender_token( *, recipient_id: str, delivery_class: str, recipient_token: str = "", ttl_seconds: int = _SENDER_TOKEN_TTL_S, ) -> dict[str, Any]: recipient_id = str(recipient_id or "").strip() delivery_class = str(delivery_class or "").strip().lower() if delivery_class not in ("request", "shared"): return {"ok": False, "detail": "Invalid delivery_class"} if not recipient_id: return {"ok": False, "detail": "recipient_id required"} if delivery_class == "shared" and not recipient_token: return {"ok": False, "detail": "recipient_token required for shared delivery"} data = read_wormhole_identity() if not data.get("bootstrapped"): bootstrap_wormhole_identity() data = read_wormhole_identity() if not data.get("node_id") or not data.get("public_key"): return {"ok": False, "detail": "Wormhole identity unavailable"} token = secrets.token_urlsafe(32) now = int(time.time()) expires_at = now + _sender_token_ttl_seconds(delivery_class, ttl_seconds) _sender_tokens[token] = { "sender_id": str(data.get("node_id", "")), "public_key": str(data.get("public_key", "")), "public_key_algo": str(data.get("public_key_algo", "Ed25519") or "Ed25519"), "protocol_version": PROTOCOL_VERSION, "recipient_id": recipient_id, "delivery_class": delivery_class, "recipient_token_hash": _token_binding_hash(recipient_token), "issued_at": now, "expires_at": expires_at, } return { "ok": True, "sender_token": token, "expires_at": expires_at, "delivery_class": delivery_class, } def issue_wormhole_dm_sender_tokens( *, recipient_id: str, delivery_class: str, recipient_token: str = "", count: int = 3, ttl_seconds: int = _SENDER_TOKEN_TTL_S, ) -> dict[str, Any]: token_count = max(1, min(int(count or 1), 4)) tokens: list[dict[str, Any]] = [] for _ in range(token_count): issued = issue_wormhole_dm_sender_token( recipient_id=recipient_id, delivery_class=delivery_class, recipient_token=recipient_token, ttl_seconds=ttl_seconds, ) if not issued.get("ok"): return issued tokens.append( { "sender_token": str(issued.get("sender_token", "")), "expires_at": int(issued.get("expires_at", 0) or 0), } ) return { "ok": True, "delivery_class": delivery_class, "tokens": tokens, } def consume_wormhole_dm_sender_token( *, sender_token: str, recipient_id: str, delivery_class: str, recipient_token: str = "", ) -> dict[str, Any]: token = str(sender_token or "").strip() if not token: return {"ok": False, "detail": "sender_token required"} token_hash = _sender_token_hash(token) record = _sender_tokens.pop(token, None) if not record: return {"ok": False, "detail": "sender_token invalid or expired"} bound_recipient_id = str(record.get("recipient_id", "") or "") normalized_recipient_id = str(recipient_id or "").strip() if normalized_recipient_id and bound_recipient_id != normalized_recipient_id: return {"ok": False, "detail": "sender_token recipient mismatch"} if str(record.get("delivery_class", "")) != str(delivery_class or "").strip().lower(): return {"ok": False, "detail": "sender_token delivery_class mismatch"} if str(record.get("recipient_token_hash", "")) != _token_binding_hash(recipient_token): return {"ok": False, "detail": "sender_token mailbox binding mismatch"} expires_at = int(record.get("expires_at", 0) or 0) if expires_at and expires_at < int(time.time()): return {"ok": False, "detail": "sender_token expired"} return {"ok": True, "sender_token_hash": token_hash, **record}