import secrets from pathlib import Path from typing import Union from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend # ===== Crypto Utilities ===== class CryptoHelper: """Cryptographic utilities for wafer data encryption/decryption.""" # ===== Hex/Byte Conversion ===== @staticmethod def hex_string_to_bytearray(hex_str: str) -> bytes: """Converts a hex string to bytes (e.g., '48656C6C6F' → b'Hello').""" if len(hex_str) % 2 != 0: raise ValueError("Hex string must have even length") try: return bytes.fromhex(hex_str) except ValueError as e: raise ValueError(f"Invalid hex string: {e}") @staticmethod def bytearray_to_hex_string(data: Union[bytes, bytearray]) -> str: """Converts bytes to lowercase hex string.""" return data.hex() # ===== Random Data ===== @staticmethod def generate_random_bytes(length: int) -> bytes: """Generates cryptographically secure random bytes.""" if length < 0: raise ValueError("Length must be non-negative") return secrets.token_bytes(length) # ===== AES Decryption ===== @staticmethod def decrypt_aes(data: bytes, key: bytes, iv: bytes) -> bytes: """ Decrypts AES-128-CBC data with PKCS7 padding. Args: data: Encrypted ciphertext (must be multiple of 16 bytes) key: 16-byte AES key iv: 16-byte IV Returns: Decrypted plaintext Raises: ValueError: If inputs are invalid cryptography.exceptions.InvalidSignature: On decryption failure """ if len(key) != 16: raise ValueError("Key must be 16 bytes (AES-128)") if len(iv) != 16: raise ValueError("IV must be 16 bytes") if len(data) == 0: return b"" if len(data) % 16 != 0: raise ValueError("Ciphertext length must be multiple of 16 bytes") cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) decrypt_ctx = cipher.decryptor() plaintext_padded = decrypt_ctx.update(data) + decrypt_ctx.finalize() # Remove PKCS7 padding pad_len = plaintext_padded[-1] if pad_len > 16 or pad_len == 0: raise ValueError("Invalid padding") return plaintext_padded[:-pad_len] # ===== Binary File I/O ===== @staticmethod def save_to_binary_file(file_path: Union[str, Path], data: bytes) -> None: """Writes bytes to a binary file.""" Path(file_path).write_bytes(data) @staticmethod def read_from_binary_file(file_path: Union[str, Path]) -> bytes: """Reads bytes from a binary file.""" return Path(file_path).read_bytes() # ===== Convenience Helpers ===== @classmethod def read_hex_string_from_binary_file(cls, file_path: Union[str, Path]) -> str: """Reads binary file and returns hex string.""" data = cls.read_from_binary_file(file_path) return cls.bytearray_to_hex_string(data)