"""AES-128-ECB framing used by the Shining Mask BLE protocol. Every command written to the command characteristic is a 16-byte block encrypted with a fixed AES-128 key in ECB mode, and notifications coming back on the notify characteristic are encrypted the same way. The key is baked into the mask firmware / the official "Shining Mask" app -- it is *not* a secret and there is no key exchange. It was recovered by the reverse-engineering community (see the reddit post referenced in the project README) and is identical across every vendor variant of this mask. This module prefers ``cryptography`` (which is what the rest of the robotics stack already ships) and transparently falls back to ``pycryptodome`` if only that is installed. """ from __future__ import annotations # Fixed device key -- hardcoded in firmware, not a credential. Do not "rotate". MASK_AES_KEY = bytes.fromhex("32672f7974ad43451d9c6c894a0e8764") BLOCK_SIZE = 16 try: # Preferred backend: cryptography (pyca) from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes def _cipher() -> "Cipher": return Cipher(algorithms.AES(MASK_AES_KEY), modes.ECB()) # noqa: S305 - ECB is what the device uses def encrypt(block: bytes) -> bytes: """Encrypt one or more 16-byte blocks with AES-128-ECB.""" if len(block) == 0 or len(block) % BLOCK_SIZE != 0: raise ValueError(f"data length must be a positive multiple of {BLOCK_SIZE}, got {len(block)}") enc = _cipher().encryptor() return enc.update(bytes(block)) + enc.finalize() def decrypt(data: bytes) -> bytes: """Decrypt one or more 16-byte blocks with AES-128-ECB.""" if len(data) == 0 or len(data) % BLOCK_SIZE != 0: raise ValueError(f"data length must be a positive multiple of {BLOCK_SIZE}, got {len(data)}") dec = _cipher().decryptor() return dec.update(bytes(data)) + dec.finalize() BACKEND = "cryptography" except ImportError: # pragma: no cover - exercised only when pyca is absent from Crypto.Cipher import AES # type: ignore def encrypt(block: bytes) -> bytes: """Encrypt one or more 16-byte blocks with AES-128-ECB.""" if len(block) == 0 or len(block) % BLOCK_SIZE != 0: raise ValueError(f"data length must be a positive multiple of {BLOCK_SIZE}, got {len(block)}") return AES.new(MASK_AES_KEY, AES.MODE_ECB).encrypt(bytes(block)) def decrypt(data: bytes) -> bytes: """Decrypt one or more 16-byte blocks with AES-128-ECB.""" if len(data) == 0 or len(data) % BLOCK_SIZE != 0: raise ValueError(f"data length must be a positive multiple of {BLOCK_SIZE}, got {len(data)}") return AES.new(MASK_AES_KEY, AES.MODE_ECB).decrypt(bytes(data)) BACKEND = "pycryptodome"