Я не хотел, чтобы WeTransfer читал мои файлы, и написал хранилище, которое не доверяет само себе

от автора

Сразу дисклеймер: я ничего не продаю. share·me, бесплатный open-source проект под AGPL, без тарифов, подписок и регистрации. Это история про то, как обычная задача «передать человеку файл» бесила меня ровно столько раз, что в итоге я сел и написал своё.

Передать файл и не отдать его половине интернета

Сценарий до боли знакомый: надо скинуть коллеге пароль от сервера, или документ, или дамп базы. Открываешь варианты и тихо вздыхаешь.

WeTransfer и аналоги видят всё, что ты загрузил, по определению. Почта и телеграм оставляют файл лежать в плейнтексте навсегда, на всех серверах по пути. PrivateBin отличный, но это только текст, файл на пару гигабайт туда не положишь. А хочется простого: бросил файл, получил ссылку, и сервер при этом физически не может его прочитать. Причём сервер мой.

В какой-то момент я перестал искать и написал share·me.

Идея на салфетке: ключ кладём в ссылку, серверу не показываем

Браузер генерирует случайный ключ и шифрует файл прямо у тебя на клиенте через AES-256-GCM. А ключ отправляется вот сюда:

https://share.example/d/AbC123#k=<base64-ключ>

Всё, что после #, браузер никогда не отправляет на сервер. Этого нет ни в строке запроса, ни в логах сервера, ни в access-логах прокси, который ты воткнул спереди. Ссылка несёт ключ, а сервер видит только шифротекст, включая имена файлов. Вот и весь фокус, до неприличия простой.

Не хочешь ключ в ссылке? Есть парольный режим: ключ выводится через Argon2id (фолбэк PBKDF2). Неверный пароль просто не проходит серверную авторизацию, а она с rate-limit, так что офлайн-перебора нет.

Нюанс, про который туториалы скромно молчат: 5 ГБ в память не положишь

Любой гайд «зашифруй файл через WebCrypto» вызывает encrypt(весь_файл). Попробуй так с загрузкой на 5 ГБ, и вкладка ловит OOM. Реальные файлы надо стримить.

Поэтому крипто-пакет использует сегментированный AES-256-GCM STREAM: файл режется на чанки, каждый шифруется на HKDF-производном подключе со счётчиком-нонсом. Две вещи, на которых я споткнулся и которые стоит назвать вслух:

  • Нонсы чанков должны быть детерминированными и упорядоченными. Иначе ты не расшифруешь поток, который не буферизовал целиком. Счётчики, а не случайные нонсы.

  • AES-GCM не key-committing. Один шифротекст можно подделать так, что он чисто расшифруется под двумя разными ключами. Для сервиса обмена файлами это реальная мина, поэтому есть key-committing заголовок, привязывающий шифротекст ровно к одному ключу.

Браузер ↔ API стримят напрямую, Rust-сервис не держит файл в памяти целиком.

Архитектура

Браузер (AES-256-GCM на клиенте)   │   ▼Traefik ──/api/*──► Rust / axum API ──► блобы (диск или S3) + метаданные (SQLite/Postgres)   └─────/*──────► Next.js BFF

Один origin через Traefik, а значит, никакого CORS. Тонкий BFF на Next.js (Server Actions) держит owner-токен каждого дропа в httpOnly-cookie; большие блобы идут мимо него, прямо в API. Срок жизни, лимит скачиваний, burn-after-reading и time-lock enforced на сервере, клиент не сможет их обмануть.

Поднять у себя

git clone https://github.com/onokashino/share-me.git && cd share-medocker compose up --build   # http://localhost

Указал DOMAIN + ACME_EMAIL, и Traefik сам выпустит сертификат Let’s Encrypt. Готовые образы лежат на ghcr.io. Хватает 1 vCPU / 1 ГБ RAM: упирается в диск и трафик, а не в процессор.

Одна честная оговорка: стороннего аудита пока нет. Криптография специально вынесена в один небольшой пакет с минимумом зависимостей, чтобы её реально можно было прочитать за один присест. Буду рад, если кто-то знающий поищет в ней дыры. Лицензия AGPL-3.0.

ссылка на оригинал статьи https://habr.com/ru/articles/1045242/