Создайте an Зашифрованный Обмен файлами App from Scratch
Step-by-step tutorial to build an encrypted файл sharing application. Frontend encryption, secure backend, и deployment walkthrough.
Создание приложения для зашифрованного обмена файлами означает, что криптография выполняется в браузере, а не на сервере. Минимальный стек выглядит так: фронтенд на Vite + React, который шифрует данные с помощью AES-256-GCM через Web Crypto API; бэкенд на Fastify, воспринимающий каждую загрузку как непрозрачный блоб; S3-совместимое хранилище (Cloudflare R2 или Backblaze B2); и короткая ссылка, в которой ключ дешифровки находится после # — так он никогда не достигает сервера. Рабочую версию можно выпустить примерно в 400 строк кода и хостить менее чем за 400 рублей в месяц.
Архитектурные решения, которые реально важны
Самое важное решение — где хранится ключ шифрования. Если он хоть раз коснётся вашего сервера, у вас не сквозное шифрование, а серверное с лишними шагами. Правильный подход: сгенерировать случайный 256-битный ключ в браузере, зашифровать им файл, загрузить шифротекст и поместить ключ во фрагмент URL (https://yourapp.com/f/abc123#k=base64key). Браузеры никогда не отправляют фрагменты в HTTP-запросах, поэтому ключ остаётся на стороне клиента.
Второе решение: чанковые загрузки. Для файлов больше 100 МБ нужны возобновляемые составные загрузки — иначе любой сбой Wi-Fi прервёт передачу. API мультизагрузки S3 поддерживает чанки от 5 МБ и до 10 000 частей, давая потолок в 50 ГБ. Планируйте это с первого дня.
Создание скелета проекта
Начните с двух пакетов: фронтенд Vite + React и бэкенд Fastify. Фронтенд берёт на себя всю криптографию, бэкенд — хранение и метаданные. Используйте TypeScript для типобезопасности при работе с ArrayBuffer и CryptoKey.
pnpm create vite@latest frontend -- --template react-ts
pnpm create fastify backend
Добавьте на бэкенд эти зависимости: @fastify/multipart, @aws-sdk/client-s3, @aws-sdk/s3-request-presigner и better-sqlite3 для метаданных ссылок. Схема SQLite должна быть минимальной: shares(id, object_key, size_bytes, expires_at, download_count, max_downloads). Никаких имён файлов, никаких пользовательских данных, никаких IP.
Написание конвейера шифрования на фронтенде
Сгенерируйте ключ, зашифруйте с помощью AES-256-GCM и получите блоб шифротекста плюс ключ в base64 для фрагмента URL:
async function encryptFile(file: File) {
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, key, plaintext
);
const rawKey = await crypto.subtle.exportKey('raw', key);
const blob = new Blob([iv, new Uint8Array(ciphertext)]);
return { blob, keyBase64: toBase64(rawKey) };
}
Для файлов свыше 50 МБ замените это потоковой версией, которая шифрует чанки по 4 МБ и добавляет их в ReadableStream. Куча памяти мобильного Safari не справляется ни с чем больше ~400 МБ в одном ArrayBuffer.
Проектирование эндпоинта загрузки
Бэкенд не должен знать ничего полезного. Принимайте POST с шифротекстом, генерируйте случайный URL-безопасный ID из 16 символов, сохраняйте в SQLite с истечением срока и стримьте тело напрямую в S3:
fastify.post('/upload', async (req, reply) => {
const id = nanoid(16);
const key = `blobs/${id}`;
const upload = new Upload({
client: s3,
params: { Bucket: 'hexa-transfers', Key: key, Body: req.raw }
});
await upload.done();
db.prepare('INSERT INTO shares VALUES (?, ?, ?, ?, 0, ?)').run(
id, key, req.headers['content-length'], Date.now() + 7*86400*1000, 10
);
return { id };
});
Срок истечения по умолчанию — 7 дней, максимум 10 загрузок. Строгие дефолты нужны, иначе хранилище растёт неконтролируемо. Удаляйте истёкшие блобы по крону каждую ночь.
Генерация ссылок с ключами во фрагменте
После завершения загрузки формируйте URL ссылки на стороне клиента:
const { id } = await uploadResponse.json();
const shareUrl = `${location.origin}/f/${id}#k=${keyBase64}`;
Этот фрагмент никогда не покидает браузер пользователя. Когда получатель переходит по ссылке, React-приложение читает window.location.hash, парсит ключ, загружает шифротекст и дешифрует локально. У сервера нет пути к открытому тексту, кроме как подсунуть пользователю вредоносный JavaScript — именно поэтому нужно развернуть строгую Content Security Policy и закрепить хеши подресурсов.
Реализация процесса скачивания
На странице скачивания получайте шифротекст потоком и расшифровывайте с выравниванием по чанкам:
const keyRaw = base64ToBytes(location.hash.slice(3));
const key = await crypto.subtle.importKey(
'raw', keyRaw, { name: 'AES-GCM' }, false, ['decrypt']
);
const resp = await fetch(`/blob/${id}`);
const iv = new Uint8Array(await resp.body.getReader().read().then(r => r.value.slice(0, 12)));
const ct = await resp.arrayBuffer();
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct.slice(12));
const url = URL.createObjectURL(new Blob([pt]));
Для больших файлов используйте StreamSaver.js или File System Access API, чтобы расшифрованные байты шли прямо на диск, а не в RAM.
Деплой всего вместе
Фронтенд: пуш на Cloudflare Pages или Vercel — бесплатные тиры это обслуживают. Бэкенд: один дроплет DigitalOcean за $5 с Node 22 за Caddy (автоматический TLS 1.3). Хранилище: Cloudflare R2 даёт нулевую плату за исходящий трафик — это убийственное преимущество для файлового обмена на масштабе. WeTransfer, Smash и SwissTransfer горят на исходящем трафике AWS; R2 полностью меняет экономику.
Настройте строгие заголовки через Caddy: Strict-Transport-Security, Content-Security-Policy: default-src 'self'; script-src 'self', X-Content-Type-Options: nosniff и Referrer-Policy: no-referrer. Они закрывают типичные векторы утечки фрагмента URL через рефереры или внедрённые скрипты.
Приложение, соответствующее GDPR по умолчанию
Поскольку сервер видит только шифротекст, у вас практически нет данных, квалифицируемых как персональные по статье 4 GDPR. Тем не менее задокументируйте отношения с процессорами данных (R2 и DigitalOcean), установите максимальный срок хранения блобов в 30 дней и опубликуйте понятное уведомление о том, что ссылки содержат ключи и должны передаваться по доверенным каналам. Добавьте ограничение скорости: 50 загрузок с одного IP в час — для борьбы со злоупотреблениями без логирования личностей пользователей. Если вы обслуживаете российских пользователей, убедитесь, что серверы первичной обработки находятся на территории РФ согласно требованиям 152-ФЗ.
HexaTransfer построен именно по такой архитектуре — Web Crypto в браузере, непрозрачное хранилище блобов, ключи во фрагментах URL, ноль открытого текста на серверах. Попробуйте на hexatransfer.com — бесплатно, без аккаунта, до 10 ГБ.
Что добавить после запуска MVP
После выхода базового функционала наиболее ценные дополнения: защита паролем поверх ключа фрагмента (PBKDF2 с 600 000 итерациями), уведомления по email на одноразовый ящик о каждой загрузке и жалобы на злоупотребления, позволяющие флагировать хеш шифротекста без раскрытия содержимого. Не добавляйте аккаунты без конкретной причины — они мгновенно превращают ваш инструмент конфиденциальности в обязательство по персональным данным. Выпускайте малое, придерживайтесь позиции, и пусть криптография делает тяжёлую работу.
Безопасная отправка больших файлов со сквозным шифрованием
Передавайте файлы до 10 ГБ бесплатно со сквозным шифрованием. Регистрация не требуется. Ваши файлы шифруются в браузере перед загрузкой — никто другой не может их прочитать.
Отправить файл