"""Public-key directory for NPE/HPKE endpoint encryption. Only *public* material is listed. Private keys stay in private/ (0600). Every service that needs E2E looks up handles here (HTTP or files). """ from __future__ import annotations # annotations import json # records import os # chmod import time # updated_ts from pathlib import Path # dir root from typing import Any, Dict, List, Optional # types from .hpke import generate_keypair # X25519 class PubKeyDir: """File-backed directory: public/.json + private/.sk.""" def __init__(self, root: Path): self.root = Path(root) # /opt/pfc/etc/npe-dir self.public = self.root / "public" self.private = self.root / "private" self.public.mkdir(parents=True, exist_ok=True) self.private.mkdir(parents=True, exist_ok=True) def create_endpoint(self, handle: str) -> Dict[str, Any]: """Generate X25519 keypair; write public record + private sk.""" handle = _safe(handle) sk, pk = generate_keypair() rec = { "handle": handle, "kem": "x25519", "kdf": "hkdf-sha256", "aead": "chacha20poly1305", "suite": "HPKE-Base X25519-HKDF-SHA256-ChaCha20-Poly1305", "enc_pk": pk.hex(), "inbox": "npe.inbox." + pk[:16].hex(), "updated_ts": time.time(), } (self.public / (handle + ".json")).write_text(json.dumps(rec, indent=2) + "\n") skp = self.private / (handle + ".sk") skp.write_bytes(sk) os.chmod(skp, 0o600) return rec def get(self, handle: str) -> Optional[Dict[str, Any]]: p = self.public / (_safe(handle) + ".json") if not p.exists(): return None return json.loads(p.read_text()) def list(self) -> List[Dict[str, Any]]: rows = [] for p in sorted(self.public.glob("*.json")): rows.append(json.loads(p.read_text())) return rows def put_public(self, rec: Dict[str, Any]) -> Dict[str, Any]: """Publish someone else's public key (no private).""" handle = _safe(str(rec.get("handle") or "")) if not handle or not rec.get("enc_pk"): raise ValueError("handle and enc_pk required") rec = dict(rec) rec["handle"] = handle rec["updated_ts"] = time.time() (self.public / (handle + ".json")).write_text(json.dumps(rec, indent=2) + "\n") return rec def load_sk(self, handle: str) -> bytes: p = self.private / (_safe(handle) + ".sk") if not p.exists(): raise FileNotFoundError("no private key for " + handle) return p.read_bytes() def available(self) -> Dict[str, Any]: """What every E2E service can see: handles + public keys only.""" recs = self.list() return { "ok": True, "directory": str(self.root), "suite": "HPKE-Base X25519-HKDF-SHA256-ChaCha20-Poly1305", "handles": [r["handle"] for r in recs], "keys": recs, } def _safe(handle: str) -> str: h = "".join(c for c in handle if c.isalnum() or c in "-_") if not h: raise ValueError("empty handle") return h