// the specification

Протокол v1, точно.

下面的一切都是依据冻结并已发布的代码撰写的。每个常量都可在 репозитории中核查,跨实现测试向量固定了字节级行为。如果本页与代码有任何不一致,以代码为准,而该不一致是一个值得сообщить的缺陷。

Кратко

  • Текст записки шифруется на устройстве отправителя. Ни один сервер его никогда не видит.
  • Ключ расшифровки передаётся только во фрагменте ссылки — части после #, — которую браузеры никогда не отправляют ни на один сервер.
  • Расшифровка происходит в браузере получателя, на JavaScript, который любой может прочитать через «Просмотр исходного кода».
  • Токен управления, стоящий за сжиганием, сроком и статусом, может управлять запиской, но никогда её не расшифрует. Сервер хранит только его SHA-256-хеш.
  • Полную ссылку нельзя восстановить после закрытия экрана созданной записки. Приложение хранит только id записки и токен, но никогда ключ.

Ссылка

burnpony.app/n/{id}#{key}{flags}. id — это 22 символа base64url, которые сервер чеканит из 16 случайных байтов. key — это 43 символа base64url без дополнения, кодирующие ровно 32 случайных байта, сгенерированных на устройстве отправителя. Необязательный суффикс flags — это точка плюс буквы: p означает, что нужна парольная фраза, r — что отправитель включил уведомление о прочтении, так что страница может сообщить вам обе вещи до раскрытия, расходующего просмотр. Всё после # — фрагмент URL, а браузеры не отправляют фрагменты в запросах. Ключ никогда не достигает сервера, не появляется ни в одном журнале и не входит в хранимую записку. Одно это свойство несёт весь замысел.

Конверт

То, что хранит сервер, — это одна непрозрачная строка JSON: {"v":1,"pw":false,"salt":"…","nonce":"…","ct":"…"} — версия формата, флаг парольной фразы, 16-байтовая соль, 12-байтовый нонс и шифртекст AES-256-GCM с добавленным 16-байтовым тегом аутентификации, всё в стандартном base64. Порядок ключей в конверте не важен; он разбирается как JSON.

Полезная нагрузка, байт за байтом

Открытый текст под шифрованием — это {"v":1,"t":"<text>","ah":<seconds>} — записка (до 50 000 символов) и таймер автоскрытия. Его сериализация детерминирована в каждой реализации: ключи ровно в этом порядке, без пробелов, экранирование ограничено \" \\ \n \r \t \b \f плюс \u00XX для остальных управляющих символов, всё прочее выводится буквально как UTF-8. Эталонные реализации на Swift, WebCrypto и Python выдают одинаковые байты, и общие векторы это обеспечивают.

Вывод ключа

Ключ шифрования — это HKDF-SHA256(ikm, salt, info = "BurnPony-v1-key", 32 bytes). Без парольной фразы ikm — это ключ фрагмента. С фразой она растягивается PBKDF2-HMAC-SHA256 за 600 000 итераций над той же солью и добавляется к ключу фрагмента спереди. Парольные фразы канонизируются перед растяжением в каждой реализации: нормализация Unicode NFC, затем обрезка ведущих и конечных пробелов, включая U+FEFF. Случайный пробел с клавиатуры или декомпозированный ввод Unicode прощается; изменения регистра и внутренних пробелов — намеренно нет. Неверная фраза и изменённый шифртекст оба просто не проходят аутентификацию GCM; ничто их не различает.

API

Шесть операций под /api, все в JSON. POST /notes сохраняет конверт с лимитом просмотров (1–100), сроком (от 300 до 2 592 000 секунд) и флагом уведомления, возвращая id записки и одноразовый 64-символьный шестнадцатеричный токен управления. GET /notes/{id} расходует один просмотр внутри транзакции и обнуляет шифртекст при достижении лимита. С токеном управления: GET …/status сообщает счётчики и время уведомлений, DELETE сжигает немедленно, PATCH перезадаёт срок живой записки, считая от текущего момента, а POST /notes/{id}/push регистрирует до пяти токенов APNs или FCM для доставки уведомлений. POST /report/{id} подаёт жалобу на злоупотребление, не расходуя просмотр. Токен хранится на сервере только как SHA-256-хеш, и каждая ошибка, которая могла бы отличить неверный токен от сожжённой записки и от записки, которой никогда не было, возвращает байт-в-байт одинаковый {"error":"not_found"} — постоянство намеренно.

Что хранит сервер

Шифртекст (обнуляется при сжигании или исчерпании), счётчики просмотров, метки времени, хеш токена, метки времени уведомлений, push-токены для записок с уведомлениями, счётчики ограничения частоты, хешированные с локальным секретом, и жалобы на злоупотребления. Без аккаунтов, без адресов эл. почты, без сырых IP-адресов, без аналитики. Сборщик удаляет истёкшие строки, но истечение также применяется при чтении, так что мёртвый cron никогда не продлевает жизнь записки.

Уведомления о прочтении, механически

Уведомления раскрываются получателю до того, как он раскроет записку — именно для этого существует флаг r. Полезная нагрузка пуша намеренно обобщена: оповещение сообщает лишь, что записка была открыта, и единственный пользовательский ключ — это id записки. Собственное устройство отправителя локально переписывает уведомление, используя метку, которая не существует нигде, кроме этого устройства; сервер не знает никакой метки.

Проверьте сами

Три проверки, доверие не требуется. Одна: страница получателя воспроизводима — scripts/assemble_viewer.py --verify в репозитории доказывает, что поставляемая viewer.html байт в байт идентична своим трём проверяемым частям, а «Просмотр исходного кода» на любой живой записке позволяет сравнить с опубликованным файлом. Две: криптография зафиксирована — shared/burnpony_vectors.json хранит эталонную истину от независимой реализации, и как набор тестов на Swift, так и встроенное ядро WebCrypto воспроизводят каждый шифртекст точно. Три: бедность сервера читаема — server/schema.sql и маршрутизатор API достаточно коротки, чтобы прочитать за один присест, и в них нечего утекать. Честные ограничения остаются в любом случае: раскрытую записку всегда можно скопировать или сфотографировать, а оператор мог бы выдавать код, отличный от опубликованного — именно поэтому страница воспроизводима и существует эта спецификация, чтобы подмена была обнаружима.

Более дружелюбный обзор той же механики — в документации; модель безопасности и путь сообщения — на странице безопасности.