secure-messaging/python/secure_messaging/content.py
George Lambert 94919185a9
Some checks are pending
ci / python (push) Waiting to run
ci / go (push) Waiting to run
S27: HPKE-Base content wrap + public-key directory; reject xor on send
Routing/error fields stay clear. Content is X25519-HKDF-SHA256-ChaCha20.
2026-09-15 23:43:01 -04:00

63 lines
2.1 KiB
Python

"""NATS content wrapping: HPKE body, routing/error in the clear.
Clear (broker may see): subject, ``to``, ``from_lookup_id``, ``alg``,
error_code, dest_class, lookup_id.
Never clear: inner content JSON / PHI / object bytes.
"""
from __future__ import annotations # annotations
import json # inner body
import os # urandom
from typing import Any, Dict, Mapping, Optional # types
from .envelope import lookup_id # opaque sender handle
from .hpke import open_ct, seal # HPKE-Base
from .pubkey_dir import PubKeyDir # directory
def wrap_content(
*,
to_handle: str,
sender: str,
body: Mapping[str, Any],
directory: PubKeyDir,
) -> Dict[str, Any]:
"""Encrypt body to the recipient's directory public key."""
rec = directory.get(to_handle)
if not rec: # unknown endpoint
raise KeyError("directory has no public key for " + to_handle)
pk = bytes.fromhex(rec["enc_pk"])
inner = json.dumps(body, sort_keys=True, separators=(",", ":")).encode()
aad = ("to=" + to_handle).encode() # bind ciphertext to routing dest
enc, ct = seal(pk, inner, aad=aad)
nonce = os.urandom(16)
return {
"v": 1,
"alg": "npe",
"mode": "passthrough",
"to": rec.get("inbox") or to_handle, # routing in the clear
"to_handle": to_handle,
"from_lookup_id": lookup_id(sender, nonce),
"nonce": nonce.hex(),
"enc": enc.hex(), # HPKE encapsulated key (not content)
"ct": ct.hex(), # content ciphertext
}
def unwrap_content(env: Mapping[str, Any], *, handle: str, directory: PubKeyDir) -> Dict[str, Any]:
"""Open content as the named endpoint. Broker cannot do this."""
sk = directory.load_sk(handle)
to_handle = str(env.get("to_handle") or handle)
aad = ("to=" + to_handle).encode()
pt = open_ct(sk, bytes.fromhex(env["enc"]), bytes.fromhex(env["ct"]), aad=aad)
return json.loads(pt.decode())
def routing_view(env: Mapping[str, Any]) -> Dict[str, Any]:
"""What an untrusted router is allowed to look at."""
return {
k: env[k]
for k in ("v", "alg", "mode", "to", "to_handle", "from_lookup_id", "nonce")
if k in env
}