Привет, Хабр! Когда речь заходит о защите конфиденциального файла, на ум приходят два пути: шифрование и стеганография. Первый делает файл нечитаемым для посторонних. Второй — делает сам факт существования файла незаметным. А что, если объединить эти два подхода, создав по-настояшему надежное "двойное дно" для ваших данных?
В этой статье мы не просто обсудим теорию, а пошагово, с подробным разбором кода, создадим собственный простой и надежный формат шифрования .cha
(сокращение от Chameleon) на Python. А затем покажем, как именно работает механизм LSB-встраивания в картинку и как наша программа «ChameleonLab» автоматически находит эти скрытые данные.

Часть 1: Пошаговая реализация .CHA на Python
Цель — создать утилиту, которая:
Надежно шифрует файл с помощью пароля.
Сохраняет оригинальное имя файла внутри зашифрованного контейнера.
Проверяет целостность данных при расшифровке.
Для этого мы будем использовать отличную библиотеку cryptography
.
Шаг 1: Генерация ключа из пароля (KDF)
Никогда не используйте пароль напрямую в качестве ключа шифрования! Его нужно "растянуть" до криптографически стойкого ключа с помощью функции деривации ключей (KDF). Мы используем PBKDF2 со 100 000 итераций.
# Из crypto_utils.py
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os
def generate_key_from_password(password: str, salt: bytes) -> bytes:
"""
Генерирует 32-байтный ключ из пароля и соли с помощью PBKDF2-HMAC-SHA256.
"""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32, # Длина ключа в байтах (256 бит для AES)
salt=salt,
iterations=100000, # Рекомендованное количество итераций
)
# Кодируем ключ в URL-safe Base64 для совместимости с Fernet
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
Шаг 2: Упаковка и шифрование файла
Структура нашего .cha
файла: [Магическое число] + [Соль] + [Зашифрованные данные]
.
Магическое число (
b'CHAMELEON'
): Уникальная сигнатура для опознания формата.Соль: 16 случайных байт для PBKDF2.
Зашифрованные данные: Результат шифрования по стандарту Fernet (AES-128-CBC + HMAC-SHA256), который гарантирует конфиденциальность и целостность данных.
# Из crypto_utils.py
from cryptography.fernet import Fernet
from pathlib import Path
MAGIC_NUMBER = b'CHAMELEON'
def encrypt_cha_file(input_path: str, output_path: str, password: str):
"""Шифрует файл в формат .cha, сохраняя имя файла."""
input_path = Path(input_path)
with open(input_path, 'rb') as f:
plaintext = f.read()
# Упаковываем имя файла и его содержимое через разделитель '|'
packaged_data = input_path.name.encode('utf-8') + b'|' + plaintext
salt = os.urandom(16)
key = generate_key_from_password(password, salt)
f = Fernet(key)
encrypted_data = f.encrypt(packaged_data)
with open(output_path, 'wb') as f_out:
f_out.write(MAGIC_NUMBER)
f_out.write(salt)
f_out.write(encrypted_data)
Эта функция реализована в UI на вкладке "Шифрование файлов".
Шаг 3: Расшифровка и проверка
Процесс расшифровки обратный, но с ключевым моментом: fernet.decrypt()
автоматически проверит HMAC-подпись. Если данные повреждены или пароль неверен, функция выбросит исключение InvalidToken
, защищая от работы с некорректными данными.
# Из crypto_utils.py
def decrypt_cha_file(input_path: str, output_path: str, password: str) -> str:
"""Расшифровывает .cha файл, возвращает оригинальное имя файла."""
with open(input_path, 'rb') as f:
if f.read(len(MAGIC_NUMBER)) != MAGIC_NUMBER:
raise ValueError("Это не файл формата .cha или он поврежден.")
salt = f.read(16)
encrypted_data = f.read()
key = generate_key_from_password(password, salt)
f = Fernet(key)
decrypted_packaged_data = f.decrypt(encrypted_data)
filename_bytes, file_content = decrypted_packaged_data.split(b'|', 1)
original_filename = filename_bytes.decode('utf-8')
with open(output_path, 'wb') as f_out:
f_out.write(file_content)
return original_filename
Часть 2: Прячем зашифрованный файл в картинке
Итак, у нас есть надежно зашифрованный файл. Теперь задача — сделать его невидимым. Как именно происходит встраивание в картинку и, что еще важнее, как потом найти эти данные автоматически?
Шаг 2a: Создание "Стего-Пакета"
Чтобы утилита могла автоматически находить данные, мы "заворачиваем" наш .cha
файл в еще один контейнер со служебным заголовком. Структура этого стего-пакета:
[Длина данных (4б)] + [Число бит (1б)] + [СИГНАТУРА (4б)] + [Флаг шифрования (1б)] + [Сам файл]
Ключевой элемент здесь — СИГНАТУРА (b'S6KB'
). Это уникальный "маячок", который наша программа ищет в файлах для автоматического обнаружения.
Шаг 2b: Алгоритм встраивания (hide)
Функция hide
берет этот пакет и последовательно записывает его биты в младшие биты (LSB) пикселей изображения.
# steganography_core.py
def hide(carrier_bytes: np.ndarray, final_payload: bytes, n_bits: int, is_encrypted: bool) -> np.ndarray:
encryption_flag = b'\x01' if is_encrypted else b'\x00'
data_to_hide = MAGIC_NUMBER + encryption_flag + final_payload
metadata_len = len(data_to_hide).to_bytes(4, 'big')
n_bits_val = int(n_bits).to_bytes(1, 'big')
full_data = metadata_len + n_bits_val + data_to_hide
full_bits = data_to_binary(full_data)
carrier_flat = carrier_bytes.flatten().copy().astype(np.uint8)
mask = ~((1 << n_bits) - 1) & 0xFF
carrier_flat &= mask
bit_index = 0
for i in range(math.ceil(len(full_bits) / n_bits)):
chunk = full_bits[bit_index : bit_index + n_bits].ljust(n_bits, '0')
carrier_flat[i] |= int(chunk, 2)
bit_index += n_bits
return carrier_flat.reshape(carrier_bytes.shape)
Шаг 2c: Автоматическое извлечение (reveal)
Программа для извлечения не знает, сколько LSB было использовано, поэтому она перебирает все варианты от 1 до 8. Для каждого варианта она пытается прочитать заголовок и найти сигнатуру.
# steganography_core.py
def reveal(carrier_bytes: np.ndarray) -> Tuple[bytes, bool, bool]:
flat = carrier_bytes.flatten().astype(np.uint8)
METADATA_BITS = 40
for candidate_n in range(1, 9):
bytes_for_metadata = math.ceil(METADATA_BITS / candidate_n)
if flat.size < bytes_for_metadata: continue
bitstream = _get_bitstream(flat, candidate_n, bytes_for_metadata)
if len(bitstream) < METADATA_BITS: continue
data_len = int(bitstream[:32], 2)
declared_n = int(bitstream[32:40], 2)
if declared_n != candidate_n: continue
total_bytes_to_read = math.ceil((5 + data_len) * 8 / declared_n)
full_bitstream = _get_bitstream(flat, declared_n, total_bytes_to_read)
full_bytes = binary_to_data(full_bitstream)
data_to_hide = full_bytes[5 : 5 + data_len]
if data_to_hide.startswith(MAGIC_NUMBER):
header_len = len(MAGIC_NUMBER)
is_encrypted = (data_to_hide[header_len] == 1)
packaged_data = data_to_hide[header_len + 1:]
return packaged_data, is_encrypted, True
return b'', False, False
Часть 3: Синергия: Шифрование + Стеганография = ❤️
Использование связки .cha
и стеганографии дает два независимых уровня защиты:
-
Уровень 1: Шифрование (Конфиденциальность и целостность): Вы берете ваш
my_plan.docx
и превращаете его вmy_plan.docx.cha
. Даже если злоумышленник его найдет, он не сможет его прочитать без пароля или незаметно изменить.Программа "ChameleonLab". Шифрование Уровень 2: Стеганография (Скрытность и правдоподобное отрицание): Вы берете полученный
my_plan.docx.cha
и встраиваете его в фотографиюkitten.png
. Теперь у постороннего наблюдателя нет даже оснований подозревать, что какой-то секретный файл вообще существует. Файл не просто защищен — он невидим.

Эта двухступенчатая защита — и есть философия ChameleonLab. Сначала мы делаем данные нечитаемыми, а затем делаем их невидимыми. Это обеспечивает правдоподобное отрицание: вы всегда можете утверждать, что это просто картинка, и доказать обратное будет практически невозможно.
Последнюю версию программы «Steganographia» от ChameleonLab для Windows и macOS можно скачать на нашем официальном сайте. https://chalab.ru
А чтобы быть в курсе обновлений, обсуждать новые функции и общаться с единомышленниками, присоединяйтесь к нашему Telegram-каналу: https://t.me/ChameleonLab
Спасибо за ваше внимание и интерес к проекту!
Комментарии (10)
andreymal
03.09.2025 06:42Теперь у постороннего наблюдателя нет даже оснований подозревать, что какой-то секретный файл вообще существует. Файл не просто защищен — он невидим.
Замечаем, что картинка какая-то подозрительно шумная, достаём младшие биты, видим S6KB и идём за паяльником
Хотя в общем-то даже замечать не надо, можно автоматизировать и просканировать вообще все картинки
kuza2000
03.09.2025 06:42Статья, бесспорно, полезная для изучения технологий. Но сигнатуру оставлять нельзя, уже написали.
Ну и метод встраивания в младшие биты обнаруживается очень легко, даже без сигнатуры. Перспективны методы, не изменяющие энтропии данных. Они есть, но что-то я не видел хорошей реализации.
kuza2000
03.09.2025 06:42Файл не просто защищен — он невидим.
Увы. Хорошо виден, даже если уберите сигнатуру. Не глазом виден, конечно, а с помощью простых и распространенных детекторов.
Lomakn Автор
03.09.2025 06:42Спасибо за комментарии и развёрнутые ответы. Мы методы будем менять и добавлять новые: DCT, F5 и другие. Нам важно на данном этапе, чтобы программа работала правильно и все работало с коробки. Дальше будем делать профессиональный режим.
kuza2000
03.09.2025 06:42Попробуйте это реализовать. Вот это было бы реально чума!)
Lomakn Автор
03.09.2025 06:42Спасибо еще раз pа статью. Это интересная обзорная статья (Survey Paper). Она не предлагает один конкретный новый метод для реализации, а анализирует и классифицирует множество уже существующих техник стеганографии и стегоанализа, в основном в контексте глубокого обучения и нейронных сетей.
Реализовать методы из этой статьи "в лоб" — это задача уровня научной диссертации, так как она требует создания сложных генеративных моделей (как GPT-2 для текста или WaveRNN для аудио), а не просто написания скрипта. Если это реализовать, тогда и вес программы будет в разы больше.
Но мы придумали свой 100% новый рабочий способ для игр в прятки с PDF. Думаю, через несколько недель его реализуем. Он будет связан со структорой PDF документов.
JBFW
Не раскрыт метод встраивания в картинку.
Не, то что так сделать можно - это понятно, вопрос как именно в данном случае? Может ли быть создана утилита, которая в автоматическом режиме просканирует картинку и извлечёт скрытые данные в пригодном для расшифровки виде, с сигнатурой например?
Lomakn Автор
Спасибо за интересные и правильные вопросы. Дополнили статью.