ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

如何安全分享Breakscale设计?AES-GCM加密与URL片段密钥分离原理解析

如何安全分享Breakscale设计?AES-GCM加密与URL片段密钥分离原理解析

【免费下载链接】breakscale

Build a system, load it until it breaks, and watch why. A system design simulator for learning distributed systems.

项目地址:https://gitcode.com/gh_mirrors/br/breakscale
点击查看免费下载

在 Breakscale 中,你画下的每个架构设计都可以一键变成链接发给别人——但设计内容是否会被服务器读到?答案是:不会。本文解析 Breakscale 分享链接的两套机制:AES-GCM 客户端加密与URL 片段(fragment)密钥分离,让你明白为什么哪怕链接存储库被拖库,对方也拿不到你的架构。

两种分享方式:先看全貌

Breakscale 提供两种把设计变成链接的方式,安全性各有侧重:

方式链接形态设计存放在哪适用场景
纯片段链接…#d3.……URL 的#之后小设计、离线可用、永久有效
加密存储链接…?s=<id>#k=<key>云端存储(密文)大设计、短链接

关键区别在于链接长度的代价:纯片段链接把整个设计塞进 URL,超过约 2000 字符后,Discord、Slack 或邮件客户端就可能截断它;而存储链接无论设计多大,URL 始终只有约 60 个字符。

核心原理:密钥分离为什么有效

先理解浏览器的一条基础规则:#之后的 fragment 永远不会被发送到服务器。它只存在于用户的浏览器里,只有用户主动粘贴时才"旅行"。

Breakscale 把加密链接拆成两半,分别走两条路线:

  • 查询参数?s=<id>:服务器看得到,但它只是一个不透明的 ID;
  • 片段#k=<key>:服务器永远看不到,但它是打开密文的钥匙。

于是存储端拿到的是"一把锁着的箱子 + 一个取件码",唯独没有钥匙。整个设计的核心可以一句话概括:

存储持有密文和命名它的 id,但在任何时刻都不持有能打开它的密钥。

加密逻辑位于 src/share/crypto.ts,密钥拼装成最终链接的实现在 src/share/store.ts。

AES-GCM 加密细节:为什么选它

打开 src/share/crypto.ts,可以看到几个耐人寻味的选型决策:

为什么是 AES-GCM 而不是 AES-CBC

GCM 模式一边加密一边做完整性认证。被篡改的密文——无论是在存储里、传输途中,还是被人手抖敲错一个字符——会直接解密失败,而不是解出一堆"看起来像那么回事的垃圾数据"再靠下游兜底。

为什么密钥是 128 位而不是 256 位

因为钥匙本身就挂在 URL 上,会被粘贴进聊天窗口。128 位密钥编码成 base64url 只有 22 个字符,256 位则要 43 个。128 位 AES 目前没有实际可行的破解路径,而这里真正防的威胁是"存储被攻破",不是"有人对着你这张图算十年"。

IV 的处理:12 字节,每次新生成

GCM 模式规定 IV 为 12 字节(IV_BYTES 定义)。它不是秘密,但绝不能与同一把密钥复用——而 Breakscale 为每个设计单独生成一把密钥,天然杜绝了复用问题。这也是刻意不用"一个浏览器一把钥匙"的原因:共享链接会把钥匙交给每个持有 URL 的人,一把钥匙覆盖多个设计,等于把别人的设计也一并送出。

打开与加密的完整链路

发送端(storeTopology):

  1. 把设计压缩打包成紧凑字节流;
  2. 生成一次性密钥,用 AES-GCM 加密,拼出IV + 密文的 blob;
  3. 只把 blob 上传到存储,拿到id;
  4. 把密钥编码进#k=,组装出最终链接——密钥从未出现在上传请求里。

接收端(fetchStored):

  1. 从 URL 片段取出密钥(浏览器本地就能读到);
  2. 凭?s=<id>拉取密文 blob;
  3. 在本地浏览器中解密——明文从未离开过持有链接的人的环境;
  4. 解出的字节走与纯片段链接完全相同的解码和校验路径,一个存储的设计不会比一个粘贴的设计多获得一分信任。

存储端为什么"构造上"读不到设计

链接存储是一个极简的 Cloudflare Worker:worker/src/index.ts 只做两件事——POST /v1存入密文并返回随机 ID,GET /v1/:id取回。

它"不能"做的事情,是设计出来的:

  • 读不了:每个 blob 都带着只存在于发送者浏览器里的密钥;
  • 搜不了:它不知道里面是什么,无法按内容检索;
  • 发不了:没拿到链接的人拿到 ID 也只是一堆无意义的字节。代码注释里说得很直白:" Losing the whole store would leak nothing. "(整个存储丢了也什么都泄露不了。)

正因为存储连"这是不是架构图"都分不清,防御只能做在形状上而非内容上:大小上限 256KB(MAX_BLOB_BYTES)、按 IP 的写入速率限制(429)、ID 由服务端随机生成(80 bit,穷举撞中概率极低)、写操作做 Origin 白名单校验。

解码侧的防御纵深

即使拿到了密文和密钥,恶意构造的 URL 依然过不了客户端的层层关卡,逻辑集中在 src/share.ts:

  • 版本前缀d3.:格式可被"检测"而不是"猜",旧版链接(d1./d2.)的读取器保留至今,离线永久可开;
  • base64url 字母表校验:链接被聊天软件截断或转义污染时,解码器统一给出可读的报错,而不是崩溃;
  • 解压炸弹防护:deflate 可以把几百字节膨胀成几百 MB,MAX_DECODED_BYTES 把输出上限钉在 2MB,超限就取消流而不是等它膨胀完;
  • 结构校验 + 注释消毒:解出的拓扑走与粘贴板完全相同的isTopology校验,注释还要过sanitizeAnnotations,防止用颜色值逃逸 style 属性注入样式。

一个细节值得留意:解码永远不会抛异常——截断、坏字母表、膨胀炸弹、恶意注释,全部收敛为同一句人类可读的提示,而不是白屏。

分享按钮的"刻意设计"

最后看一个容易被忽略的交互决策,它其实和安全模型直接相关:Share 对话框打开时不会生成链接,必须再点一次按钮才真正创建。原因有三:

  1. "打开对话框看看说明"不等于"同意上传设计"——写存储是设计中唯一离开浏览器的一刻;
  2. 存储的 blob不可撤销(无法"取消存储"),所以它应当跟随一次明确点击;
  3. 每次窥视都会消耗当日写入预算,为一条从没发出去的链接付费不值。

同样刻意为之的还有"不做"清单:没有过期时间选择器、没有"谁可以查看"、没有账号——因为存储根本读不懂设计,也认不出作者,任何这类配置都是空转的。

小结

机制防住的威胁
密钥放 URL fragment服务器/存储看到设计明文
AES-GCM 认证加密密文被篡改后静默解码出错误内容
每设计一把密钥一个链接泄露牵连其他设计
80 bit 随机 ID + 速率限制ID 被枚举遍历
2MB 解压上限解压炸弹拖垮浏览器
统一的校验/消毒器恶意拓扑与样式注入

下次在 Breakscale 点下"分享"时,你可以有把握地说:这条链接里的架构,只属于把它粘贴出来的人。想动手验证细节,推荐从 src/share/ 目录开始读起——加密(crypto.ts)、存储(store.ts)与链接格式(wire.ts)各占一个文件,注释写得像设计文档,非常值得一读。

【免费下载链接】breakscale

Build a system, load it until it breaks, and watch why. A system design simulator for learning distributed systems.

项目地址:https://gitcode.com/gh_mirrors/br/breakscale
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进