運用担当がワンタイムリンクを Slack のチャンネルに貼る。先にカードが出て、タイトルはサイト名だ。十分後に受け手が開くと、ページは「閲覧され、永久に破棄されました」と出る。どちらも平文は読んでいないつもりだ。ホストへ最初に届いた GET は、同僚の指ではないことが多い。カードを描くためにチャット側が先に取りに行った回だ。

前の記事では同僚にパスワードを一度だけ渡すとき、なぜ復号鍵を URL の # の後ろに置くのかを書いた。あれは「暗号文を預かる HTTP ホストが鍵を見るか」の話だ。本稿の問いは別にある。人が押す前に、プレビューが閲覧回数を一つ使うか。鍵はいまも # の後ろにある。作成も閲覧も登録不要。MyPassGen のワンタイムは開いたその場で使える。閲覧回数の初期値は 1、上限は 10。作成画面のボタン案内ではない。Network、アドレスバー、チャット窓で見えるものだけを照合する。

先に二つを分ける

プレビューが暗号文を破棄するかと、チャット履歴に完全なURLが残るかは、同じ問いではない。HTMLだけ取り、ページのスクリプトを動かさないプレビューは、通常 # の後ろの鍵を読めず、暗号文を取るエンドポイントにも届かない。チャンネルには s.html?id=…#… の文字列が残る。コピーした人はまだ開ける。プレビューが消費しなかったことは、リンクが資格情報でなくなったことではない。

破棄されたと言われた。自分は開いていない

一度限りのよくある設計は、最初の取得で暗号文を渡し、サーバがそれを消すことだ。閲覧回数が 1 なら、その一回が最後のコピーになる。送信側の「作成できた」と受信側の破棄状態のあいだに、人のクリックではない訪問が挟まる。チャンネルのアンフォーリング、メールの安全スキャン、会社のゲートウェイがURLを書き換えてから探りに来る、といった経路だ。

Slack はこれを製品ドキュメントに書いている。Unfurling links in messages には、メッセージにリンクを貼ると Slack がそのページを取得してプレビューを付ける、とある。公式の robots ページは、この取得を行うボットを Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots) と名付け、できるだけ少ない量を取る(HTTP Range を使う)と書く。目的は oEmbed、Twitter Card、Open Graph といったメタタグの抽出だ。タグが画像や音声、動画を指していれば、検証のためにそのファイルも取る。Slack Robots はさらに、同じURLの応答をサービス内でおよそ 30 分キャッシュすること、そして robots.txt では自分を止めないことを書く。利用者の代理で要約を取るのであり、検索エンジンのようなサイト全体の巡回ではない、という立場だ。

だから「チャンネルにカードが出た」が証明できるのは、受け手ではない機械が、貼ったURLへ少なくとも一度 HTTP リクエストを出した、ということだけだ。平文が読まれた証明にはならない。暗号文が閲覧として数えられた証明にもならない。破棄されたかを判断するには、そのリクエストがどの層に当たったかを見る。

プレビューが送るのはURLのどこまでか

コピーする完全なリンクの形は …/s.html?id={id}#{key} だ。疑問符のあとの id は HTTP のリクエスト行に乗る。ハッシュのあとの鍵は、プロトコル上クライアント側に残る。根拠は RFC 3986 §3.5 と RFC 9110 §7.1 だ。対象URIはフラグメントを含まない。この部分はクライアントが処理する。プレビュー用ボットが出すのは普通の HTTP GET であり、合法なリクエスト行は GET /ja/s.html?id=… までだ。ハッシュのあとの鍵は、暗号文を預かる機械には届かない。

HTMLだけ取るプレビューが、通常は平文を解けない理由もここにある。閲覧ページが復号するには、ブラウザのスクリプトが location.hash を読み、サーバに暗号文を求め、この端末で AES-256-GCM で解く必要がある。プレビュー用クローラにはフラグメントの鍵がなく、番号だけがある。s.html の HTML 全文を保存しても、そのマークアップにパスワードは入っていない。閲覧ページは noindex の着地だ。初期 HTML にあるのは汎用のタイトル「ワンタイムリンク · MyPassGen」だ。平文はスクリプトが終わったあとでページに書き込まれる。

チャット側が保存するのは、別の物体だ。利用者が貼ったのはハッシュと鍵を含む文字列全体だ。チャンネル検索、メッセージ同期、別の端末で同じスレッドを開くと、完全な資格情報が返る。# の後ろに鍵を置くのは、プレビューの GET と典型的なサーバログから鍵を外すためだ。グループ履歴から完全なURLを消すわけではない。より慎重な受け渡しでは、番号と鍵を二つの経路に分ける。後述する。

取得は三種類、回数に入るのは一つ

RFC 9110 は GET を安全なメソッドと定義する。送っても破壊的な副作用を持たない、という語義だ。カードを描くためだけの GET は、その語義では暗号文を消してはいけない。現実の一度限りツールが「文書への最初の GET」を閲覧とみなすなら、プレビュー用ボットが受け手より先に内容を破棄する。差はチャット製品の名前ではない。回数をどのリクエストに結びつけたかだ。

実行できるもので取得を分ける。第一は、最初の HTML を取り、<title> と Open Graph を解析し、ページのスクリプトを動かさない。Slack 公式の Link Expanding の説明はここに入る。メタタグを抜き、必要ならタグが指名したメディアを取る。Apple の Ensuring Beautiful Rich Links は、リッチリンク生成では JavaScript を動かさないので、Open Graph はページソースに書いておけ、と述べる。Discord の埋め込みもよく同じ実装だ。Discordbot/2.0 が最初の HTML を読むのであり、完全なブラウザエンジンを開くのではない。

第二は、ページのスクリプトは動かすが、リクエストにフラグメントが無い。ヘッドレスブラウザや、一部の企業向けスキャン用サンドボックスがここだ。閲覧ページの JavaScript は実行できるが、location.hash は読めない。本サイトでは、鍵が無ければ暗号文は取らない。ページは「リンクが不完全です」で止まる。閲覧回数は動かないはずだ。

第三は、スクリプトを動かし、ハッシュ込みの完全なURLを本物のページ環境に載せる。クリック直前のリンクを端末の WebView で開く一部のエンドポイント保護や、送信側の端末でページをフルロードするプレビューは、「暗号文を取り、回数を増やす」まで到達しうる。プロトコルではこの類を排除できない。同じ経路でテスト用リンクを再現するしかない。

Slack、LINE、メールゲートウェイの差

Slack がいちばん照合しやすい。公式 robots が User-Agent を公開している。同じ文字列で閲覧ページを取り、応答が静的 HTML か、あとで閲覧回数が動いたかを見られる。およそ 30 分の全体キャッシュも書いてある。短い時間に同じURLを複数チャンネルへ貼っても、毎回オリジンへ当たり直すとは限らない。カードのサイト名は、メタタグか <title> が読まれたことだけを示す。日本語の閲覧ページの既定タイトルは「ワンタイムリンク · MyPassGen」だ。テスト用の文は入っていない。

国内の日常チャットでは LINE の方が先に出てくる。LINE は Open Graph を読んでリンクのカードを出す。Slack のような公開 robots ページは無く、内部クローラの能力を契約書のように読めない。その場で見えるのは、URLを貼ったあとタイトルカードが出るか、受け手が開いたあと閲覧ページが復号か不完全か破棄か、だ。日本のメッセンジャでカードを出すよくある実装は、サーバが HTML のタイトルと要約を取るのであり、受け手のスマホで閲覧ページのスクリプトを先に走らせることではない。公開仕様が無いときは、自分のテスト用リンクの結果を信じる。「カードが出た」を「暗号文が取られた」と同一視しない。

中国側のチームと渡すときは WeChat(微信)も同じ確認になる。こちらも Slack 型の公式 robots は無い。カードが出ることと、完全なリンクをもう一度開いたときの状態を、別々に見る。

社用メールは第三条だ。Microsoft の Safe Links の概要は、受信メールをスキャンし、URLを書き換えることがある、クリック時に再検証する、と書く。書き換え後のアドレスは safelinks.protection.outlook.com のような接頭辞を持つ。配送時のスキャンとクリック時のリダイレクトの両方で、対象へもう一度 GET が飛びうる。見る場所は二つだ。本文がすでに包まれているか。クリックしたあとアドレスバーに # の後ろが残っているか。書き換えやリダイレクトで鍵が落ちれば、閲覧ページは「リンクが不完全です」と出るはずで、平文は出ない。ハッシュ込みの完全なURLが、スクリプトを動かすサンドボックスに載れば、回数は増えうる。「どのゲートウェイも HTML だけ見る」と仮定しない。Microsoft Teams のチャンネル内リンクも Safe Links を通ることがある。方針はテナント次第だ。

閲覧ページが実際に出すリクエスト

MyPassGen の閲覧ページが開くと、スクリプトは順に二つ行う。先に番号で状態を聞く。このリクエストは、まだあるか、すでに破棄されたか、期限切れかを答え、閲覧回数は増やさない。# の後ろに鍵が無ければここで止まり、ページはリンクが不完全だと出す。鍵があれば、続けて暗号文の GET を出す。回数を増やすのはこのリクエストだ。read_count が設定した上限(初期値 1、最大 10)に達すると、サーバは暗号文を消す。次に開くと破棄状態になる。期限は作成時に選んだ TTL に従う。1 時間、24 時間、7 日、または回数だけ見て TTL 無しだ。

したがって、s.html?id=… だけを GET するプレビューと、状態エンドポイントだけを叩くスキャナは、その一回を使わない。回数を使うのは、「鍵をすでに持った閲覧ページのスクリプト」が出す暗号文リクエストだ。作成時、ブラウザはこの端末で AES-256-GCM で暗号化し、IV は 12 バイト、平文の上限は 32 KB だ。サーバが預かるのは暗号文だけ。鍵は HTTP のリクエスト行に入らない。アルゴリズムと、平文が上がっていないことの確かめ方は、ブラウザで暗号化したとき、平文が送られていないことをその場で確かめるにある。

閲覧ページに「表示する」ボタンは無い。鍵が揃っているとき、タブを開いた時点で暗号文を取る。これは「プレビュー用クローラは HTML だけ取る」と矛盾しない。そのクローラは通常、取得の段階まで届かない。矛盾が出るのは第三の取得だ。完全なURLが、スクリプトを動かす環境に載ったときだ。そのときは人がタブを開いたのと同じで、初期値 1 のリンクは消費される。カードの見た目では類は判らない。次の節のように状態を見る。

プレビューが消費しなくても、スレッドには資格情報が残る

Slack のチャンネル、LINE のトーク、WeChat のチャット、メールスレッドに残る完全なリンクは、まだ資格情報だ。平文まであと一段あるだけだ。そのメッセージを検索できる人は、破棄される前に開ける。シークレットウィンドウを閉じても、ブックマークやダウンロードに残った完全なURLは消えない。見方はシークレットウィンドウを閉じたあと、ダウンロード、ブックマーク、クリップボードにパスワードが残る。

プレビューのあと、何が残るか

同じテスト用リンクで閲覧回数を 1 にしたとき、プレビューのあと送信側と受信側が見るものは同じではない。表は開けるページで書く。製品のスローガンでは書かない。

何が起きたか チャット窓でよく見えるもの 完全なリンクをもう一度開くと
HTMLのメタだけ。スクリプト無し タイトルカード。テスト用の文は無い まだテスト用の文を復号できる
スクリプトは動いたが、リクエストに # が無い カードか空白プレビュー。サーバは状態照会だけ まだ復号できる。鍵無しの回は不完全と出る
完全なURLが、スクリプトを動かす環境に載った カードかスキャン報告。回数は使われた 破棄または期限切れ。平文は無い
メールゲートウェイがURLを書き換え、リダイレクトでフラグメントを落とした safelinks… のような包まれたアドレス リンクが不完全。暗号文は残っていることが多い

四行目と三行目を混ぜない。鍵が落ちたなら、受け手は開けないが、送信側は元の完全なURLでまだ開ける。回数は使われていない。受け手が持っているのが途中までのアドレスなだけだ。鍵は残っていてページがすでに破棄なら、プレビューかスキャンが先に読んだ。前者はハッシュの後ろだけを送り直せば足りる。後者は新しいリンクが要る。サーバ側に平文の控えは無く、破棄済みを取り戻せる問い合わせ窓口も無い。

その場で確かめる

次の手順は、どのブランドの約束にも依らない。実在のアカウントに入れないテスト用の文を使う。例は orange-lake-7 だ。使っているマスターパスワード、本番の鍵、本物のワンタイムでは試さない。

  1. ワンタイムを開き、テスト用の文を入れ、期限は 24 時間、閲覧回数は 1 のままリンクを作る。完全なアドレスを控え、形が s.html?id=…#… であることを確認する。登録は不要。
  2. ハッシュより前だけをコピーする。端末から Slack の User-Agent でそのURLを取る。例は curl -A "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)" "https://mypassgen.com/ja/s.html?id=…"。応答は閲覧ページの HTML だ。タイトルや本文にテスト用の文は出てこないはずだ。これは「文書の取得では平文が見えない」ことだけを示す。本物のチャットの代わりにはならない。
  3. 同じ「id だけ、鍵無し」のアドレスをブラウザで開く。「リンクが不完全です」と出るはずで、秘密は出ない。開発者ツールの Network を開き、ログを保持する。状態照会は見える。そのあとの暗号文取得の成功応答は見えないはずだ。
  4. すぐ、完全なリンク(# 付き)を別タブで開く。テスト用の文が復号されるはずだ。前の段階ですでに破棄されていれば、実装か中間装置が「鍵無しの訪問」も閲覧とみなしている。ここで止める。本番の秘密は送らない。
  5. 閲覧回数 1 のテスト用リンクをもう一本作る。完全なURLを、自分で管理できる Slack チャンネル、LINE の自分宛て、または私用のテストグループへ貼る。プレビューカードを待つ。出なければ、出ないことを確認する。閲覧ページは開かない。
  6. プレビューが出たあと、同じ完全なリンクをデスクトップのブラウザで開く。まだ復号できるなら、その経路のプレビューは上の二類だ。すでに破棄なら、途中に第三の取得があるか、自分の端末が鍵付きページを先読みした。まだ送る必要があれば新しいリンクを作る。読んだあとはパスワードをコピーしたあと、クリップボードはまだ誰が読めるかのとおりクリップボードを上書きする。# が残るアドレスはブックマークしない。

社用メールなら半歩足す。テスト用リンクを自分の受信箱へ送り、本文が Safe Links 風に包まれているか、クリック後のアドレスバーに鍵が残るかを見る。鍵が消えたら、番号はメール、鍵は電話に分ける。破棄されたら回数を上げるか、経路を変える。MyPassGen は各社のゲートウェイを代行して分類しない。いま開いた窓を信じる。

カードが出るチャンネルへ出すとき

初期値 1 回は、「相手が完全なリンクを持ち、すぐ開き、それで終わり」に合う。チャンネル、大人数のグループ、カードを出すメーリングリストでは、先にテスト用リンクで前節を通す。プレビューが回数に入らないことを確かめてから、本物の秘密を出す。テストがすでに破棄されていたなら、回数を上げてもプレビューが「安全になる」とは期待しない。上限を 2 にしても、人がもう一度開ける余裕が増えるだけだ。毎回スクリプトを動かすスキャナは、回数を使い切れる。

より堅い分け方は、二つの経路だ。一方のメッセージには s.html?id=… だけを載せる。プレビューが取るのは番号と汎用タイトルまでだ。もう一方は電話、対面、別のメッセンジャアカウントで、# の後ろだけを渡す。片方だけでは復号できない。これは使い方であり、製品の既定分割ではない。作成画面はいまも一本の完全なリンクを出す。一対一の受け渡しにはそれが速い。

パスワード自体はこの端末で作る。グループに書いてから消さない。MyPassGen のパスワード生成はランダムが 6–128 文字、初期値 16、8 未満は弱いと出す。登録不要。生成結果は業務データとして上がらない。32 KB を超える証明書束や書き出しは、テキストのワンタイムに載せない。ファイル暗号を使う。ブラウザ内のストリーミング AES-256-GCM、1 ファイルは 5 GB まで、出力は .lock / .enc、パスフレーズは別経路。クラウドが持つのは暗号文だけだ。見方はファイルをクラウドに入れる前、平文は誰が読めるか、パスフレーズはどの経路で送るか。

「プレビューのあと完全なリンクはまだ復号できるか」と「スレッドに完全な資格情報が残るか」の二回を終えれば、本稿の問いに答えられる。HTMLだけ取るプレビューは、通常このサイトのワンタイムを先に消費しない。スクリプトを動かし # を運ぶ取得は消費する。カード自体は類を教えない。状態ページと二度目のオープンが教える。

よくある質問

Slack のチャンネルにプレビューカードが出た。リンクはもう死んでいるか

必ずしもそうではない。Slack の説明では、Link Expanding はメタタグを抜き、Range でできるだけ少ない量を取る。カードが証明するのは、閲覧ページの HTML が取られたことだけだ。完全なリンクをもう一度開く。まだ復号できるなら回数は残っている。破棄されていれば新しく作る。同じURLを何度も更新して戻るのを待たない。

LINE や WeChat に公式の robots が無い。どの文を信じるか

このテスト用リンクの結果を信じる。タイトルカードは、どこかのプログラムがタイトルか要約を読んだことだけを示す。受け手が押す前に、自分で完全なリンクを開く。テスト用の文が残っていれば、その経路のプレビューは暗号文を取っていない。破棄されていれば、URLを分けるか、一対一で送る。

閲覧回数を 2 や 3 にすれば、プレビューに勝てるか

取得の枠が増えるだけだ。プレビューが安全なメソッドになるわけではない。鍵付きで毎回スクリプトを動かすスキャナは、回数を使い切れる。先に回数 1 のテスト用リンクで、プレビューが上の二類かを見る。不安定な経路を使わざるを得ないとき、回数を上げ、もう一人が開く可能性を受け入れる。

作成と閲覧に登録は要るか。プレビュー用ボットは閲覧者か

登録は不要。作成も閲覧も訪問者に公開されている。HTMLだけ取るプレビュー用ボットは、閲覧ではない。暗号文の GET を出せば、サーバは人と区別できず、回数は減る。閲覧ページは受け手に公開だ。ログインの門は無く、破棄済みを取り戻せる問い合わせ窓口も無い。