Сразу про название. .fsecurity — файловое обозначение FlautSecurity, отдельной части инфраструктуры FlautCompany. Дальше в статье я буду использовать именно короткое название .fsecurity.
Идея появилась из довольно простой проблемы: если приложение хранит зашифрованные файлы, где-то должны находиться ключи. Часто их кладут в БД, конфигурацию или секрет-хранилище. Я решил построить архитектуру так, чтобы база данных вообще не содержала ключей, а .fsecurity стал отдельным криптографическим слоем внутри проекта.
При этом .fsecurity — не сервер, не прокси и не отдельный процесс. Это Python-модуль без собственного IP, порта и внешнего API.
Как работает .fsecurity
Для тестирования технологии я создал Telegram-бота @flautcloud_bot — облачное хранилище, на котором проверяю систему в реальных условиях.
Схема загрузки выглядит так:
Telegram ↓ Application ↓ .fsecurity ↓ AES-256-GCM внутри .fsecurity ↓ Зашифрованный файл ↓ Storage
Пользователь отправляет файл, .fsecurity генерирует для него уникальный криптографический ключ и шифрует данные с помощью AES-256-GCM.
Для каждого файла используется отдельный ключ:
file A → K1 file B → K2 file C → K3
Ключи не вычисляются из user_id, file_id, имени файла или времени. Поэтому нельзя просто написать:
key = sha256(str(user_id).encode())
и получить ключ пользователя.
Ключ должен быть независимым криптографическим секретом.
В базе остаются только метаданные:
file_id user_id filename storage_id size created_at
А encryption_key там отсутствует.
Что происходит с файлами
Здесь я решил добавить ещё один уровень защиты.
Зашифрованные файлы не должны находиться рядом с основной БД и обычной файловой системой приложения.
Storage должен быть отдельным закрытым хранилищем. Каждому файлу назначается случайный непрозрачный storage_id:
file_id: 18421 storage_id: 9c7f2a81d04e...
Сам файл может выглядеть примерно так:
9c7f2a81d04e....bin
Никаких:
/users/ivan/passport.pdf
или:
/928374/photo.jpg
Злоумышленник не должен получать возможность угадать расположение файлов по имени, user_id или последовательному ID.
В идеальном варианте Storage вообще находится в отдельном изолированном сегменте или на отдельном сервере. Тогда компрометация приложения не означает автоматический доступ к зашифрованным файлам.
Получается своеобразное разделение:
Database │ метаданные │ × │ .fsecurity │ ключи │ × │ Storage ciphertext
Чтобы получить исходный файл, атакующему нужно одновременно получить несколько независимых компонентов и правильно сопоставить их между собой.
А если украли .fsecurity?
Это уже серьёзный сценарий.
Допустим, злоумышленник получил .fsecurity и Database.
При этом самих файлов у него всё ещё нет.
Он получает код криптографического модуля, внутреннее состояние и содержимое базы с метаданными. Но Storage находится отдельно, а файлы внутри него представлены только в зашифрованном виде.
Теперь предположим обратное: злоумышленник получил Storage.
Перед ним будет примерно такой набор:
9c7f2a81d04e....bin 18af71c9b2e1....bin 7d441ac82f03....bin
Это шифротекст, а не исходные документы.
Даже если злоумышленник дополнительно получил базу данных, ему всё равно необходимо установить соответствие между конкретным файлом, пользователем и ключом.
Ключи для файлов независимы друг от друга и не вычисляются из user_id, file_id, имени файла или других публичных параметров.
То есть нельзя просто взять:
user_id = 928374 file_id = 18421
и математически получить ключ от нужного файла.
Не получится и просто перебрать несколько вариантов по имени пользователя или имени файла. Криптографический ключ генерируется как случайный секрет, а не как производное от этих данных.
Именно здесь работает идея разделения доверия: один компонент сам по себе не должен давать возможность восстановить пользовательский файл.
Конечно, если атакующий получает полный контроль над всей инфраструктурой одновременно, включая сервер, .fsecurity, Storage и механизм управления ключами, ситуация уже принципиально другая. Об этом ниже.
Почему файлы должны иметь случайные идентификаторы
Есть ещё одна небольшая, но важная деталь.
Не стоит хранить файл как:
/users/12345/photo.jpg
Если злоумышленник получил доступ к файловой системе, такая структура сама рассказывает ему слишком много.
Гораздо интереснее:
/storage/9c/9c7f2a81d04e8a....bin
Причём storage_id должен быть криптографически случайным и не иметь связи с user_id или последовательным file_id.
В таком случае даже сама файловая структура не превращается в готовую карту пользовательского хранилища.
Это не заменяет шифрование, но добавляет ещё один слой защиты: затрудняется обнаружение и сопоставление объектов.
Можно усилить ещё больше
Для инфраструктуры с особенно высокими требованиями я бы добавил внешний KMS или HSM.
Тогда критический ключевой материал можно вынести за пределы самого сервера:
.fsecurity ↓ KMS/HSM ↓ ключевой материал ↓ AES-256-GCM ↓ шифротекст
В таком варианте даже получение полного доступа к серверу приложения не означает автоматического получения всех ключей.
Это уже более серьёзная архитектура, требующая дополнительной инфраструктуры, политик доступа и резервирования.
Но базовая идея .fsecurity остаётся прежней: ключи не должны лежать в БД рядом с пользовательскими данными.
Почему AES-256-GCM?
Здесь я сознательно не изобретаю собственную криптографию.
Для шифрования используется AES-256-GCM — проверенный криптографический примитив, который обеспечивает конфиденциальность и проверку целостности.
Если кто-то изменит зашифрованный файл, проверка должна завершиться ошибкой.
Это принципиальный момент. Собственную архитектуру безопасности придумать можно. Собственный алгоритм шифрования — лучше не надо.
Иначе через пару лет кто-нибудь найдёт в коде функцию:
my_super_secure_encrypt()
и спросит, почему она состоит из XOR и трёх строк Python.
Что происходит при скачивании?
Обратная операция выглядит примерно так:
Пользователь ↓ Telegram ↓ Application ↓ .fsecurity ↓ получение нужного ключа ↓ проверка целостности ↓ AES-256-GCM decrypt ↓ файл ↓ Telegram ↓ Пользователь
Основному приложению не требуется самостоятельно хранить или обрабатывать ключ.
Оно передаёт операцию .fsecurity, а криптографическая логика остаётся внутри этого модуля.
Именно поэтому .fsecurity можно воспринимать как отдельную границу между обычной логикой приложения и криптографией.
Почему .fsecurity — не отдельный сервер?
Можно было построить архитектуру так:
Application ↓ Crypto Server ↓ Storage
Но тогда появляется ещё один сетевой сервис.
А значит, появляются: отдельный порт, API, авторизация, TLS; firewall, сетевые ACL, мониторинг, дополнительная поверхность атаки, ещё один компонент, который нужно обновлять и защищать.
Мне хотелось сделать иначе:
Application ↓ .fsecurity ↓ Storage
Если компоненту не нужен сетевой интерфейс, зачем его создавать?
Поэтому .fsecurity — именно модуль внутри проекта, а не отдельный сервер.
Но это не «невзламываемая технология»
Здесь я специально не хочу использовать формулировку «невозможно взломать».
Так не бывает.
Если злоумышленник получает полный контроль над сервером, он потенциально может читать файлы, менять код, исследовать память процессов и вмешиваться в выполнение приложения.
.fsecurity не отменяет безопасность операционной системы, хостинга и всей остальной инфраструктуры.
Его задача намного конкретнее:
Один украденный компонент не должен автоматически превращаться в украденные пользовательские файлы.
Если украли только БД — ключей там нет.
Если украли только Storage — там шифротекст.
Если получили .fsecurity без доступа к хранилищу — самих пользовательских файлов нет.
Если всё это находится в разных изолированных компонентах, атакующему приходится компрометировать сразу несколько уровней системы.
Именно это и есть основная идея архитектуры.
Зачем вообще всё это?
Для меня .fsecurity — не просто файл с функцией шифрования.
Для тестирования я создал Telegram-бота с функциями облачного хранилища, где проверяю загрузку, скачивание, шифрование, ошибки, восстановление после сбоев и реальные сценарии эксплуатации. Так что, пожалуйста, не посчитайте, что я все это говорю без проверок.
Бот нужен именно как испытательный стенд. Главная цель проекта — сама технология .fsecurity.
В дальнейшем .fsecurity планируется отделить от конкретного Telegram-бота и сделать самостоятельным решением, которое можно будет подключать к другим проектам.

