v1 プロトコルを、 正確に。
以下のすべては、凍結され公開されたコードに対して書かれています。すべての定数は リポジトリで確認でき、実装間テストベクターがバイトレベルの挙動を固定します。このページとコードが食い違うことがあれば、コードが真実であり、その食い違いは報告するに値するバグです。
短い要約
- ノート本文は送信者の端末で暗号化されます。どのサーバーもそれを見ることはありません。
- 復号鍵はリンクのフラグメント、つまり
#より後の部分だけを移動し、ブラウザはそれをどのサーバーにも送りません。 - 復号は受信者のブラウザ内で、誰でもソースの表示で読める JavaScript で行われます。
- 焼却・有効期限・状態を支える管理トークンは、ノートを制御できても復号は決してできません。サーバーはその SHA-256 ハッシュだけを保存します。
- 作成完了画面を閉じた後は、完全なリンクを再構築できません。アプリはノート id とトークンだけを保持し、鍵は決して保持しません。
リンク
burnpony.app/n/{id}#{key}{flags}。id はサーバーが 16 バイトの乱数から作る 22 文字の base64url です。key は、送信者の端末で生成された正確に 32 バイトの乱数をエンコードする、パディングなしの base64url 43 文字です。任意の flags 接尾辞はピリオドと文字で、p はパスフレーズが必要、r は送信者が開封確認を有効にしたことを意味し、ページは閲覧を消費する表示の前にその両方を伝えられます。# より後はすべて URL フラグメントで、ブラウザはフラグメントをリクエストで送りません。鍵はどのサーバーにも届かず、どのログにも現れず、保存されるノートの一部でもありません。この一つの性質が設計全体を支えています。
エンベロープ
サーバーが保存するのは、1 つの不透明な JSON 文字列です: {"v":1,"pw":false,"salt":"…","nonce":"…","ct":"…"} — フォーマットのバージョン、パスフレーズフラグ、16 バイトのソルト、12 バイトのノンス、そして 16 バイトの認証タグを付加した AES-256-GCM 暗号文で、すべて標準の 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 で 60 万回反復して強化され、まずフラグメント鍵に付加されます。パスフレーズは、どの実装でも強化前に正規化されます: Unicode NFC 正規化、続いて U+FEFF を含む先頭と末尾の空白の除去。うっかりのキーボード空白や分解された Unicode 入力は許されますが、大文字小文字や内部の空白の変更は意図的に許されません。誤ったパスフレーズと改ざんされた暗号文は、どちらも単に GCM 認証に失敗し、両者を区別するものはありません。
API
/api の下に 6 つの操作があり、すべて JSON です。POST /notes は、閲覧上限(1–100)、有効期限(300〜2,592,000 秒)、確認フラグを持つエンベロープを保存し、ノート id と使い捨ての 64 桁 16 進の管理トークンを返します。GET /notes/{id} はトランザクション内で 1 回分の閲覧を消費し、上限に達すると暗号文を null にします。管理トークンを使うと: GET …/status はカウンタと確認時刻を報告し、DELETE は即座に焼却し、PATCH は生きているノートを今からの計算で再設定し、POST /notes/{id}/push は確認配信のために最大 5 つの APNs または FCM トークンを登録します。POST /report/{id} は閲覧を消費せずに不正利用の報告を行います。トークンはサーバー側で SHA-256 ハッシュとしてのみ保存され、誤ったトークン・焼却済みノート・存在しなかったノートを区別しうるあらゆる失敗は、バイト単位で同一の {"error":"not_found"} を返します — その一定さは意図的です。
サーバーが保存するもの
暗号文(焼却または枯渇時に null 化)、閲覧カウンタ、タイムスタンプ、トークンハッシュ、確認タイムスタンプ、確認ノート用のプッシュトークン、ローカルの秘密でハッシュ化されたレート制限カウンタ、そして不正利用の報告。アカウントなし、メールなし、生の IP アドレスなし、解析なし。スイーパーが期限切れの行を削除しますが、期限は読み取り時にも適用されるため、死んだ cron がノートの寿命を延ばすことはありません。
開封確認、その仕組み
確認は、受信者が表示する前に開示されます — r フラグはそのために存在します。プッシュのペイロードは意図的に一般化されています: 通知はノートが開かれたことだけを告げ、唯一のカスタムキーはノート id です。送信者自身の端末が、その端末以外のどこにも存在しないラベルを使って通知をローカルで書き換えます。サーバーはいかなるラベルも知りません。
自分で検証する
3 つのチェック、信頼は不要です。1 つ: 受信者ページは再現可能です — リポジトリの scripts/assemble_viewer.py --verify は、配信される viewer.html がその 3 つの監査可能な部分とバイト単位で同一であることを証明し、任意のライブノートでソースの表示を使えば公開ファイルと比較できます。2 つ: 暗号は固定されています — shared/burnpony_vectors.json は独立した実装からの基準真値を保持し、Swift テストスイートと組み込みの WebCrypto コアの両方がすべての暗号文を正確に再現します。3 つ: サーバーの貧しさは読み取れます — server/schema.sql と API ルーターは一気に読めるほど短く、その中に漏れるものは何もありません。正直な限界はいずれにせよ残ります: 表示されたノートは常にコピーや撮影ができ、運営者は公開されたものと異なるコードを配信しうる — だからこそページは再現可能で、この仕様が存在し、差し替えを検出できるのです。