"""Encryption and Decryption functions for KNX Secure.""" from __future__ import annotations from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from .util import Buffer, byte_pad def calculate_message_authentication_code_cbc( key: bytes, additional_data: bytes, payload: Buffer = b"", block_0: bytes = bytes(16), ) -> bytes: """Calculate the message authentication code (MAC) for a message with AES-CBC.""" blocks = ( block_0 + len(additional_data).to_bytes(2, "big") + additional_data + payload ) y_cipher = Cipher(algorithms.AES(key), modes.CBC(bytes(16))) y_encryptor = y_cipher.encryptor() y_blocks = ( y_encryptor.update(byte_pad(blocks, block_size=16)) + y_encryptor.finalize() ) # only calculate, no ctr encryption return y_blocks[-16:] def decrypt_ctr( key: bytes, counter_0: bytes, mac: bytes, payload: Buffer = b"", ) -> tuple[bytes, bytes]: """ Decrypt data from SecureWrapper. MAC will be decoded first with counter 0. Returns a tuple of (KNX/IP frame bytes, MAC TR for verification). """ cipher = Cipher(algorithms.AES(key), modes.CTR(counter_0)) decryptor = cipher.decryptor() mac_tr = decryptor.update(mac) # MAC is encrypted with counter 0 decrypted_data = decryptor.update(payload) + decryptor.finalize() return (decrypted_data, mac_tr) def encrypt_data_ctr( key: bytes, counter_0: bytes, mac_cbc: bytes, payload: Buffer = b"", ) -> tuple[bytes, bytes]: """ Encrypt data with AES-CTR. Payload is expected a full Plain KNX/IP frame with header. MAC shall be encrypted with counter 0, KNXnet/IP frame with incremented counters. Returns a tuple of encrypted data (if there is any) and encrypted MAC. """ s_cipher = Cipher(algorithms.AES(key), modes.CTR(counter_0)) s_encryptor = s_cipher.encryptor() mac = s_encryptor.update(mac_cbc) encrypted_data = s_encryptor.update(payload) + s_encryptor.finalize() return (encrypted_data, mac) def derive_device_authentication_password(device_authentication_password: str) -> bytes: """Derive device authentication password.""" kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=16, salt=b"device-authentication-code.1.secure.ip.knx.org", iterations=65536, ) return kdf.derive(device_authentication_password.encode("latin-1")) def derive_user_password(password_string: str) -> bytes: """Derive user password.""" kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=16, salt=b"user-password.1.secure.ip.knx.org", iterations=65536, ) return kdf.derive(password_string.encode("latin-1")) def generate_ecdh_key_pair() -> tuple[X25519PrivateKey, bytes]: """ Generate an ECDH key pair. Return the private key and the raw bytes of the public key. """ private_key = X25519PrivateKey.generate() public_key_raw = private_key.public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ) return private_key, public_key_raw