import base64 import os from cryptography.hazmat.primitives.asymmetric import x25519 from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives import serialization from app.core.config import config def encrypt_system_message(text: str, user_public_key_b64: str) -> str: """ Шифрует системное сообщение от id: 0 для конкретного пользователя. Универсальное решение: автоматически преобразует любые форматы ключей в Raw 32-bytes. """ # 1. Загружаем приватный ключ сервера (id: 0) server_priv_bytes = base64.b64decode(config.SYSTEM_CHAT_PRIVATE_KEY) server_private_key = x25519.X25519PrivateKey.from_private_bytes(server_priv_bytes) # 2. Безопасно загружаем публичный ключ получателя user_pub_bytes = base64.b64decode(user_public_key_b64) # ФИКС: Если длина ключа 42 байта (формат SubjectPublicKeyInfo / DER от OpenSSL), # мы извлекаем из него чистые 32 байта, которые понимает Flutter. if len(user_pub_bytes) == 42: user_public_key = serialization.load_der_public_key(user_pub_bytes) elif len(user_pub_bytes) == 32: user_public_key = x25519.X25519PublicKey.from_public_bytes(user_pub_bytes) else: raise ValueError(f"Неподдерживаемая длина публичного ключа: {len(user_pub_bytes)} байт") # 3. Вычисляем чистый, сырой Shared Secret (32 байта) # В Dart методе `sharedSecretKey` для X25519 это и есть финальный AES-ключ shared_secret = server_private_key.exchange(user_public_key) # 4. Инициализируем AESGCM с чистым общим секретом aesgcm = AESGCM(shared_secret) # 5. Генерируем случайный Nonce (12 байт) nonce = os.urandom(12) # 6. Шифруем plain text plaintext_bytes = text.encode('utf-8') encrypted_data = aesgcm.encrypt(nonce, plaintext_bytes, None) # Разделяем результат на ciphertext и 16-байтовый MAC ciphertext = encrypted_data[:-16] mac = encrypted_data[-16:] # 7. Склеиваем под формат Dart: nonce (12) + mac (16) + ciphertext final_payload = nonce + mac + ciphertext return base64.b64encode(final_payload).decode('utf-8')