ha-inlite

Home Assistant integration for in-lite
git clone https://git.stephank.nl/ha-inlite
Log | Files | Refs | README | LICENSE | ZIP

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