test_crypto.py (3304B)
1 """Tests for inlite_ble crypto module.""" 2 3 from inlite_ble.crypto import CsrMeshCrypto 4 5 6 class TestCsrMeshCrypto: 7 """Tests for CsrMeshCrypto.""" 8 9 def test_requires_exactly_one_credential_type(self) -> None: 10 """A derived passphrase and a direct key must not be mixed.""" 11 import pytest 12 13 with pytest.raises(ValueError, match="exactly one"): 14 CsrMeshCrypto() 15 with pytest.raises(ValueError, match="exactly one"): 16 CsrMeshCrypto("test", b"\x00" * 16) 17 18 def test_accepts_direct_16_byte_network_key(self) -> None: 19 key = CsrMeshCrypto.derive_key("test123") 20 assert CsrMeshCrypto(network_key=key)._enc_key == key 21 22 def test_rejects_wrong_sized_network_key(self) -> None: 23 import pytest 24 25 with pytest.raises(ValueError, match="16 bytes"): 26 CsrMeshCrypto(network_key=b"short") 27 28 def test_key_derivation_deterministic(self) -> None: 29 """Same passphrase should produce the same key.""" 30 c1 = CsrMeshCrypto("test123") 31 c2 = CsrMeshCrypto("test123") 32 assert c1._enc_key == c2._enc_key 33 34 def test_different_passphrases_different_keys(self) -> None: 35 c1 = CsrMeshCrypto("password_a") 36 c2 = CsrMeshCrypto("password_b") 37 assert c1._enc_key != c2._enc_key 38 39 def test_key_is_16_bytes(self) -> None: 40 c = CsrMeshCrypto("test") 41 assert len(c._enc_key) == 16 42 43 def test_controller_address_in_range(self) -> None: 44 """Controller address should be in [0x8000, 0xFFFD].""" 45 for _ in range(50): 46 c = CsrMeshCrypto("test") 47 assert 0x8000 <= c.controller_address <= 0xFFFD 48 49 def test_encrypt_decrypt_roundtrip(self) -> None: 50 """Encrypting then decrypting should return the original data.""" 51 crypto = CsrMeshCrypto("roundtrip_test") 52 dest_id = 0x1234 53 pkt_type = 0x71 54 data = b"\x01\x02\x03" 55 56 packet = crypto.encrypt_packet(dest_id, pkt_type, data) 57 result = crypto.decrypt_packet(packet) 58 59 assert result is not None 60 assert result["dest_id"] == dest_id 61 assert result["pkt_type"] == pkt_type 62 assert bytes(result["data"]) == data 63 64 def test_decrypt_invalid_packet_returns_none(self) -> None: 65 crypto = CsrMeshCrypto("test") 66 assert crypto.decrypt_packet(b"\x00" * 5) is None 67 68 def test_decrypt_bad_checksum_returns_none(self) -> None: 69 crypto = CsrMeshCrypto("test") 70 packet = crypto.encrypt_packet(0x0001, 0x71, b"\x01") 71 # Corrupt one byte of the checksum 72 corrupted = bytearray(packet) 73 corrupted[-5] ^= 0xFF 74 assert crypto.decrypt_packet(bytes(corrupted)) is None 75 76 def test_sequence_number_increments(self) -> None: 77 crypto = CsrMeshCrypto("seq_test") 78 p1 = crypto.encrypt_packet(1, 0x71, b"") 79 p2 = crypto.encrypt_packet(1, 0x71, b"") 80 # First 3 bytes are seq number — they should differ 81 assert p1[:3] != p2[:3] 82 83 def test_uses_secrets_not_random(self) -> None: 84 """Verify we use the secrets module (no 'random' import).""" 85 import inlite_ble.crypto as mod 86 import inspect 87 source = inspect.getsource(mod) 88 assert "import secrets" in source 89 assert "import random" not in source