mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-26 00:57:51 +02:00
334 lines
14 KiB
Python
334 lines
14 KiB
Python
"""Phase 5E — Meshtastic Transport Hardening tests.
|
|
|
|
Validates that:
|
|
1. Default config preserves current public deployment values
|
|
2. Custom 16-byte PSK is consumed by both TX and RX
|
|
3. Custom 32-byte PSK is consumed by both TX and RX
|
|
4. Invalid PSK encoding/length is rejected
|
|
5. Startup warning matrix behaves correctly
|
|
6. No split-brain between send and receive configuration
|
|
"""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_LONGFAST_DEFAULT_KEY = bytes(
|
|
[
|
|
0xD4, 0xF1, 0xBB, 0x3A, 0x20, 0x29, 0x07, 0x59,
|
|
0xF0, 0xBC, 0xFF, 0xAB, 0xCF, 0x4E, 0x69, 0x01,
|
|
]
|
|
)
|
|
|
|
|
|
def _patch_settings(monkeypatch, **overrides):
|
|
"""Patch get_settings() to return a fake with given overrides."""
|
|
from services.config import Settings
|
|
|
|
defaults = {
|
|
"MESH_MQTT_BROKER": "mqtt.meshtastic.org",
|
|
"MESH_MQTT_PORT": 1883,
|
|
"MESH_MQTT_USER": "meshdev",
|
|
"MESH_MQTT_PASS": "large4cats",
|
|
"MESH_MQTT_PSK": "",
|
|
# Satisfy _ensure_dm_token_pepper so it doesn't write to disk
|
|
"MESH_DM_TOKEN_PEPPER": "a" * 32,
|
|
}
|
|
defaults.update(overrides)
|
|
|
|
class _FakeSettings:
|
|
pass
|
|
|
|
for k, v in defaults.items():
|
|
setattr(_FakeSettings, k, v)
|
|
|
|
# Patch in all modules that import get_settings
|
|
import services.config
|
|
import services.env_check
|
|
|
|
fake_fn = lambda: _FakeSettings()
|
|
fake_fn.cache_clear = lambda: None # satisfy lru_cache callers
|
|
monkeypatch.setattr(services.config, "get_settings", fake_fn)
|
|
monkeypatch.setattr(services.env_check, "get_settings", fake_fn)
|
|
|
|
# Patch in mesh_router and sigint_bridge at module level
|
|
from services.mesh import mesh_router
|
|
import services.sigint_bridge
|
|
|
|
monkeypatch.setattr(mesh_router, "get_settings", fake_fn, raising=False)
|
|
monkeypatch.setattr(services.sigint_bridge, "get_settings", fake_fn)
|
|
|
|
return _FakeSettings
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Default config preserves current public deployment values
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDefaultCompatibility:
|
|
def test_default_broker(self):
|
|
from services.config import Settings
|
|
s = Settings()
|
|
assert s.MESH_MQTT_BROKER == "mqtt.meshtastic.org"
|
|
|
|
def test_default_port(self):
|
|
from services.config import Settings
|
|
s = Settings()
|
|
assert s.MESH_MQTT_PORT == 1883
|
|
|
|
def test_default_credentials(self):
|
|
from services.config import Settings
|
|
s = Settings()
|
|
assert s.MESH_MQTT_USER == "meshdev"
|
|
assert s.MESH_MQTT_PASS == "large4cats"
|
|
|
|
def test_default_psk_empty(self):
|
|
from services.config import Settings
|
|
s = Settings()
|
|
assert s.MESH_MQTT_PSK == ""
|
|
|
|
def test_tx_default_psk_is_longfast(self, monkeypatch):
|
|
_patch_settings(monkeypatch)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
assert MeshtasticTransport._resolve_psk() == _LONGFAST_DEFAULT_KEY
|
|
|
|
def test_rx_default_psk_is_longfast(self, monkeypatch):
|
|
_patch_settings(monkeypatch)
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
assert MeshtasticBridge._resolve_psk() == _LONGFAST_DEFAULT_KEY
|
|
|
|
def test_tx_default_broker_config(self, monkeypatch):
|
|
_patch_settings(monkeypatch)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
broker, port, user, pw = MeshtasticTransport._mqtt_config()
|
|
assert broker == "mqtt.meshtastic.org"
|
|
assert port == 1883
|
|
assert user == "meshdev"
|
|
assert pw == "large4cats"
|
|
|
|
def test_rx_default_broker_config(self, monkeypatch):
|
|
_patch_settings(monkeypatch)
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
broker, port, user, pw = MeshtasticBridge._mqtt_config()
|
|
assert broker == "mqtt.meshtastic.org"
|
|
assert port == 1883
|
|
assert user == "meshdev"
|
|
assert pw == "large4cats"
|
|
|
|
def test_rx_client_id_is_runtime_unique_not_fixed_literal(self, monkeypatch):
|
|
_patch_settings(monkeypatch)
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
|
|
first = MeshtasticBridge()
|
|
second = MeshtasticBridge()
|
|
|
|
assert first._client_id.startswith("sb096-")
|
|
assert second._client_id.startswith("sb096-")
|
|
assert first._client_id != second._client_id
|
|
assert first._client_id != "shadowbroker-mesh"
|
|
assert second._client_id != "shadowbroker-mesh"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Custom 16-byte PSK consumed by both TX and RX
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCustom16BytePSK:
|
|
PSK_HEX = "00112233445566778899aabbccddeeff"
|
|
PSK_BYTES = bytes.fromhex(PSK_HEX)
|
|
|
|
def test_tx_resolves_16byte_psk(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK=self.PSK_HEX)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
assert MeshtasticTransport._resolve_psk() == self.PSK_BYTES
|
|
|
|
def test_rx_resolves_16byte_psk(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK=self.PSK_HEX)
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
assert MeshtasticBridge._resolve_psk() == self.PSK_BYTES
|
|
|
|
def test_16byte_psk_not_longfast(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK=self.PSK_HEX)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
assert MeshtasticTransport._resolve_psk() != _LONGFAST_DEFAULT_KEY
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Custom 32-byte PSK consumed by both TX and RX
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCustom32BytePSK:
|
|
PSK_HEX = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
|
|
PSK_BYTES = bytes.fromhex(PSK_HEX)
|
|
|
|
def test_tx_resolves_32byte_psk(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK=self.PSK_HEX)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
assert MeshtasticTransport._resolve_psk() == self.PSK_BYTES
|
|
|
|
def test_rx_resolves_32byte_psk(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK=self.PSK_HEX)
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
assert MeshtasticBridge._resolve_psk() == self.PSK_BYTES
|
|
|
|
def test_32byte_psk_length(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK=self.PSK_HEX)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
assert len(MeshtasticTransport._resolve_psk()) == 32
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Invalid PSK encoding/length is rejected
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestInvalidPSKRejected:
|
|
def test_non_hex_rejected(self):
|
|
from services.env_check import validate_mesh_mqtt_psk
|
|
err = validate_mesh_mqtt_psk("not-hex-at-all")
|
|
assert err is not None
|
|
assert "hex" in err.lower()
|
|
|
|
def test_odd_hex_rejected(self):
|
|
from services.env_check import validate_mesh_mqtt_psk
|
|
err = validate_mesh_mqtt_psk("abc") # odd-length hex
|
|
assert err is not None
|
|
|
|
def test_wrong_length_8_bytes_rejected(self):
|
|
from services.env_check import validate_mesh_mqtt_psk
|
|
err = validate_mesh_mqtt_psk("00112233aabbccdd") # 8 bytes
|
|
assert err is not None
|
|
assert "16 or 32" in err
|
|
|
|
def test_wrong_length_24_bytes_rejected(self):
|
|
from services.env_check import validate_mesh_mqtt_psk
|
|
err = validate_mesh_mqtt_psk("00" * 24) # 24 bytes
|
|
assert err is not None
|
|
assert "16 or 32" in err
|
|
|
|
def test_valid_16_bytes_accepted(self):
|
|
from services.env_check import validate_mesh_mqtt_psk
|
|
assert validate_mesh_mqtt_psk("00" * 16) is None
|
|
|
|
def test_valid_32_bytes_accepted(self):
|
|
from services.env_check import validate_mesh_mqtt_psk
|
|
assert validate_mesh_mqtt_psk("00" * 32) is None
|
|
|
|
def test_empty_string_accepted(self):
|
|
from services.env_check import validate_mesh_mqtt_psk
|
|
assert validate_mesh_mqtt_psk("") is None
|
|
|
|
def test_validate_env_rejects_bad_psk(self, monkeypatch):
|
|
"""validate_env with strict=False returns False on bad PSK."""
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK="bad-hex")
|
|
from services.env_check import validate_env
|
|
result = validate_env(strict=False)
|
|
assert result is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Startup warning matrix
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestStartupWarningMatrix:
|
|
def test_default_config_no_mqtt_warnings(self, monkeypatch):
|
|
"""Default public config must emit no MQTT warning."""
|
|
_patch_settings(monkeypatch)
|
|
from services.env_check import _mqtt_startup_warnings, get_settings
|
|
warnings = _mqtt_startup_warnings(get_settings())
|
|
assert warnings == []
|
|
|
|
def test_custom_broker_default_psk_warns(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_BROKER="my-broker.local")
|
|
from services.env_check import _mqtt_startup_warnings, get_settings
|
|
warnings = _mqtt_startup_warnings(get_settings())
|
|
psk_warnings = [w for w in warnings if "LongFast PSK" in w]
|
|
assert len(psk_warnings) == 1
|
|
|
|
def test_custom_broker_default_creds_warns(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_BROKER="my-broker.local")
|
|
from services.env_check import _mqtt_startup_warnings, get_settings
|
|
warnings = _mqtt_startup_warnings(get_settings())
|
|
cred_warnings = [w for w in warnings if "credentials" in w.lower()]
|
|
assert len(cred_warnings) == 1
|
|
|
|
def test_custom_broker_custom_psk_no_psk_warning(self, monkeypatch):
|
|
_patch_settings(
|
|
monkeypatch,
|
|
MESH_MQTT_BROKER="my-broker.local",
|
|
MESH_MQTT_PSK="00" * 16,
|
|
)
|
|
from services.env_check import _mqtt_startup_warnings, get_settings
|
|
warnings = _mqtt_startup_warnings(get_settings())
|
|
psk_warnings = [w for w in warnings if "LongFast PSK" in w]
|
|
assert len(psk_warnings) == 0
|
|
|
|
def test_custom_broker_custom_creds_no_cred_warning(self, monkeypatch):
|
|
_patch_settings(
|
|
monkeypatch,
|
|
MESH_MQTT_BROKER="my-broker.local",
|
|
MESH_MQTT_USER="private",
|
|
MESH_MQTT_PASS="secretpass",
|
|
)
|
|
from services.env_check import _mqtt_startup_warnings, get_settings
|
|
warnings = _mqtt_startup_warnings(get_settings())
|
|
cred_warnings = [w for w in warnings if "credentials" in w.lower()]
|
|
assert len(cred_warnings) == 0
|
|
|
|
def test_default_broker_custom_psk_no_warning(self, monkeypatch):
|
|
"""Using a custom PSK on the default public broker is fine — no warning."""
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK="00" * 16)
|
|
from services.env_check import _mqtt_startup_warnings, get_settings
|
|
warnings = _mqtt_startup_warnings(get_settings())
|
|
assert warnings == []
|
|
|
|
def test_custom_broker_both_defaults_emits_two_warnings(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_BROKER="private.example.com")
|
|
from services.env_check import _mqtt_startup_warnings, get_settings
|
|
warnings = _mqtt_startup_warnings(get_settings())
|
|
assert len(warnings) == 2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. No split-brain between send and receive configuration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestNoSplitBrain:
|
|
def test_tx_rx_same_broker(self, monkeypatch):
|
|
_patch_settings(monkeypatch, MESH_MQTT_BROKER="custom.broker.io", MESH_MQTT_PORT=8883)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
tx_broker, tx_port, tx_user, tx_pw = MeshtasticTransport._mqtt_config()
|
|
rx_broker, rx_port, rx_user, rx_pw = MeshtasticBridge._mqtt_config()
|
|
assert tx_broker == rx_broker == "custom.broker.io"
|
|
assert tx_port == rx_port == 8883
|
|
assert tx_user == rx_user
|
|
assert tx_pw == rx_pw
|
|
|
|
def test_tx_rx_same_psk(self, monkeypatch):
|
|
psk_hex = "aabbccdd" * 4
|
|
_patch_settings(monkeypatch, MESH_MQTT_PSK=psk_hex)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
assert MeshtasticTransport._resolve_psk() == MeshtasticBridge._resolve_psk()
|
|
|
|
def test_tx_rx_same_default_psk(self, monkeypatch):
|
|
_patch_settings(monkeypatch)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
assert MeshtasticTransport._resolve_psk() == MeshtasticBridge._resolve_psk()
|
|
assert MeshtasticTransport._resolve_psk() == _LONGFAST_DEFAULT_KEY
|
|
|
|
def test_tx_rx_same_custom_credentials(self, monkeypatch):
|
|
_patch_settings(
|
|
monkeypatch,
|
|
MESH_MQTT_USER="private-user",
|
|
MESH_MQTT_PASS="private-pass",
|
|
)
|
|
from services.mesh.mesh_router import MeshtasticTransport
|
|
from services.sigint_bridge import MeshtasticBridge
|
|
_, _, tx_user, tx_pw = MeshtasticTransport._mqtt_config()
|
|
_, _, rx_user, rx_pw = MeshtasticBridge._mqtt_config()
|
|
assert tx_user == rx_user == "private-user"
|
|
assert tx_pw == rx_pw == "private-pass"
|