运维把数据库口令、API Key 或恢复码发给同事,最省事的做法是直接贴进聊天。消息可搜索、多端同步、半年后还能翻出来。换成「一次性链接」之后,很多人会把解密密钥写成 ?key= 跟在编号后面。这样托管密文的服务器、反向代理和访问日志都能看见密钥——加密等于半做。
本文不讲「怎么点按钮生成一条阅后即焚」,那是工具页的事。问题只有一个:一次性传口令时,密钥为什么要放在网址的 # 后面,服务器因此少看到什么,以及这一层保护到不了哪里。上一篇写过怎么用 Network 核对明文没有上传;这里把同一套核对方法用到「密钥住在哪一段 URL」上。MyPassGen 的阅后即焚按这个边界工作:浏览器里用 AES-256-GCM 算完,服务端只暂存密文,密钥只在 # 后面,创建与阅读都不用注册。
先分清两段网址
问号 ? 后面是 query,会进入 HTTP 请求行,也会进对端访问日志。井号 # 后面是 fragment,按协议留给客户端处理,默认不随请求发给服务器。密钥放错一段,后面所有「零知识」都站不住。
三种发法差在哪
同一条测试口令 orange-lake-7,至少能分成三类发送。差别不在页面口号,而在:明文有没有先变成密文,密钥有没有进入 HTTP,以及半年后还能不能在聊天记录里搜到原文。
| 做法 | 托管方或聊天软件拿到什么 | 半年后还能翻到什么 |
|---|---|---|
| 直接把口令贴进聊天 | 完整明文 | 可搜索的原文 |
加密链接,密钥写成 ?key= |
密文 + 密钥(都在请求里) | 访问日志里有密钥;聊天里有完整网址 |
加密链接,密钥放在 # 后面 |
服务器只看到密文编号;聊天软件仍可能保存整段网址 | 服务器日志没有密钥;聊天记录仍可能有完整链接 |
第三类只回答「托管密文的那台机器不该拿得到密钥」。它不回答「聊天软件会不会把整段网址存下来」。完整链接仍是凭证:谁复制了带 # 的地址,谁就能在浏览器里解密。把密钥放在井号后,解决的是服务端信任,不是链接被转发。
短文本才适合走这条路。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 /cn/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 服务看不到密钥。聊天软件、邮件客户端、浏览器历史和同步的标签页,保存的是用户看到的整段网址,包括井号后面。谁拿到完整链接,谁就能打开阅读页解密。这叫 bearer URL:链接本身就是凭证,没有第二道「只有接收方知道的口令」。
更敏感的交接可以拆开:一条消息只发 s.html?id=…,另一条通道——电话、当面、或另一套即时消息——只发 # 后面那一截。没有两半就解不开。这是用法,不是产品默认拆分。默认生成的仍是一条完整链接,方便对方一次打开。
它也不能阻止截图、复制或转发明文。阅后即焚减少的是「链接被反复打开」和「服务端长期保存明文」这两件事。对方看完立刻截图,或把明文再贴进另一个群,协议帮不上忙。只适合信任对方会看完即止、并且内容可以作废重发的一次性传递。长期主密码、私钥或助记词,本来就不该走这条路。
当场核对
下面这组步骤不依赖任何品牌承诺。用一句可丢弃的测试句做,不要拿真实数据库口令练手。
- 打开开发者工具 Network,勾选「保留日志」。再打开阅后即焚创建页,写入测试句
orange-lake-7,阅读次数设为 1,生成链接。 - 看创建时的 POST:请求体应有密文字段,不应出现
orange-lake-7。记下生成的完整链接,确认形态是s.html?id=…#…,井号后面有一截,问号后面只有id。 - 把完整链接粘到新标签打开。文档请求的请求行应是
…/s.html?id=…,不应出现#和后面的密钥。 - 再看随后的 Fetch / XHR:取密文的请求路径里应有编号,不应有密钥。分析上报同样不该带走
#后面那一截或测试句原文。 - 页面应解密出测试句。另开一个标签,只粘贴井号前面的部分,应提示链接不完整,而不是解出原文。
- 回到第一条已读过的链接再刷新:应看到已焚毁或已过期,而不是原文。需要再传时重新创建,没有服务端明文备份。
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 后失效;再次打开会看到已焚毁或已过期,而不是原文。
我只复制了井号前面的部分,还能打开吗?
不能解密。没有 # 后面的密钥,阅读页应提示链接不完整。请向发送方要完整链接。链接丢失或已焚毁后无法恢复,需要再传时重新创建。
这能防止聊天软件留下记录吗?
不能。聊天软件通常保存你粘贴的整段网址,包括密钥。井号只让托管密文的 HTTP 服务少看到密钥。不想让群记录留下凭证,就不要把完整链接发进会长期留存的群。