mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-08 23:33:57 +02:00
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:
+4
-5
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user