Шифрование через нейросети
Привет, читатель. Статья больше про исследование и тестирование метода, я не пытаюсь навязать все нижеописанное и не заявляю что это 100% рабочее шифрование. Но как по мне идея шифрования через обобщение нейросетей достойна внимания.
Обзор метода
Само сообщение шифруется через симметричное шифрование AES-CBC, где нужен ключ (16 байт) и IV (начальный вектор, чтобы шифр был разным). Ключ генерируется из эмбеддингов, полученных моделью. Но особенность метода в том, что изображение не является секретом, любой желающий может увидеть изображение и зашифрованное сообщение. Но при этом изображение не является публичным ключом. Ключ может получить только конкретная модель нейросети. Поэтому чтобы это работало, yolo-seg должен быть обучен на своем датасете. Одна и та же модель должна быть у обоих участников. Веса модели и датасет должны храниться в строгом секрете. Любой человек в здравом уме скажет, что yolo не сможет уникально сегментировать изображения и на этом моменте закроет статью. Эмбеддинги из 4 и 16 слоя это 192 числа, типа float32, обычно в диапазоне от -10 до 10. Самый важный момент, который позволяет создать уникальный ключ для каждого изображения. По крайней мере это позволяет учитывать малейшие изменения в изображении. Учитывается все изображение, сам класс и его фон. Даже если меняется всего 1 пиксель, создается уникальный ключ. Провел тест из 1000 одинаковых изображений где рандомно заменены 1-5 пикселей. Результат в статье. На 2 разных системах всегда генерируются одинаковые ключи к одним и тем же изображениям. Самое важное условие чтобы на изображении был класс, который может быть сегментирован, иначе ключа просто не будет и сообщение нельзя будет отправить. Чтобы расшифровать сообщение нужно получить изображение, на основе которого был получен ключ и само зашифрованное сообщение. Через такую же модель yolo-seg получаются эмбеддинги чтобы повторить ключ.
Эмбеддинги yolo11n-seg
В основе метода лежит использование эмбеддингов, полученных из модели YOLO11n-seg, для генерации ключа из изображения. Эмбеддинги представляют собой числовое описание изображения, которое позволяет учесть как общие характеристики (например, объект на изображении), так и мелкие детали (изменение одного пикселя). В этом разделе я подробно объясню, что такое эмбеддинги, как они формируются в модели YOLO11n-seg, почему выбраны слои 4 и 16, как обеспечивается их чувствительность к изменениям изображения и как они используются для создания уникального ключа. Эмбеддинги — это компактное числовое представление изображения, которое извлекается из внутренних слоёв нейронной сети. В контексте метода они являются результатом обработки изображения моделью yolo11n-seg и представляют собой массив из 192 чисел с плавающей запятой (float32). Эти числа описывают различные аспекты изображения: объект (например, кот), фон, текстуры, края и даже отдельные пиксели. Каждое число отражает вклад определённой особенности изображения, выявленной нейросетью. Но самое важное что каждое число имеет свой диапазон влияния. И этот уровень влияния определяется датасетом, обучением, разметкой, пайлпайном обучения и инференса. В моём методе эмбеддинги состоят из двух частей:
- 128 чисел из 4 слоя. Отвечает за мелкие детали изображения, фон
- 64 числа из 16 слоя, Описывает более крупные структуры, такие как форма класса, его цвет.
Диапазон значений чисел обычно находится в пределах от -10 до 10, хотя в зависимости от изображения и характеристик модели значения могут быть больше или меньше. Например, типичный фрагмент эмбеддингов для изображения кота может выглядеть следующим образом: [0.1234, -0.5678, 1.2345, -0.0987, 2.3456, 0.6789, -1.5432, 0.4321, ...] Если изменить один пиксель изображения, например, с [100, 100, 100] на [101, 100, 100], некоторые числа в эмбеддингах слегка изменятся: [0.1235, -0.5678, 1.2344, -0.0987, 2.3457, 0.6789, -1.5431, 0.4321, ...]
Эти небольшие изменения играют ключевую роль: они обеспечивают чувствительность метода к минимальным различиям в изображении. Это позволяет генерировать уникальный ключ для каждой версии изображения.
Формирование эмбеддингов
YOLO11n-seg изначально нейронная сеть, предназначенная для обнаружения и сегментации объектов. Она состоит из 23 слоёв, каждый из которых обрабатывает изображение на разном уровне абстракции. В моём методе я использую не итоговые маски сегментации, а промежуточные активации двух слоёв — 4 и 16, которые извлекаются во время обработки изображения. Сперва загружается изображение. Изображение (maryJane.png размером 1088x1920 пикселей) загружается и преобразуется в тензор с тремя каналами (RGB). Тензор имеет размер [1, 3, 1088, 1920], где:
- 1 количество изображений,
- 3 каналы цвета,
- 1088 потому что 1080 не кратно 32, а самое близкое число 1088. Это связано с архитектурой модели.
Обработка через yolo11n-seg. Модель прогоняет тензор через свои слои. Каждый слой выполняет операции свёртки, нормализации и активации, создавая всё более абстрактные представления изображения. На выходе слоя 4 получается тензор размером [1, 128, 34, 60], где:
- 128 — количество каналов,
- 34x60 — уменьшенное пространственное разрешение изображения.
На выходе слоя 16 — тензор [1, 64, 17, 30], где:
- 64 — каналы,
- 17x30 — ещё меньшее разрешение.
Извлечение активаций.
Я использую механизм хуков (register_forward_hook) для получения активаций слоёв 4 и 16 во время обработки изображения. Это позволяет заглянуть внутрь модели и извлечь промежуточные данные, которые более уникальны чем итоговый результат сегментации.
Python: activations = {} layers[4].register_forward_hook(lambda m, i, o: activations.update({"layer_4": o})) layers[16].register_forward_hook(lambda m, i, o: activations.update({"layer_16": o})) with torch.no_grad(): model(image, conf=0.4, imgsz=(1088, 1920))
Усреднение активаций.
Чтобы получить массив из 192 чисел (128 + 64), активации каждого слоя усредняются по пространственным измерениям (высоте и ширине).
- Для слоя 4: тензор [1, 128, 34, 60] → массив [128] Усреднение по 34x60,
- Для слоя 16: тензор [1, 64, 17, 30] → массив [64] Усреднение по 17x30
Python: embeddings = [activations[k].mean(dim=[2, 3]) for k in ["layer_4", "layer_16"]]
Округление. На разных системах результат может отличаться. Вычисления на GPU и CPU дают разные результаты. Поэтому они округляются до 4 знаков после запятой. Это по прежнему сохраняет устойчивость к изменению даже 1 пикселя.
Python: flat_embeddings = np.round(torch.cat(embeddings).cpu().numpy(), decimals=4)
Почему выбраны слои 4 и 16? Выбор слоёв 4 и 16 не случаен.
- Слой 4: Ранний слой модели, который обрабатывает изображение на низком уровне. Он чувствителен к мелким деталям, таким как отдельные пиксели, края, текстуры и локальные цветовые переходы. Активации слоя 4 содержат 128 каналов, что позволяет уловить широкий спектр низкоуровневых признаков.
- Слой 16: Более глубокий слой, который анализирует изображение на высоком уровне. Он распознаёт крупные структуры, такие как форма объекта (например, кота), его поза и общий контекст сцены. Активации слоя 16 содержат 64 канала, что достаточно для описания объектов, но менее детализировано, чем слой 4.
Возможно стоит попробовать комбинации других слоев, но с этими все в порядке пока что. Меня результат удовлетворил.
Чувствительность YOLO11n-seg. Изменение одного пикселя, например с [100, 100, 100] на [101, 100, 100]) влияет на активации слоя 4, потому что он анализирует локальные детали. Это изменение распространяется на тензор [1, 128, 34, 60], слегка сдвигая некоторые значения. Слой 16 менее чувствителен к отдельным пикселям, но может уловить изменения, если они влияют на форму объекта или контекст После усреднения сдвиг в активациях приводит к изменению одного или нескольких чисел в эмбеддингах.
Работа с SHA-256. Эмбеддинги (192 числа) преобразуются в байты и хешируются с помощью SHA-256. SHA-256 чувствителен к входным данным, изменение одного числа в эмбеддингах на 0.0001 приводит к совершенно другому хешу. Это гарантирует, что ключ и IV для каждой версии изображения будут уникальными.
Python: hash_digest = hashlib.sha256(flat_embeddings.tobytes()).digest() key = hash_digest[:16] # Ключ (16 байт) iv = hash_digest[16:32] # IV (16 байт)
Пример.
- Исходное изображение, эмбеддинги [0.1234, -0.5678, 1.2345, ...]. Ключ c87bce1c05dd27f6f9c256399c43aade.
- Изображение с изменённым пикселем, эмбеддинги [0.1235, -0.5678, 1.2344, ...]. Ключ 1279567999291717328cd268d7bde881.
Тестирование уникальности ключа.
Как обсуждалось выше, ключевая проблема, уникальность ключей для каждого изображения. Чтобы проверить метод я сгенерировал 1000 копий изображений своей кошки, заменив на каждом по 1-5 пикселей рандомно, RGB-значения выбирались случайно.
Как только изображения сгенерировались, запускаю код который симулирует работу шифрования, а в конце сравнивает результаты. Если хотя бы 1 раз результаты повторятся, метод не рабочий. Можно попробовать учесть эмбеддинги из еще одного слоя, но пока нет нужды делать этого не нужно. Мы все прекрасно понимаем, что метод и так не рабочий, рано или поздно копии ключей случатся, но ведь интересно когда это случится. По большей части это обусловлено обучением и датасетом, чем больше и разнообразнее тем лучше результат в плане уникальности в теории. Но есть еще очень большое количество параметров, которые могут влиять на это.
Размер обучаемой модели.
Разнообразие датасета и общее количество изображений.
Пайплайн обучения.
Пайплайн инференса.
Качество разметки.
Теперь проведем тест
Python:
import cv2
import numpy as np
import torch from ultralytics
import YOLO from Crypto.Cipher
import AES from Crypto.Util.Padding import pad import hashlib
import os
from collections import defaultdict
import time
# Настройка детерминизма torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False torch.use_deterministic_algorithms(True)
Ограничение числа ядер CPU (например, 2 ядра) torch.set_num_threads(4) # Уменьшает нагрузку на CPU
def generate_key_and_iv(embeddings): flat_embeddings = torch.cat([e.flatten() for e in embeddings]).cpu().numpy() flat_embeddings = np.round(flat_embeddings, decimals=4) hash_digest = hashlib.sha256(flat_embeddings.tobytes()).digest() return hash_digest[:16], hash_digest[16:32]
def encrypt_data(data, key, iv): cipher = AES.new(key, AES.MODE_CBC, iv=iv) ct_bytes = cipher.encrypt(pad(data, AES.block_size)) return ct_bytes
def process_image(image_path, model): image = cv2.imread(image_path) if image is None: raise ValueError(f"Не удалось загрузить изображение: {image_path}") original_shape = image.shape[:2] yolo_size = (max(32, (original_shape[0] + 31) // 32 * 32), max(32, (original_shape[1] + 31) // 32 * 32)) image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.uint8) image_tensor = torch.from_numpy(image_rgb).permute(2, 0, 1).float() / 255.0 image_tensor = image_tensor.clamp(0.0, 1.0).unsqueeze(0).to(model.device) activations = {} def get_activation(name): def hook(module, input, output): activations[name] = output return hook layers = model.model.model layers[4].register_forward_hook(get_activation("layer_4")) layers[16].register_forward_hook(get_activation("layer_16")) with torch.no_grad(): results = model(image, conf=0.4, imgsz=yolo_size) embeddings = [] for layer_name in ["layer_4", "layer_16"]: activation = activations.get(layer_name, torch.zeros(1)) embedding = activation.mean(dim=[2, 3]) if len(activation.shape) == 4 else torch.zeros(1) embeddings.append(embedding) return embeddings
def test_images(image_folder, model, message, delay=0.5): results = [] image_files = [f for f in os.listdir(image_folder) if f.endswith('.png')] if len(image_files) != 1000: print(f"Предупреждение: Найдено {len(image_files)} изображений, ожидалось 1000")
Прогоняем каждое изображение for idx, image_file in enumerate(image_files, 1): image_path = os.path.join(image_folder, image_file) print(f"Обработка изображения {idx}/1000: {image_file}") try: embeddings = process_image(image_path, model) key, iv = generate_key_and_iv(embeddings) ct_bytes = encrypt_data(message, key, iv) results.append({ 'image': image_file, 'key': key.hex(), 'iv': iv.hex(), 'ciphertext': ct_bytes.hex() }) # Задержка для снижения нагрузки time.sleep(delay) # Пауза 0.5 секунды между изображениями except Exception as e: print(f"Ошибка при обработке {image_file}: {str(e)}") # Проверяем уникальность ключей key_to_images = defaultdict(list) for result in results: key_to_images[result['key']].append(result['image']) # Выводим результаты print("\n=== Результаты теста ===") print(f"Обработано изображений: {len(results)}") print(f"Уникальных ключей: {len(key_to_images)}") duplicates_found = False for key, images in key_to_images.items(): if len(images) > 1: duplicates_found = True print(f"\nОШИБКА: Одинаковый ключ найден для {len(images)} изображений!") print(f"Ключ: {key}") print("Изображения:") for img in images: print(f" - {img}") if not duplicates_found: print("\nУСПЕХ: Все ключи уникальны! Метод прошёл тест.") else: print("\nПРОВАЛ: Найдены одинаковые ключи. Метод недостаточно чувствителен!") # Выводим все результаты print("\nПодробные результаты:") for result in results: print(f"Изображение: {result['image']}") print(f" Ключ: {result['key']}") print(f" IV: {result['iv']}") print(f" Шифротекст: {result['ciphertext']}\n") return results, duplicates_found # Основной код device = torch.device("cpu") model = YOLO("yolo11n-seg.pt").to(device) image_folder = "modified_images" # Папка с 1000 изображениями message = "vc,ru".encode() delay = 0.5 # Задержка в секундах results, duplicates_found = test_images(image_folder, model, message, delay)
Результат ?
В следующем попробую собрать просто разные, но похожие изображения, без замены пикселей. Для меня такой результат говорит о том что метод имеет место быть или как минимум еще одно тестирование можно провести.