インフラ担当がデータベースのパスワード、API キー、復旧コードを同僚へ渡すとき、いちばん安いのは Slack や Chatwork、Teams へそのまま貼ることだ。メッセージは検索できる。端末間で同期する。半年後にも残っている。「ワンタイムリンク」に切り替えても、復号鍵を暗号文の番号の後ろに ?key= と書く人が多い。そうすると預かり先、リバースプロキシ、アクセスログのすべてが鍵を見る。暗号化は半分で止まる。

ここでは「ワンタイムの画面でどのボタンを押すか」は扱わない。それはツールページの仕事だ。問いは一つだけだ。パスワードを一度だけ送るとき、なぜ復号鍵を URL の # の後ろに置くのか。サーバーは何を見なくなるのか。その守りはどこで終わるのか。前の記事では平文が送られていないことを Network で確かめる手順を書いた。同じパネルで、今度は「鍵が URL のどの切片に住んでいるか」を見る。MyPassGen のワンタイムリンクはこの境界で動く。AES-256-GCM はブラウザ内で終え、サーバーが一時保管するのは暗号文だけ。鍵は # の後ろ。作成も閲覧も登録は要らない。

先に URL を二つの対象に分ける

疑問符 ? の後ろはクエリーだ。HTTP のリクエスト行に乗り、対向のアクセスログにも残る。シャープ # の後ろはフラグメントだ。仕様はクライアント側の処理に残す。鍵を違う切片へ置けば、あとから「ゼロ知識」と言っても立たない。

同じパスワードを送る三つのやり方

捨ててよいテスト用パスワード orange-lake-7 を、少なくとも三通りで送れる。差は画面の宣伝文句ではない。平文が先に暗号文になったか、鍵が HTTP に入ったか、半年後に原文を検索できるか、だ。

やり方 預かり先やチャットが受け取るもの あとから残るもの
パスワードをチャットへ貼る 平文そのもの 検索できる原文
暗号化リンク。鍵を ?key= と書く 暗号文と鍵(どちらもリクエスト上) アクセスログの鍵。チャットの完全な URL
暗号化リンク。鍵を # の後ろへ サーバーが見るのは暗号文の番号。チャットは URL 全体を残し得る サーバーログに鍵は無い。チャット履歴には完全なリンクが残り得る

三行目が答えるのは「暗号文を預かる機械は鍵を受け取るべきではない」だけだ。「チャットが URL 全体を保存するか」には答えない。リンク全体は資格情報だ。# 付きのアドレスをコピーした人は、ブラウザで復号できる。鍵をハッシュの後ろへ置くのは、サーバーへの信頼を減らすためだ。転送されたリンクは解けない。

この経路に向くのは短いテキストだ。MyPassGen は 1 件の平文を 32 KB までに限る。パスワード、鍵の断片、短い説明向きだ。証明書の束、書き出した表、それを超える体積はファイル暗号へ。端末で .lock / .enc を作り、パスフレーズは別経路で送る。ファイルの場合はクラウドへ上げる前に平文を誰が見るかで書いた。

HTTP が実際に送るもの

RFC 3986 §3.5 は # の後ろを fragment identifier と呼ぶ。主資源を取ったあと、クライアントが解釈する二次的な位置だ。ブラウザはまずパスとクエリーでページを取り、資源が着いてからフラグメントでスクロール位置を決めるか、ページスクリプトへ渡す。HTTP リクエスト自体に、この切片は要らない。

現行の HTTP 意味論はさらに硬い。RFC 9110 §7.1 は、ターゲット URI が参照のフラグメント成分を含まないと書く。フラグメントはクライアント側の処理に予約されているからだ。リクエスト行として合法な origin-form は、パスと任意のクエリーだ。# の生成規則は無い。アドレスバーに s.html?id=abc#鍵 と見えても、タブから出る文書リクエストは GET /ja/s.html?id=abc であるべきだ。ハッシュの後ろはこの端末に残る。

Referer もフラグメントを剥がす。MDN の Referer は、このヘッダーがオリジン、パス、クエリーを運べると書く。URL フラグメントとユーザー名・パスワードは運べない。W3C の Referrer Policy も、送信前の除去手順でフラグメントを空にする。だから「復号鍵を # の後ろに置けば、暗号文を預かる機械へ HTTP では届かない」はプロトコルの振る舞いだ。あるサイトの口約束ではない。

疑問符とシャープは入れ替えられない

WHATWG URL では、http / https を開くとき ? の後ろはリクエスト行に入る。s.html?id=abc&key=鍵 と書けば、鍵はアクセスログ、リバースプロキシ、一部の CDN 記録に出る。公開リンクを転送するとき消してよい計測パラメータは別の問題だ。消してよいパラメータと残すパラメータを見よ。復号鍵をクエリーへ移してはいけない。

符号化の罠もある。%23 は # のパーセントエンコードだ。パスやクエリーに書いた %23 はサーバーへ送られ、復号すると文字どおりのシャープになる。フラグメントではない。鍵はアドレスバーの、エンコードされていない # の後ろに置かねばならない。パスへ折り込んだ %23 の後ろではない。

サーバーログから落ちるもの

一度きりの送信は二段階に分かれる。作成時、タブは 32 バイトの乱数鍵を作り、平文を AES-256-GCM で暗号化する(IV は 12 バイト。暗号文と一緒に置く)。そのあと暗号文、有効期限、閲覧回数を POST する。閲覧時、スクリプトは location.hash から鍵を取り、サーバーには暗号文だけを求め、この端末で復号する。リンクの形は s.html?id={id}#{key} だ。クエリーにあるのは番号だけ。鍵はハッシュの後ろだけだ。

NIST SP 800-38D は GCM を認証付き暗号として定める。暗号文が改ざんされていれば復号は失敗する。乱れた本文をメモとして受け取ってはならない。RFC 5116 の AEAD_AES_256_GCM は 32 バイト鍵、12 バイト nonce、16 バイト認証タグを使う。MDN の AesGcmParams も 96 ビット IV を勧め、同じ鍵での暗号化ごとに新しい IV を使えと書く。これらの数字は実装で照合できる。

だからサーバーログに出てよいのは、暗号文の番号 id、作成時の JSON フィールド ciphertext / ttl_hours / max_reads、あとからの暗号文 GET だ。出ていけないのは平文、32 バイト鍵、アドレスバーの # の後ろだ。有効期限は 1 時間、24 時間、7 日から選べる。閲覧回数だけで破棄してもよい。回数は 1 から 10 だ。作成ページに見える制約であり、後から作った SLA ではない。

プロトコルは、ページスクリプトが何を送るかを監視しない

HTTP がフラグメントを送らないことは、鍵がこのコンピュータを絶対に出ない、という意味ではない。ページスクリプトは window.location.hash を読み、自分で fetch できる。解析が完全な location.href をページ URL として送れれば、ハッシュの後ろは統計ログに入る。解析リクエストを Network で読め。「HTTP は送らない」で止めるな。

リンク全体は依然として資格情報

鍵を # の後ろへ置くのは、暗号文を預かる HTTP サービスから隠すためだけだ。チャット、メールクライアント、ブラウザ履歴、同期されたタブが保存するのは、利用者が見た URL 全体だ。ハッシュも含む。完全なリンクを持つ人は、閲覧ページを開いて復号できる。これは Bearer URL だ。リンクそのものが資格情報である。受信者だけが知る第二のパスワードは、手で足さない限り無い。

より慎重な受け渡しなら、二つに割れる。一方のメッセージは s.html?id=… だけ。もう一方の経路——電話、席での会話、別のメッセンジャー——は # の後ろだけ。半分だけでは復号できない。これは使い方であり、製品の既定ではない。既定は、相手が一度開けば済むよう、完全なリンクを一つ出す。

スクリーンショット、コピー、平文の転送も止められない。ワンタイムリンクが減らすのは、繰り返し開くことと、サーバー側に平文が長く残ることだ。相手がメモを撮るか、別チャネルへ貼れば、プロトコルは助けない。相手が一度読んで止めると信じられ、中身を破棄して再送できるときだけ使え。長く使うマスターパスワード、秘密鍵、ニーモニックはこの経路に載せない。

Network でその場確認

下の手順は、どのブランドの約束にも依存しない。捨ててよいテスト文を使え。本番のデータベースパスワードで練習するな。

  1. 開発者ツールのネットワークを開き、「ログを保持」を入れる。ワンタイムの作成ページを開き、テスト文 orange-lake-7 を書き、閲覧回数を 1 にしてリンクを作る。
  2. 作成時の POST を見る。本文に暗号文のフィールドがあるはずだ。orange-lake-7 は出てこないはずだ。生成された URL を控え、s.html?id=…#… の形か、ハッシュの後ろに切片があるか、疑問符の後ろが id だけかを確認する。
  3. 完全なリンクを新しいタブへ貼る。文書リクエストのリクエスト行は …/s.html?id=… であるべきだ。# と、その後ろの鍵は出てこないはずだ。
  4. 続く Fetch / XHR を見る。暗号文のリクエストは番号を運び、鍵は運ばないはずだ。解析も # の後ろやテスト文の原文を持っていかないはずだ。
  5. ページはテスト文を復号するはずだ。別タブではハッシュより前だけを貼る。リンクが不完全だと出るはずで、原文は出ない。
  6. すでに読んだ最初のリンクを再読み込みする。破棄済みか期限切れが見えるはずで、原文は見えない。再送するなら新しく作る。サーバー側の平文バックアップは無い。

MyPassGen のワンタイムはこの境界で動く。AES-256-GCM、平文 32 KB まで、鍵は # の後ろ、作成も閲覧もアカウント不要。信じるべきなのは、依然として Network と「ハッシュを外すと復号できない」だ。ページ上の「ゼロ知識」という三語ではない。制約の一覧はセキュリティにある。

# が守れないもの

社内メールや一部のメッセンジャーはリンクを展開する。バックエンドがページを一度 GET し、題名や抜粋を取る。HTML だけを取り、ページスクリプトを走らせないプレビューはフラグメントを見ず、暗号文 API も通常は呼ばない。ページスクリプトを実行するスキャナは、受信者より先に閲覧を終え、暗号文は破棄される。プロトコルの欠陥ではない。「閲覧ページを開くと、スクリプトが暗号文を取る」ことの帰結だ。相手が開けないと言ったら、プレビューが先に読んだかを先に聞け。再送するなら新しく作る。

ブラウザ履歴、クラッシュ報告、一部の同期アカウントは完全な URL を残す。ワンタイムリンクをアドレスバーに置きっぱなしにすれば、鍵をローカル履歴に残すことになる。読んだあと、# 付きのアドレスをチケットのひな型へ貼るな。「永久バックアップ」としてブックマークするな。失ったリンクは戻せない。サポートによるリセットは無く、フッターにも暗号文を復元する受信箱は無い。

長い説明を外へ出すとき、リンクと本文は別の工程だ。utm_source やクリック ID はパラメータ表で落とす。チケットの電話番号や番号類は送る前に伏せるべき項目で処理する。ワンタイムリンクが答えるのは「預かり先は鍵も平文も見ない」だ。「この議論に口座番号を載せてはいけない」には答えない。

よくある誤解

「鍵が URL にあるなら、サーバーは必ず見る。」切片次第だ。? の後ろなら、そのとおりだ。# の後ろなら、RFC 9110 は文書リクエストと Referer がそれを省略すると書く。Network のリクエスト行を読め。直感で争うな。

「# の後ろなら、チャット履歴は安全だ。」チャットが保存するのは、貼った文字列だ。サーバーログに鍵が無いことは、チャネルの全員、あるいはあとからその端末を開く人に鍵が無いことではない。

「閲覧後に消えるリンクはスクリーンショットを止める。」止めない。繰り返し開くことと、サーバー側の平文は減る。コピーや画面の写真は減らない。ローテーションできるパスワードだけを送れ。

「相手も登録しないと読めない。」このサイトでは、作成も閲覧もアカウント不要だ。閲覧ページは受信者に開かれている。「登録せずに開く」は「リンクを持つ人は先にログインする」ではない。

どこから始めるか

今日出そうとしていた、その一本のパスワードから始めよ。まず捨ててよいテスト文で、上の六ステップを歩け。作成リクエストに原文が無い。閲覧ページのリクエスト行に # の後ろの鍵が無い。ハッシュを外すと復号できない。二度目は破棄済みと出る。それから本番を送れ。

本番のパスワードは、この端末のパスワード生成で作る。ランダムは 6–128 文字、初期値 16。8 未満は弱いとみなせ。完全なリンクを送れ。より慎重なら、ハッシュとパスを二つの経路に割れ。同じパスワードを「バックアップ」としてチャットへ再貼りするな。この一本を終えれば、見出しには答えられる。復号鍵を # の後ろに置くのは、暗号文を預かる HTTP サービスに鍵を見せないためだ。チャット履歴もスクリーンショットも守れない。

よくある質問

鍵を # の後ろに置けば、サーバーは本当に見ないのか

RFC 9110 では、文書リクエストのターゲット URI にフラグメントは含まれない。MDN では Referer もそれを省略する。Network を開け。閲覧ページのリクエスト行に、ハッシュの後ろの鍵は出ないはずだ。ページスクリプトが自分で location.href を送れば、それは実装の漏れだ。Fetch 一覧を当たれ。

相手はリンクを開くのにアカウントが要るか

要らない。作成も閲覧もアカウント不要だ。受信者は閲覧ページを開いて復号する。設定した回数を読むか、TTL に達したあと、再度開くと破棄済みか期限切れと出る。原文は出ない。

ハッシュより前だけをコピーした。まだ開けるか

復号できない。# の後ろの鍵が無ければ、閲覧ページはリンクが不完全だと出すはずだ。送り主に完全な URL を求めよ。失ったリンク、すでに破棄されたリンクは戻せない。再送するなら新しく作る。

チャットが記録を残すのを止められるか

止められない。チャットは、貼った URL 全体を残すことが多い。鍵も含む。ハッシュが守るのは、暗号文を預かる HTTP サービスから鍵を外すことだけだ。長く残るチャネルに資格情報を置きたくなければ、そのチャネルへ完全なリンクを貼るな。