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