fix: cross-node gate decryption, UI text scaling, aircraft zoom

- Derive gate envelope AES key from gate ID via HKDF so all nodes
  sharing a gate can decrypt each other's messages (was node-local)
- Preserve gate_envelope/reply_to in chain payload normalization
- Bump Wormhole modal text from 9-10px to 12-13px
- Add aircraft icon zoom interpolation (0.8→2.0 across zoom 5-12)
- Reduce Mesh Chat panel text sizes for tighter layout
This commit is contained in:
anoracleofra-code
2026-03-26 20:00:30 -06:00
parent c6fc47c2c5
commit da09cf429e
8 changed files with 111 additions and 56 deletions
+4 -5
View File
@@ -3825,11 +3825,10 @@ def _submit_gate_message_envelope(request: Request, gate_id: str, body: dict[str
# — doing so would pre-advance the counter and cause append() to reject
# the event as a replay, silently dropping the message.
#
# The chain payload must match the signed payload exactly. The message
# was signed WITHOUT the `epoch` field (compose_encrypted_gate_message
# excludes it from the signing payload), so we must strip it here too —
# otherwise infonet.append() re-verifies the signature against a payload
# that includes epoch and gets a mismatch → "invalid signature".
# Strip `epoch` — the message was signed without it so including it
# would cause a signature mismatch. `gate_envelope` and `reply_to`
# are kept in the payload for cross-node decryption; signature
# verification in build_signature_payload() strips them automatically.
chain_payload = {k: v for k, v in gate_payload.items() if k != "epoch"}
chain_event_id = ""
try:
+5
View File
@@ -83,6 +83,11 @@ def build_signature_payload(
payload: dict[str, Any],
) -> str:
normalized = normalize_payload(event_type, payload)
# gate_envelope and reply_to ride alongside the signed payload — they are
# added after the message is signed so must be excluded from verification.
if event_type == "gate_message":
for _unsig in ("gate_envelope", "reply_to"):
normalized.pop(_unsig, None)
payload_json = canonical_json(normalized)
return "|".join(
[PROTOCOL_VERSION, NETWORK_ID, event_type, node_id, str(sequence), payload_json]
+39 -9
View File
@@ -58,15 +58,36 @@ MLS_GATE_FORMAT = "mls1"
_GATE_ENVELOPE_DOMAIN = "gate_persona"
def _gate_envelope_key() -> bytes:
"""Return the 256-bit AES key for gate envelope encryption."""
from services.mesh.mesh_secure_storage import _load_domain_key # type: ignore[attr-defined]
return _load_domain_key(_GATE_ENVELOPE_DOMAIN)
def _gate_envelope_key_shared(gate_id: str) -> bytes:
"""Derive a 256-bit AES key from the gate ID via HKDF.
Every node that knows the gate name derives the same key, enabling
cross-node decryption of gate_envelope payloads.
"""
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
ikm = gate_id.strip().lower().encode("utf-8")
return HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=b"shadowbroker-gate-envelope-v1",
info=b"gate_envelope_aes256gcm",
).derive(ikm)
def _gate_envelope_key_legacy() -> bytes | None:
"""Return the old node-local domain key, or None if unavailable."""
try:
from services.mesh.mesh_secure_storage import _load_domain_key # type: ignore[attr-defined]
return _load_domain_key(_GATE_ENVELOPE_DOMAIN)
except Exception:
return None
def _gate_envelope_encrypt(gate_id: str, plaintext: str) -> str:
"""Encrypt plaintext under the gate domain key. Returns base64."""
key = _gate_envelope_key()
"""Encrypt plaintext under the shared gate-derived key. Returns base64."""
key = _gate_envelope_key_shared(gate_id)
nonce = _os.urandom(12)
aad = f"gate_envelope|{gate_id}".encode("utf-8")
ct = _AESGCM(key).encrypt(nonce, plaintext.encode("utf-8"), aad)
@@ -74,15 +95,24 @@ def _gate_envelope_encrypt(gate_id: str, plaintext: str) -> str:
def _gate_envelope_decrypt(gate_id: str, token: str) -> str | None:
"""Decrypt a gate envelope token. Returns plaintext or None on failure."""
"""Decrypt a gate envelope token. Tries the shared key first, then
falls back to the legacy node-local key for old messages."""
try:
raw = base64.b64decode(token)
if len(raw) < 13:
return None
nonce, ct = raw[:12], raw[12:]
key = _gate_envelope_key()
aad = f"gate_envelope|{gate_id}".encode("utf-8")
return _AESGCM(key).decrypt(nonce, ct, aad).decode("utf-8")
# Try shared (cross-node) key first
try:
return _AESGCM(_gate_envelope_key_shared(gate_id)).decrypt(nonce, ct, aad).decode("utf-8")
except Exception:
pass
# Fall back to legacy node-local key for pre-migration messages
legacy_key = _gate_envelope_key_legacy()
if legacy_key:
return _AESGCM(legacy_key).decrypt(nonce, ct, aad).decode("utf-8")
return None
except Exception:
return None
# Self-echo plaintext cache: MLS cannot decrypt messages authored by the same
+9
View File
@@ -49,6 +49,15 @@ def normalize_gate_message_payload(payload: dict[str, Any]) -> dict[str, Any]:
epoch = _safe_int(payload.get("epoch", 0), 0)
if epoch > 0:
normalized["epoch"] = epoch
# gate_envelope carries cross-node decryptable ciphertext — preserve it
# on-chain so receiving nodes can decrypt without MLS key exchange.
gate_envelope = str(payload.get("gate_envelope", "") or "").strip()
if gate_envelope:
normalized["gate_envelope"] = gate_envelope
# reply_to is a display-only parent message reference.
reply_to = str(payload.get("reply_to", "") or "").strip()
if reply_to:
normalized["reply_to"] = reply_to
return normalized