系統維護把資料庫密碼、API Key 或復原碼傳給同事,最省事的做法是直接貼進 LINE 或 Slack。訊息可搜尋、多裝置同步,半年後還能翻出來。換成「一次性連結」之後,很多人會把解密金鑰寫成 ?key= 跟在編號後面。這樣代管密文的伺服器、反向代理和存取紀錄都能看見金鑰——加密等於只做了一半。

本文不講「怎麼按按鈕產生一條閱後即焚」,那是工具頁的事。問題只有一個:一次性傳密碼時,金鑰為什麼要放在網址的 # 後面,伺服器因此少看到什麼,以及這一層保護到不了哪裡。上一篇寫過怎麼用 Network 核對明文沒有上傳;這裡把同一套核對方法用到「金鑰住在哪一段 URL」上。MyPassGen 的閱後即焚依這個界線運作:瀏覽器裡用 AES-256-GCM 算完,伺服器只暫存密文,金鑰只在 # 後面,建立與閱讀都不用註冊。

先分清兩段網址

問號 ? 後面是 query,會進入 HTTP 請求列,也會進對端存取紀錄。井字號 # 後面是 fragment,依協定留給用戶端處理,預設不隨請求發給伺服器。金鑰放錯一段,後面所有「零知識」都站不住。

三種傳法差在哪

同一條測試密碼 orange-lake-7,至少能分成三類傳送。差別不在頁面口號,而在:明文有沒有先變成密文,金鑰有沒有進入 HTTP,以及半年後還能不能在聊天紀錄裡搜到原文。

做法 代管方或通訊軟體拿到什麼 半年後還能翻到什麼
直接把密碼貼進聊天 完整明文 可搜尋的原文
加密連結,金鑰寫成 ?key= 密文 + 金鑰(都在請求裡) 存取紀錄裡有金鑰;聊天裡有完整網址
加密連結,金鑰放在 # 後面 伺服器只看到密文編號;通訊軟體仍可能保存整段網址 伺服器紀錄沒有金鑰;聊天紀錄仍可能有完整連結

第三類只回答「代管密文的那台機器不該拿得到金鑰」。它不回答「LINE 會不會把整段網址存下來」。完整連結仍是憑證:誰複製了帶 # 的地址,誰就能在瀏覽器裡解密。把金鑰放在井字號後,解決的是伺服器端信任,不是連結被轉傳。

短文本才適合走這條路。MyPassGen 單條明文上限 32 KB,適合密碼、金鑰片段和短說明。憑證包、匯出表或超過這個體積的內容,應走檔案加密盒:本機產生 .lock / .enc 再另傳密碼,而不是硬塞進一條焚鏈。檔案情境見上傳雲端硬碟前誰拿得到明文。

HTTP 到底送出什麼

RFC 3986 §3.5 把 # 後面稱為 fragment identifier:它標示的是取回主資源之後,在用戶端解釋的次級位置。瀏覽器先依路徑和查詢去取頁面,等資源到達,再用 fragment 決定捲動位置或交給頁面腳本。HTTP 請求本身不需要這一段。

現行 HTTP 語意寫得更硬。RFC 9110 §7.1 寫明:目標 URI 排除引用裡的 fragment 元件,因為 fragment 保留給用戶端處理。請求列裡合法的 origin-form 是路徑加可選查詢,沒有 # 這一段。你在網址列看到 s.html?id=abc#金鑰,送出去的文件請求應是 GET /tw/s.html?id=abc,後面那截留在本機。

Referer 也會剝掉 fragment。MDN Referer 寫明:該標頭可以帶來源、路徑和查詢,不可以帶 URL fragment 和使用者名稱密碼。W3C Referrer Policy 在送出前的剝離步驟裡同樣把 fragment 置空。因此「金鑰放在 # 後面,就不會隨 HTTP 發給代管密文的機器」是協定行為,不是某家網站的口頭保證。

問號和井字號不能互換

依 WHATWG URL,開啟 http / https 時,? 後面會進入請求列。寫成 s.html?id=abc&key=金鑰,金鑰會出現在存取紀錄、反向代理和部分 CDN 紀錄裡。外發行銷連結時哪些查詢參數能刪,見哪些參數能刪;那是另一類問題。金鑰絕不能改放進 query。

還有一個編碼陷阱:%23 是 # 的百分號編碼。寫在路徑或查詢裡的 %23 會發給伺服器,解碼後變成字面量井字號,並不是 fragment。金鑰必須是網址列裡未編碼的 # 分隔,而不是被折進路徑的 %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 不送出 fragment,並不等於金鑰永遠不離開這台電腦。頁面腳本可以讀 window.location.hash,再自己 fetch 出去。分析若把完整 location.href 當頁面地址回報,井字號後面會進統計紀錄。你要在 Network 裡看分析請求,而不是只相信「反正 HTTP 不會帶」。

完整連結仍是憑證

把金鑰放在 # 後面,只讓代管密文的 HTTP 服務看不到金鑰。LINE、信件客戶端、瀏覽器歷程和同步的分頁,保存的是使用者看到的整段網址,包括井字號後面。誰拿到完整連結,誰就能開啟閱讀頁解密。這叫 bearer URL:連結本身就是憑證,沒有第二道「只有接收方知道的密碼」。

更敏感的交接可以拆開:一則訊息只發 s.html?id=…,另一條通道——電話、當面、或另一套即時通訊——只發 # 後面那一截。沒有兩半就解不開。這是用法,不是產品預設拆分。預設產生的仍是一條完整連結,方便對方一次開啟。

它也不能阻止截圖、複製或轉發明文。閱後即焚減少的是「連結被反覆開啟」和「伺服器長期儲存明文」這兩件事。對方看完立刻截圖,或把明文再貼進另一個群組,協定幫不上忙。只適合信任對方會看完即止、並且內容可以作廢重發的一次性傳遞。長期主密碼、私鑰或助記詞,本來就不該走這條路。

當場核對

下面這組步驟不依賴任何品牌承諾。用一句可丟棄的測試句做,不要拿真實資料庫密碼練手。

  1. 開啟開發者工具 Network,勾選「保留記錄」。再開啟閱後即焚建立頁,寫入測試句 orange-lake-7,閱讀次數設為 1,產生連結。
  2. 看建立時的 POST:請求主體應有密文欄位,不應出現 orange-lake-7。記下產生的完整連結,確認形態是 s.html?id=…#…,井字號後面有一截,問號後面只有 id。
  3. 把完整連結貼到新分頁開啟。文件請求的請求列應是 …/s.html?id=…,不應出現 # 和後面的金鑰。
  4. 再看隨後的 Fetch / XHR:取密文的請求路徑裡應有編號,不應有金鑰。分析回傳同樣不該帶走 # 後面那一截或測試句原文。
  5. 頁面應解密出測試句。另開一個分頁,只貼上井字號前面的部分,應提示連結不完整,而不是解出原文。
  6. 回到第一條已讀過的連結再重新整理:應看到已焚毀或已過期,而不是原文。需要再傳時重新建立,沒有伺服器明文備份。

MyPassGen 的閱後即焚依這個界線運作:AES-256-GCM,明文上限 32 KB,金鑰在 # 後面,建立與閱讀都不用註冊。你要相信的仍是 Network 和「去掉井字號就解不開」,不是頁面上的「零知識」三個字。完整約束見安全說明。

# 保護不了什麼

企業信箱和部分即時通訊會做連結預覽:後台先 GET 一次頁面,抓標題或摘要。只取 HTML、不執行頁面腳本的預覽,拿不到 fragment,一般也調不到取密文的介面。會執行頁面腳本的掃描器則可能先於接收方完成一次閱讀,密文隨之焚毀。這不是協定漏洞,是「開啟閱讀頁就會用腳本取密文」的後果。對方說打不開時,先問是不是預覽搶先讀了;需要再傳就重新建立。

瀏覽器歷程、當機報告和部分同步帳號會保存完整 URL。把焚鏈留在網址列,等於把金鑰留在本機歷程裡。讀完不要把帶 # 的地址寫進工單範本,也不要當作「永久備份」收藏。連結遺失無法恢復:沒有客服可以重設,頁尾也不提供信箱來找回密文。

外發說明文件時,連結和正文是另一道工序。帶 utm_source 的網址依參數清單清洗;工單裡的手機號碼、身分證字號依哪些欄位必須打碼處理。焚鏈解決的是「代管方看不到金鑰和明文」,不是「討論時少帶完整號碼」。

常見誤區

「金鑰在網址裡,伺服器一定看得到。」取決於在哪一段。在 ? 後面,對;在 # 後面,依 RFC 9110,文件請求和 Referer 都不應帶上。用 Network 看請求列,不要用直覺。

「放在 # 後面,聊天紀錄就安全了。」通訊軟體保存的是使用者複製的整段字串。伺服器紀錄少了金鑰,並不等於群組成員或以後拿到手機的人少了金鑰。

「閱後即焚能防止對方截圖。」不能。它減少反覆開啟和伺服器存明文,不減少複製和拍照。只能傳可以作廢的一次性密碼。

「對方也要註冊才能看。」本站建立與閱讀都不用帳號。閱讀頁對接收方公開。不要把「開啟即用」理解成「誰有連結誰要先登入」。

從哪一步開始

先處理今天就要送出去的那一條密碼。用可丟棄的測試句走完上一節的六步:建立請求沒有原文,閱讀頁請求列沒有 # 後面的金鑰,去掉井字號解不開,讀過再開是焚毀態。然後再發真實內容。

真實密碼用密碼產生器在本機產生,隨機模式長度 6–128,預設 16;低於 8 位應視為偏弱。把完整連結發給對方;更敏感時把井字號前後拆到兩條通道。不要把同一條密碼再貼進聊天當「備份」。做完這一條,你就已經能回答本文的問題:金鑰放在 # 後面,是為了讓代管密文的 HTTP 服務看不到金鑰;它保護不了聊天紀錄和截圖。

常見問題

把金鑰放在 # 後面,伺服器真的看不到嗎?

依 RFC 9110,文件請求的目標 URI 不含 fragment;Referer 依 MDN 也不帶這一段。開啟 Network,閱讀頁的請求列裡不應出現井字號後的金鑰。頁面腳本若主動回報 location.href,那是實作問題,也要在 Fetch 清單裡查。

對方開啟連結也要註冊嗎?

不需要。建立與閱讀都不用帳號。接收方開啟閱讀頁即可解密。內容依設定的次數閱讀後焚毀,或到達 TTL 後失效;再次開啟會看到已焚毀或已過期,而不是原文。

我只複製了井字號前面的部分,還能開啟嗎?

不能解密。沒有 # 後面的金鑰,閱讀頁應提示連結不完整。請向發送方要完整連結。連結遺失或已焚毀後無法恢復,需要再傳時重新建立。

這能防止通訊軟體留下紀錄嗎?

不能。LINE 或信件通常保存你貼上的整段網址,包括金鑰。井字號只讓代管密文的 HTTP 服務少看到金鑰。不想讓群組紀錄留下憑證,就不要把完整連結發進會長期留存的群組。