mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-08 02:16:41 +02:00
282 lines
11 KiB
Python
282 lines
11 KiB
Python
"""Local-only DM diagnostic runner.
|
|
|
|
The selftest uses dedicated synthetic aliases so operators can verify the DM
|
|
MLS path without creating a real contact or publishing a message. It is a
|
|
functional/privacy smoke test, not a substitute for a two-node network test.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import secrets
|
|
import time
|
|
from typing import Any
|
|
|
|
from services.config import get_settings
|
|
from services.mesh import mesh_dm_mls
|
|
from services.mesh.mesh_local_custody import local_custody_status_snapshot
|
|
from services.mesh.mesh_rollout_flags import signed_write_content_private_transport_lock_required
|
|
from services.mesh.mesh_wormhole_identity import register_wormhole_dm_key
|
|
from services.mesh.mesh_wormhole_persona import bootstrap_wormhole_persona_state, get_dm_identity
|
|
from services.mesh.mesh_wormhole_prekey import register_wormhole_prekey_bundle
|
|
from services.wormhole_supervisor import get_transport_tier, get_wormhole_state
|
|
|
|
|
|
def _sha256_text(value: str) -> str:
|
|
return hashlib.sha256(str(value or "").encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _sha256_b64_payload(value: str) -> str:
|
|
try:
|
|
raw = base64.b64decode(str(value or ""), validate=True)
|
|
except Exception:
|
|
raw = str(value or "").encode("utf-8")
|
|
return hashlib.sha256(raw).hexdigest()
|
|
|
|
|
|
def _step(name: str, result: dict[str, Any], *, required: bool = True) -> dict[str, Any]:
|
|
ok = bool(result.get("ok"))
|
|
return {
|
|
"name": name,
|
|
"ok": ok,
|
|
"required": bool(required),
|
|
"detail": "ok" if ok else str(result.get("detail", "failed") or "failed"),
|
|
}
|
|
|
|
|
|
def _contains_plaintext(serialized: str, plaintexts: list[str]) -> bool:
|
|
haystack = str(serialized or "")
|
|
return any(bool(text) and text in haystack for text in plaintexts)
|
|
|
|
|
|
def run_dm_selftest(message: str = "") -> dict[str, Any]:
|
|
started_at = int(time.time())
|
|
run_id = secrets.token_hex(6)
|
|
local_alias = f"sb_dm_selftest_local_{run_id}"
|
|
peer_alias = f"sb_dm_selftest_peer_{run_id}"
|
|
plaintext = str(message or "").strip() or f"ShadowBroker DM selftest {run_id}"
|
|
reply_plaintext = f"selftest reply {run_id}"
|
|
steps: list[dict[str, Any]] = []
|
|
checks: list[dict[str, Any]] = []
|
|
cleanup: dict[str, Any] = {"ok": False, "detail": "not_run"}
|
|
result: dict[str, Any] | None = None
|
|
|
|
try:
|
|
bootstrap_wormhole_persona_state()
|
|
identity = get_dm_identity()
|
|
dm_key = register_wormhole_dm_key()
|
|
prekeys = register_wormhole_prekey_bundle()
|
|
steps.append(_step("dm_identity_loaded", {"ok": bool(identity.get("node_id"))}))
|
|
steps.append(_step("dm_key_registered", dm_key))
|
|
steps.append(_step("prekey_bundle_registered", prekeys, required=False))
|
|
|
|
peer_bundle = mesh_dm_mls.export_dm_key_package_for_alias(peer_alias)
|
|
steps.append(_step("synthetic_peer_key_package", peer_bundle))
|
|
if not peer_bundle.get("ok"):
|
|
result = _finish(
|
|
ok=False,
|
|
run_id=run_id,
|
|
started_at=started_at,
|
|
steps=steps,
|
|
checks=checks,
|
|
cleanup=cleanup,
|
|
plaintext_hash=_sha256_text(plaintext),
|
|
)
|
|
return result
|
|
|
|
initiated = mesh_dm_mls.initiate_dm_session(local_alias, peer_alias, peer_bundle)
|
|
steps.append(_step("mls_session_initiated", initiated))
|
|
if not initiated.get("ok"):
|
|
result = _finish(
|
|
ok=False,
|
|
run_id=run_id,
|
|
started_at=started_at,
|
|
steps=steps,
|
|
checks=checks,
|
|
cleanup=cleanup,
|
|
plaintext_hash=_sha256_text(plaintext),
|
|
)
|
|
return result
|
|
|
|
accepted = mesh_dm_mls.accept_dm_session(peer_alias, local_alias, str(initiated.get("welcome", "")))
|
|
steps.append(_step("mls_session_accepted_by_peer", accepted))
|
|
if not accepted.get("ok"):
|
|
result = _finish(
|
|
ok=False,
|
|
run_id=run_id,
|
|
started_at=started_at,
|
|
steps=steps,
|
|
checks=checks,
|
|
cleanup=cleanup,
|
|
plaintext_hash=_sha256_text(plaintext),
|
|
)
|
|
return result
|
|
|
|
encrypted = mesh_dm_mls.encrypt_dm(local_alias, peer_alias, plaintext)
|
|
steps.append(_step("outbound_encrypt", encrypted))
|
|
if not encrypted.get("ok"):
|
|
result = _finish(
|
|
ok=False,
|
|
run_id=run_id,
|
|
started_at=started_at,
|
|
steps=steps,
|
|
checks=checks,
|
|
cleanup=cleanup,
|
|
plaintext_hash=_sha256_text(plaintext),
|
|
)
|
|
return result
|
|
|
|
decrypted = mesh_dm_mls.decrypt_dm(
|
|
peer_alias,
|
|
local_alias,
|
|
str(encrypted.get("ciphertext", "")),
|
|
str(encrypted.get("nonce", "")),
|
|
)
|
|
decrypt_matches = bool(decrypted.get("ok")) and decrypted.get("plaintext") == plaintext
|
|
steps.append(
|
|
_step(
|
|
"synthetic_peer_decrypt",
|
|
{"ok": decrypt_matches, "detail": str(decrypted.get("detail", "plaintext_mismatch"))},
|
|
)
|
|
)
|
|
|
|
reply_encrypted = mesh_dm_mls.encrypt_dm(peer_alias, local_alias, reply_plaintext)
|
|
steps.append(_step("reply_encrypt", reply_encrypted))
|
|
reply_decrypted = (
|
|
mesh_dm_mls.decrypt_dm(
|
|
local_alias,
|
|
peer_alias,
|
|
str(reply_encrypted.get("ciphertext", "")),
|
|
str(reply_encrypted.get("nonce", "")),
|
|
)
|
|
if reply_encrypted.get("ok")
|
|
else {"ok": False, "detail": "reply_encrypt_failed"}
|
|
)
|
|
reply_matches = bool(reply_decrypted.get("ok")) and reply_decrypted.get("plaintext") == reply_plaintext
|
|
steps.append(
|
|
_step(
|
|
"local_reply_decrypt",
|
|
{"ok": reply_matches, "detail": str(reply_decrypted.get("detail", "plaintext_mismatch"))},
|
|
)
|
|
)
|
|
|
|
serialized_cipher_material = "|".join(
|
|
[
|
|
str(encrypted.get("ciphertext", "")),
|
|
str(encrypted.get("nonce", "")),
|
|
str(initiated.get("welcome", "")),
|
|
str(reply_encrypted.get("ciphertext", "")),
|
|
str(reply_encrypted.get("nonce", "")),
|
|
]
|
|
)
|
|
no_plaintext_in_cipher_material = not _contains_plaintext(
|
|
serialized_cipher_material,
|
|
[plaintext, reply_plaintext],
|
|
)
|
|
checks.extend(
|
|
[
|
|
{
|
|
"name": "mls_format_locked",
|
|
"ok": bool(
|
|
mesh_dm_mls.is_dm_locked_to_mls(local_alias, peer_alias)
|
|
and mesh_dm_mls.is_dm_locked_to_mls(peer_alias, local_alias)
|
|
),
|
|
"detail": "DM pair is locked to MLS after first encrypt/decrypt.",
|
|
},
|
|
{
|
|
"name": "cipher_material_no_plaintext_substring",
|
|
"ok": no_plaintext_in_cipher_material,
|
|
"detail": "Plaintext was not found as a substring of ciphertext, nonce, or welcome material.",
|
|
},
|
|
{
|
|
"name": "synthetic_alias_separation",
|
|
"ok": local_alias != peer_alias and local_alias != str(identity.get("node_id", "")),
|
|
"detail": "Selftest aliases are separate from the persistent DM alias.",
|
|
},
|
|
{
|
|
"name": "content_private_transport_lock_required",
|
|
"ok": bool(signed_write_content_private_transport_lock_required()),
|
|
"detail": "Signed content-private writes require transport_lock=private_strong.",
|
|
},
|
|
{
|
|
"name": "relay_fallback_requires_approval",
|
|
"ok": bool(get_settings().MESH_PRIVATE_RELEASE_APPROVAL_ENABLE),
|
|
"detail": "Weaker relay fallback is approval-gated.",
|
|
},
|
|
{
|
|
"name": "local_only_no_network_release",
|
|
"ok": True,
|
|
"detail": "Selftest used local MLS compose/decrypt only; it did not publish a test message.",
|
|
},
|
|
]
|
|
)
|
|
|
|
ok = all(step["ok"] for step in steps if step["required"]) and all(check["ok"] for check in checks)
|
|
result = _finish(
|
|
ok=ok,
|
|
run_id=run_id,
|
|
started_at=started_at,
|
|
steps=steps,
|
|
checks=checks,
|
|
cleanup=cleanup,
|
|
plaintext_hash=_sha256_text(plaintext),
|
|
ciphertext_hash=_sha256_b64_payload(str(encrypted.get("ciphertext", ""))),
|
|
)
|
|
return result
|
|
finally:
|
|
cleanup = mesh_dm_mls.forget_dm_aliases([local_alias, peer_alias])
|
|
if result is not None:
|
|
result["cleanup"] = cleanup
|
|
|
|
|
|
def _finish(
|
|
*,
|
|
ok: bool,
|
|
run_id: str,
|
|
started_at: int,
|
|
steps: list[dict[str, Any]],
|
|
checks: list[dict[str, Any]],
|
|
cleanup: dict[str, Any],
|
|
plaintext_hash: str,
|
|
ciphertext_hash: str = "",
|
|
) -> dict[str, Any]:
|
|
transport_tier = "public_degraded"
|
|
try:
|
|
transport_tier = str(get_transport_tier() or "public_degraded")
|
|
except Exception:
|
|
try:
|
|
transport_tier = str(get_wormhole_state().get("transport_tier", "public_degraded") or "public_degraded")
|
|
except Exception:
|
|
transport_tier = "unknown"
|
|
return {
|
|
"ok": bool(ok),
|
|
"run_id": str(run_id),
|
|
"mode": "local_synthetic_peer",
|
|
"started_at": int(started_at),
|
|
"completed_at": int(time.time()),
|
|
"transport_tier": transport_tier,
|
|
"local_custody": local_custody_status_snapshot(),
|
|
"steps": steps,
|
|
"privacy_checks": checks,
|
|
"artifacts": {
|
|
"plaintext_sha256": plaintext_hash,
|
|
"ciphertext_sha256": ciphertext_hash,
|
|
"plaintext_returned": False,
|
|
"contact_created": False,
|
|
"network_release_attempted": False,
|
|
},
|
|
"cleanup": cleanup,
|
|
"unproven_by_this_test": [
|
|
"real two-node delivery across RNS/Tor/relay",
|
|
"passive traffic timing resistance",
|
|
"remote peer key custody",
|
|
"invite exchange UX on a separate device",
|
|
],
|
|
"next_hardening": [
|
|
"add a two-node localhost harness with separate backend data directories",
|
|
"capture packet/HTTP traces during the test and assert no plaintext or stable public identity leaks",
|
|
"add batch/timing-cover assertions for high-privacy mode",
|
|
],
|
|
}
|