mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-13 01:27:54 +02:00
668ce16dc7
Gate messages now propagate via the Infonet hashchain as encrypted blobs — every node syncs them through normal chain sync while only Gate members with MLS keys can decrypt. Added mesh reputation system, peer push workers, voluntary Wormhole opt-in for node participation, fork recovery, killwormhole scripts, obfuscated terminology, and hardened the self-updater to protect encryption keys and chain state during updates. New features: Shodan search, train tracking, Sentinel Hub imagery, 8 new intelligence layers, CCTV expansion to 11,000+ cameras across 6 countries, Mesh Terminal CLI, prediction markets, desktop-shell scaffold, and comprehensive mesh test suite (215 frontend + backend tests passing). Community contributors: @wa1id, @AlborzNazari, @adust09, @Xpirix, @imqdcr, @csysp, @suranyami, @chr0n1x, @johan-martensson, @singularfailure, @smithbh, @OrfeoTerkuci, @deuza, @tm-const, @Elhard1, @ttulttul
362 lines
12 KiB
Python
362 lines
12 KiB
Python
"""Wormhole-backed DM ratchet state and crypto.
|
|
|
|
This is the first DM custody move out of the browser. When Wormhole is active,
|
|
the frontend no longer persists ratchet/session state in IndexedDB; instead the
|
|
agent owns the session records and performs ratchet encrypt/decrypt operations
|
|
locally on behalf of the UI.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from cryptography.hazmat.primitives import hashes, hmac
|
|
from cryptography.hazmat.primitives.asymmetric import x25519
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption
|
|
|
|
from services.mesh.mesh_wormhole_identity import bootstrap_wormhole_identity, read_wormhole_identity
|
|
from services.mesh.mesh_secure_storage import read_secure_json, write_secure_json
|
|
|
|
DATA_DIR = Path(__file__).resolve().parents[2] / "data"
|
|
STATE_FILE = DATA_DIR / "wormhole_dm_ratchet.json"
|
|
STATE_LOCK = threading.RLock()
|
|
|
|
MAX_SKIP = 32
|
|
PAD_BUCKET = 1024
|
|
PAD_STEP = 512
|
|
PAD_MAX = 4096
|
|
PAD_MAGIC = "SBP1"
|
|
|
|
|
|
def _b64(data: bytes) -> str:
|
|
return base64.b64encode(data).decode("ascii")
|
|
|
|
|
|
def _unb64(data: str | bytes | None) -> bytes:
|
|
if not data:
|
|
return b""
|
|
if isinstance(data, bytes):
|
|
return base64.b64decode(data)
|
|
return base64.b64decode(data.encode("ascii"))
|
|
|
|
|
|
def _zero_bytes(length: int) -> bytes:
|
|
return bytes([0] * length)
|
|
|
|
|
|
def _stable_json(value: Any) -> str:
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
|
|
|
|
|
def _header_aad(header: dict[str, Any]) -> bytes:
|
|
return _stable_json(header).encode("utf-8")
|
|
|
|
|
|
def _build_padded_payload(plaintext: str) -> bytes:
|
|
data = plaintext.encode("utf-8")
|
|
length = len(data)
|
|
target = PAD_BUCKET
|
|
if length + 6 > target:
|
|
target = ((length + 6 + PAD_STEP - 1) // PAD_STEP) * PAD_STEP
|
|
if target > PAD_MAX:
|
|
target = ((length + 6 + PAD_STEP - 1) // PAD_STEP) * PAD_STEP
|
|
out = bytearray(target)
|
|
out[0:4] = PAD_MAGIC.encode("utf-8")
|
|
out[4] = (length >> 8) & 0xFF
|
|
out[5] = length & 0xFF
|
|
out[6 : 6 + length] = data
|
|
if target > length + 6:
|
|
out[6 + length :] = os.urandom(target - (6 + length))
|
|
return bytes(out)
|
|
|
|
|
|
def _unpad_payload(data: bytes) -> str:
|
|
if len(data) < 6:
|
|
return data.decode("utf-8", errors="replace")
|
|
magic = data[:4].decode("utf-8", errors="ignore")
|
|
if magic != PAD_MAGIC:
|
|
return data.decode("utf-8", errors="replace")
|
|
length = (data[4] << 8) + data[5]
|
|
if length <= 0 or 6 + length > len(data):
|
|
return data.decode("utf-8", errors="replace")
|
|
return data[6 : 6 + length].decode("utf-8", errors="replace")
|
|
|
|
|
|
def _load_all_states() -> dict[str, dict[str, Any]]:
|
|
with STATE_LOCK:
|
|
try:
|
|
raw = read_secure_json(STATE_FILE, lambda: {})
|
|
except Exception:
|
|
import logging
|
|
logging.getLogger(__name__).warning(
|
|
"Wormhole ratchet state could not be decrypted — starting fresh"
|
|
)
|
|
STATE_FILE.unlink(missing_ok=True)
|
|
raw = {}
|
|
if not isinstance(raw, dict):
|
|
return {}
|
|
return {str(k): dict(v) for k, v in raw.items() if isinstance(v, dict)}
|
|
|
|
|
|
def _save_all_states(states: dict[str, dict[str, Any]]) -> None:
|
|
with STATE_LOCK:
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
write_secure_json(STATE_FILE, states)
|
|
|
|
|
|
def _get_state(peer_id: str) -> dict[str, Any] | None:
|
|
return _load_all_states().get(peer_id)
|
|
|
|
|
|
def _set_state(peer_id: str, state: dict[str, Any]) -> None:
|
|
states = _load_all_states()
|
|
states[peer_id] = state
|
|
_save_all_states(states)
|
|
|
|
|
|
def reset_wormhole_dm_ratchet(peer_id: str | None = None) -> dict[str, Any]:
|
|
if peer_id:
|
|
states = _load_all_states()
|
|
states.pop(peer_id, None)
|
|
_save_all_states(states)
|
|
else:
|
|
_save_all_states({})
|
|
return {"ok": True, "peer_id": peer_id or "", "cleared_all": not bool(peer_id)}
|
|
|
|
|
|
def _generate_ratchet_key_pair() -> dict[str, str]:
|
|
priv = x25519.X25519PrivateKey.generate()
|
|
priv_raw = priv.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
|
|
pub_raw = priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
return {
|
|
"pub": _b64(pub_raw),
|
|
"priv": _b64(priv_raw),
|
|
"algo": "X25519",
|
|
}
|
|
|
|
|
|
def _derive_dh_secret(priv_b64: str, their_pub_b64: str) -> bytes:
|
|
priv = x25519.X25519PrivateKey.from_private_bytes(_unb64(priv_b64))
|
|
pub = x25519.X25519PublicKey.from_public_bytes(_unb64(their_pub_b64))
|
|
return priv.exchange(pub)
|
|
|
|
|
|
def _wormhole_long_term_dh_priv_b64() -> str:
|
|
data = read_wormhole_identity()
|
|
if not data.get("bootstrapped") or not data.get("dh_private_key"):
|
|
bootstrap_wormhole_identity()
|
|
data = read_wormhole_identity()
|
|
return str(data.get("dh_private_key", "") or "")
|
|
|
|
|
|
def _hkdf(ikm: bytes, salt: bytes, info: str, length: int) -> bytes:
|
|
derived = HKDF(
|
|
algorithm=hashes.SHA256(),
|
|
length=length,
|
|
salt=salt,
|
|
info=info.encode("utf-8"),
|
|
).derive(ikm)
|
|
return bytes(derived)
|
|
|
|
|
|
def _kdf_rk(rk: bytes, dh_out: bytes) -> tuple[bytes, bytes]:
|
|
salt = rk if rk else _zero_bytes(32)
|
|
out = _hkdf(dh_out, salt, "SB-DR-RK", 64)
|
|
return out[:32], out[32:64]
|
|
|
|
|
|
def _hmac_sha256(key_bytes: bytes, data: bytes) -> bytes:
|
|
mac = hmac.HMAC(key_bytes, hashes.SHA256())
|
|
mac.update(data)
|
|
return bytes(mac.finalize())
|
|
|
|
|
|
def _kdf_ck(ck: bytes) -> tuple[bytes, bytes]:
|
|
mk = _hmac_sha256(ck, b"\x01")
|
|
next_ck = _hmac_sha256(ck, b"\x02")
|
|
return next_ck, mk
|
|
|
|
|
|
def _aes_gcm_encrypt(mk: bytes, plaintext: str, aad: bytes) -> str:
|
|
iv = os.urandom(12)
|
|
aes = AESGCM(mk)
|
|
encoded = _build_padded_payload(plaintext)
|
|
ciphertext = aes.encrypt(iv, encoded, aad)
|
|
return _b64(iv + ciphertext)
|
|
|
|
|
|
def _aes_gcm_decrypt(mk: bytes, ciphertext_b64: str, aad: bytes) -> str:
|
|
combined = _unb64(ciphertext_b64)
|
|
iv = combined[:12]
|
|
ciphertext = combined[12:]
|
|
aes = AESGCM(mk)
|
|
plaintext = aes.decrypt(iv, ciphertext, aad)
|
|
return _unpad_payload(bytes(plaintext))
|
|
|
|
|
|
def _skip_message_keys(state: dict[str, Any], until: int) -> None:
|
|
if not state.get("ckr"):
|
|
return
|
|
skipped = dict(state.get("skipped") or {})
|
|
while int(state.get("nr", 0) or 0) < until:
|
|
next_ck, mk = _kdf_ck(_unb64(str(state["ckr"])))
|
|
key_id = f"{state.get('dhRemote', '')}:{int(state.get('nr', 0) or 0)}"
|
|
if len(skipped) < MAX_SKIP:
|
|
skipped[key_id] = _b64(mk)
|
|
state["ckr"] = _b64(next_ck)
|
|
state["nr"] = int(state.get("nr", 0) or 0) + 1
|
|
state["skipped"] = skipped
|
|
|
|
|
|
def _dh_ratchet(state: dict[str, Any], remote_dh: str, pn: int) -> dict[str, Any]:
|
|
_skip_message_keys(state, pn)
|
|
state["pn"] = int(state.get("ns", 0) or 0)
|
|
state["ns"] = 0
|
|
state["nr"] = 0
|
|
state["dhRemote"] = remote_dh
|
|
|
|
rk_bytes = _unb64(str(state.get("rk", "") or "")) or _zero_bytes(32)
|
|
dh_out_1 = _derive_dh_secret(str(state.get("dhSelfPriv", "")), str(state.get("dhRemote", "")))
|
|
rk_1, ck_r = _kdf_rk(rk_bytes, dh_out_1)
|
|
state["rk"] = _b64(rk_1)
|
|
state["ckr"] = _b64(ck_r)
|
|
|
|
fresh = _generate_ratchet_key_pair()
|
|
state["dhSelfPub"] = fresh["pub"]
|
|
state["dhSelfPriv"] = fresh["priv"]
|
|
dh_out_2 = _derive_dh_secret(str(state.get("dhSelfPriv", "")), str(state.get("dhRemote", "")))
|
|
rk_2, ck_s = _kdf_rk(_unb64(str(state.get("rk", ""))), dh_out_2)
|
|
state["rk"] = _b64(rk_2)
|
|
state["cks"] = _b64(ck_s)
|
|
state["algo"] = "X25519"
|
|
state["updated"] = int(time.time() * 1000)
|
|
return state
|
|
|
|
|
|
def _init_sender_state(peer_id: str, their_dh_pub: str) -> dict[str, Any]:
|
|
fresh = _generate_ratchet_key_pair()
|
|
dh_out = _derive_dh_secret(fresh["priv"], their_dh_pub)
|
|
rk, ck = _kdf_rk(_zero_bytes(32), dh_out)
|
|
return {
|
|
"algo": "X25519",
|
|
"rk": _b64(rk),
|
|
"cks": _b64(ck),
|
|
"ckr": "",
|
|
"dhSelfPub": fresh["pub"],
|
|
"dhSelfPriv": fresh["priv"],
|
|
"dhRemote": their_dh_pub,
|
|
"ns": 0,
|
|
"nr": 0,
|
|
"pn": 0,
|
|
"skipped": {},
|
|
"updated": int(time.time() * 1000),
|
|
}
|
|
|
|
|
|
def _init_receiver_state(peer_id: str, sender_dh_pub: str) -> dict[str, Any]:
|
|
long_term_priv = _wormhole_long_term_dh_priv_b64()
|
|
if not long_term_priv:
|
|
raise ValueError("missing_long_term_key")
|
|
dh_out = _derive_dh_secret(long_term_priv, sender_dh_pub)
|
|
rk, ck = _kdf_rk(_zero_bytes(32), dh_out)
|
|
fresh = _generate_ratchet_key_pair()
|
|
return {
|
|
"algo": "X25519",
|
|
"rk": _b64(rk),
|
|
"cks": "",
|
|
"ckr": _b64(ck),
|
|
"dhSelfPub": fresh["pub"],
|
|
"dhSelfPriv": fresh["priv"],
|
|
"dhRemote": sender_dh_pub,
|
|
"ns": 0,
|
|
"nr": 0,
|
|
"pn": 0,
|
|
"skipped": {},
|
|
"updated": int(time.time() * 1000),
|
|
}
|
|
|
|
|
|
def _ensure_send_chain(state: dict[str, Any]) -> dict[str, Any]:
|
|
if state.get("cks"):
|
|
return state
|
|
rk_bytes = _unb64(str(state.get("rk", "") or "")) or _zero_bytes(32)
|
|
dh_out = _derive_dh_secret(str(state.get("dhSelfPriv", "")), str(state.get("dhRemote", "")))
|
|
rk, ck = _kdf_rk(rk_bytes, dh_out)
|
|
state["rk"] = _b64(rk)
|
|
state["cks"] = _b64(ck)
|
|
state["updated"] = int(time.time() * 1000)
|
|
return state
|
|
|
|
|
|
def encrypt_wormhole_dm(peer_id: str, peer_dh_pub: str, plaintext: str) -> dict[str, Any]:
|
|
if not peer_id or not peer_dh_pub:
|
|
return {"ok": False, "detail": "peer_id and peer_dh_pub are required"}
|
|
state = _get_state(peer_id)
|
|
if not state:
|
|
state = _init_sender_state(peer_id, peer_dh_pub)
|
|
state = _ensure_send_chain(state)
|
|
next_ck, mk = _kdf_ck(_unb64(str(state.get("cks", ""))))
|
|
n = int(state.get("ns", 0) or 0)
|
|
state["ns"] = n + 1
|
|
state["cks"] = _b64(next_ck)
|
|
header = {
|
|
"v": 2,
|
|
"dh": str(state.get("dhSelfPub", "")),
|
|
"pn": int(state.get("pn", 0) or 0),
|
|
"n": n,
|
|
"alg": "X25519",
|
|
}
|
|
ct = _aes_gcm_encrypt(mk, plaintext, _header_aad(header))
|
|
wrapped = _b64(_stable_json({"h": header, "ct": ct}).encode("utf-8"))
|
|
state["updated"] = int(time.time() * 1000)
|
|
_set_state(peer_id, state)
|
|
return {"ok": True, "result": f"dr2:{wrapped}"}
|
|
|
|
|
|
def decrypt_wormhole_dm(peer_id: str, ciphertext: str) -> dict[str, Any]:
|
|
if not ciphertext.startswith("dr2:"):
|
|
return {"ok": False, "detail": "legacy"}
|
|
try:
|
|
raw = ciphertext[4:]
|
|
payload = json.loads(_unb64(raw).decode("utf-8"))
|
|
header = dict(payload.get("h") or {})
|
|
ct = str(payload.get("ct") or "")
|
|
remote_dh = str(header.get("dh") or "")
|
|
pn = int(header.get("pn", 0) or 0)
|
|
n = int(header.get("n", 0) or 0)
|
|
|
|
state = _get_state(peer_id)
|
|
if not state:
|
|
state = _init_receiver_state(peer_id, remote_dh)
|
|
|
|
if remote_dh and remote_dh != str(state.get("dhRemote", "")):
|
|
state = _dh_ratchet(state, remote_dh, pn)
|
|
|
|
skipped = dict(state.get("skipped") or {})
|
|
skip_key = f"{remote_dh}:{n}"
|
|
if skip_key in skipped:
|
|
mk = _unb64(skipped.pop(skip_key))
|
|
state["skipped"] = skipped
|
|
_set_state(peer_id, state)
|
|
return {"ok": True, "result": _aes_gcm_decrypt(mk, ct, _header_aad(header))}
|
|
|
|
_skip_message_keys(state, n)
|
|
if not state.get("ckr"):
|
|
return {"ok": False, "detail": "no_receive_chain"}
|
|
next_ck, mk = _kdf_ck(_unb64(str(state.get("ckr", ""))))
|
|
state["ckr"] = _b64(next_ck)
|
|
state["nr"] = int(state.get("nr", 0) or 0) + 1
|
|
state["updated"] = int(time.time() * 1000)
|
|
_set_state(peer_id, state)
|
|
return {"ok": True, "result": _aes_gcm_decrypt(mk, ct, _header_aad(header))}
|
|
except Exception as exc:
|
|
return {"ok": False, "detail": str(exc) or "ratchet_decrypt_failed"}
|