S27: HPKE-Base content wrap + public-key directory; reject xor on send
Routing/error fields stay clear. Content is X25519-HKDF-SHA256-ChaCha20.
This commit is contained in:
parent
9cdc64b185
commit
94919185a9
8 changed files with 364 additions and 13 deletions
93
python/secure_messaging/pubkey_dir.py
Normal file
93
python/secure_messaging/pubkey_dir.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""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/<handle>.json + private/<handle>.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
|
||||
Loading…
Add table
Add a link
Reference in a new issue