mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-10 12:40:34 +02:00
release: prepare v0.9.7
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
"""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",
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user