Перейти к содержимому

03 устройство

Как это устроено и чему не стоит верить на слово

Протокол

  1. Браузер делает случайный 256-битный ключ K. Если задан пароль, из него и случайной соли через PBKDF2-SHA256 (600 000 кругов) получается ещё 256 бит, и оба куска сливаются через HKDF.
  2. Текст и выбранный язык упаковываются в JSON и шифруются AES-256-GCM со случайным IV. Рядом шифруется короткая проверочная метка: по ней браузер понимает, что ключ и пароль верные, ещё до того, как записка сгорит.
  3. На сервер уходит POST /api/pastes с шифртекстом, меткой, сроком и флагом «сжечь». Сервер отвечает адресом записки и токеном удаления.
  4. Ссылка собирается в браузере: /p/<адрес>#<K>. Всё после # браузер не отправляет ни в запросе, ни в Referer.
  5. Получатель открывает ссылку. Страница читает K из фрагмента, проверяет метку, только потом забирает шифртекст, расшифровывает и показывает его как текст.

Что видит сервер

Хранится в базе (Cloudflare D1)
ПолеВидно серверуКомментарий
Текст запискинетТолько шифртекст AES-GCM. Расшифровать без K нельзя.
Язык подсветкинетЛежит внутри шифртекста.
Ключ KнетЖивёт во фрагменте URL, на сервер не отправляется.
ПарольнетНе отправляется и не проверяется сервером. Хранятся только соль и число кругов.
Размер шифртекстадаРазмер текста можно оценить с точностью до десятков байт.
Время создания и срокдаНужны для TTL и очистки.
Сжечь / пароль естьдаДва флага, без самого пароля.
Число просмотровдаСчётчик в строке записки.
IP-адресвременноДля ограничения частоты хранится только HMAC от IP в окне 10 минут, без самого адреса. Содержимое запросов не логируется.
Токен удалениятолько хэшВ базе SHA-256 токена.

Модель угроз

Защищает от

  • Утечки базы или бэкапа: там только шифртекст.
  • Оператора, который читает таблицы (вплоть до меня).
  • Подбора адресов записок: 128 бит и лимиты на промахи.
  • Повторного чтения «сжигаемой» записки, в том числе гонки из десятков запросов.
  • XSS через содержимое: оно выводится только как текст, CSP без inline-скриптов.
  • CSRF: только JSON и проверка Origin, cookie нет вообще.

Не защищает от

  • Подмены самого сайта: если сервер отдаст вредный JS, он увидит ключ. Это общее ограничение веб-приложений.
  • Утечки самой ссылки (мессенджер, история браузера, скриншот).
  • Слабого пароля у того, кто уже получил ссылку целиком.
  • Анализа метаданных: размер, время, число просмотров видны серверу.
  • Вредного расширения или заражённого устройства получателя.

Почему «сжечь» работает ровно один раз

Чтение сжигаемой записки это один SQL-блок в транзакции D1: DELETE … WHERE burn = 1 … RETURNING. SQLite выполняет записи по очереди, поэтому из сорока одновременных запросов строку получит только один, остальные увидят 404. KV для этого не годится: он только в итоге согласован, и две параллельные операции чтения могли бы обе увидеть значение. Это проверено тестом на 40 параллельных запросов.

Чтобы опечатка в ключе или пароле не сожгла записку, браузер сперва получает метаданные и проверочную метку без расхода записки (GET /api/pastes/:id/meta) и только после успешной проверки забирает шифртекст.

Ограничения

  • Потеряли ссылку с ключом, потеряли записку. Восстановления нет.
  • Пароль не заменяет ключ, а добавляется к нему: одной ссылки без пароля недостаточно.
  • Лимит частоты считается по IP из заголовка Cloudflare. Мобильные сети с общим IP могут упираться в него раньше.
  • Просроченные записки удаляются раз в 30 минут, а недоступны сразу по истечении срока.
  • Это учебный проект без внешнего аудита. Не используйте для того, от чего зависит безопасность людей.

API

curl -s -X POST https://ewsirdev-paste.ewsirdev.workers.dev/api/pastes \
  -H 'content-type: application/json' \
  -d '{"ct":"…","chk":"…","ttl":3600,"burn":true}'
# 201 {"id":"…","deleteToken":"…","expiresAt":1790000000}

curl -s https://ewsirdev-paste.ewsirdev.workers.dev/api/pastes/<id>/meta     # не расходует записку
curl -s https://ewsirdev-paste.ewsirdev.workers.dev/api/pastes/<id>          # читает (и сжигает, если burn)
curl -s -X DELETE https://ewsirdev-paste.ewsirdev.workers.dev/api/pastes/<id> -H 'authorization: Bearer <deleteToken>'

Шифровать нужно самим по схеме выше: полное описание в openapi.yaml. Сервер принимает только готовый шифртекст.