2of3 入门实战:用 Shamir 2-of-3 门限把 Ente 恢复密钥拆成三张卡
【免费下载链接】ente💚 End-to-end encrypted cloud for everything.项目地址: https://gitcode.com/GitHub_Trending/en/ente
导读:本文以 Ente 开源仓库中 2of3 入门文档 为主体,完整讲解 2of3 的完整生命周期——把一份秘密拆成三张卡、分点存储、在收起来之前测试恢复,以及日后随时用任意两张卡恢复秘密。2of3 是 Ente 提供的免费端到端私密网页工具(2of3.ente.com),无需账号、无需安装,秘密始终只停留在你的浏览器里。读完本文,你将掌握 2of3 的全部操作步骤、卡片的存储策略、错误信息的含义,以及其背后 Shamir 秘密共享的数学原理与开源实现。
什么是 2of3
2of3 把一份重要秘密变成三张恢复卡:任意两张卡都能还原出秘密,单张卡无法还原。它是一个网页应用,不需要账号、不需要安装,你的秘密永远不会离开浏览器。适用于 Ente 恢复密钥、密码管理器主密码、钱包助记词、全盘加密密钥、应急(break-glass)生产凭据等"丢不得也泄不得"的秘密。
这一设计思路与需要两把钥匙的保险箱、能容忍任意一个站点宕机的备份磁带轮换是同源的:三份拷贝放在三个地方,丢失任一份不再是灾难;"任意两张"的门限意味着攻击者即使拿到一张卡,也拿不到任何可用的东西。相关背景与使用场景详见 docs/docs/2of3/index.md。
注意:如果你的秘密就是 Ente 账户恢复密钥本身,可以优先考虑 Legacy Kit。Legacy Kit 与 2of3 使用相同的 2-of-3 Shamir 数学,但额外提供了可配置的等待期、卡片丢失时的吊销能力,以及绑定 Ente 账号的服务器中介恢复流程。2of3 则适用于非 Ente 秘密,或希望恢复过程完全不依赖任何服务器、只依赖卡片本身的情况。
将一个秘密拆成三张卡
- 打开 2of3.ente.com。
- 在左侧的Secret字段粘贴或输入你想要拆分的秘密。可以是恢复密钥、主密码、钱包助记词,或任何一小段文本。字段下方的计数器会实时显示当前已用字节数与上限。
- 可选地编辑Card label(卡片标签)。默认显示今天的日期(例如
26 May 2026)。标签会打印在每张卡上,方便日后区分不同的卡片组。 - 秘密合法后,右侧会立即出现三张卡。每张卡包含一个二维码、卡片编号(1、2、3)以及一个类似
A1B2C3D4的ID。同一份秘密生成的三张卡共享同一个 ID。
从源码看,"秘密合法后立即生成三张卡"是由splitSecret在浏览器内同步完成的,它调用crypto.getRandomValues生成随机系数、随机 ID,并输出三份编码后的 share,见 web/apps/twoof3/src/features/twoof3/utils/shamir.ts。
[!IMPORTANT]
把三张卡当作一个整体套装对待,绝不要把不同套装的卡片混用。恢复只有在合并的卡片拥有相同 ID、且由同一份秘密生成时才有效。如果秘密改变了,请生成一整套新卡并一起替换掉旧三张。
保存卡片
每张卡都有独立的操作控件,从卡片面板可以:
- Print(打印):为单张卡打开打印对话框。
- Download(下载):把该卡保存为 PNG 图片(高分辨率,适合日后打印)。
- Share(分享):在支持的平台上打开系统分享面板(移动端及部分桌面浏览器)。若不支持分享,则改为下载该卡。
- Copy code(复制代码):仅复制该卡的文本代码(以
2of3-开头),方便粘贴到笔记或密码管理器中。
在源码中,卡片渲染为 1400×1840 的高分辨率 Canvas,二维码区域 860 像素,文本代码按每行最多 80 字符、最多 4 行排版(PRINTED_SHARE_CHARS_PER_LINE = 80、PRINTED_SHARE_LINE_COUNT = 4),见 web/apps/twoof3/src/features/twoof3/utils/export.ts 与 renderShareCard。卡面还印有应用名2of3.ente.com、卡片编号、标题与ID指纹,即使多年后被人捡到也能看懂。
对于"想要三张卡外加离线恢复文件"的典型情况,点击秘密字段下方的Download all cards,会下载四个文件:
- 三张 PNG 图片,每卡一张,命名形如
today-set-a1b2c3d4-card-1.png。 - 一个 HTML 文件
2of3-recovery.html,它是 2of3 恢复流程的完全离线副本。详见 How it works。
[!NOTE]
2of3 的最大秘密长度约为 200 字节文本。精确上限是"仍然能印得进一张卡"的长度;Secret字段下方的字节计数器会显示当前限制。如果秘密太长放不下,参见 FAQ。
最大长度限制背后的实现
格式本身在原理上支持最大 2048 字节(MAX_SECRET_BYTES = 2048,见 shamir.ts),但实际可用上限取决于"可读二维码 + 四行印刷文本"能否塞进一张卡。源码用maxSecretBytesForEncodedShareLength(MAX_PRINTED_SHARE_LENGTH)计算:卡片文本区最多容纳 80×4 = 320 个字符的编码 share,反推得到可容纳的秘密字节数(约 200 字节),见 export.ts 与 shamir.ts 中的编码长度换算。字节计数器显示的正是这个实时计算的上限。
把三张卡存放在不同的地方
2of3 的全部意义就在于分离。三份拷贝放在同一个抽屉里,那只是"一个抽屉"。
一个合理的分布方案大致是:
- 一张卡与重要文件放在一起(护照、遗嘱、房产证)。
- 一张卡放在物理上独立的地方:家里的保险箱、单位的办公桌抽屉,或银行保险箱。
- 一张卡交给信任的人:配偶、成年子女、遗嘱执行人,或多年挚友。
卡片不一定要全是纸质或全是数字,混合存放完全没问题:
- 纸质卡容易离线存放,能扛住损坏硬盘的那些意外。
- 下载的 PNG便于小心复制(备份中的备份,放在保险箱里的 U 盘上),也便于通过端到端加密渠道分享。
- 复制的文本代码便于粘贴进密码管理器、密封便签或加密保险库。
许多人会希望同一张卡既有一份打印件、又有一份图片,分放在两个地方,这样一次火灾或一次磁盘故障不会带走整张卡。
[!IMPORTANT]
不要把两张卡放在同一个地方。两张卡合在一起就是完整秘密。保护来自物理分离,而不是来自格式本身。
给卡片加上标签,让陌生人也能用
卡片上已经印有应用名(2of3.ente.com)、卡片编号和 ID。对于听说过 2of3 的人来说足够了,但多年后被不熟悉的人捡到的一张卡可能需要提示。考虑贴一张便签或信封,写明:
- 这份秘密是用来做什么的(让捡到者知道是否要处理它)。
- 另外两张卡可能在哪里(给捡到者一个着手点)。
- 提醒:三张中的任意两张就足够了。
如果是作为遗产规划的一部分留下卡片,请写下每套卡片保护的是哪个秘密、应由谁持有哪张卡。
在收起卡片之前测试恢复
这是最重要、也最容易被跳过的一步。
在存储卡片之前,滚动到同一页面的Recover部分,用刚制作的三张卡中的任意两张尝试恢复秘密。这一步能发现:
- 打印质量差(二维码太淡或污损)。
- 下载文件被截断。
- 不小心混入的另一套卡片。
- 复制粘贴时在代码末尾丢了一个字符。
如果三组可能的配对(1+2、1+3、2+3)都能恢复出秘密,这套卡就是完好的,可以收起来了。这也是 FAQ 中最被强调的一条建议,见 docs/docs/2of3/faq.md。
恢复秘密
日后任意时间,你可以通过以下任一方式恢复:
- 在 2of3.ente.com 的Recover部分,或
- 随卡片一起下载的离线恢复 HTML 文件。
两条路径的工作方式完全相同,且都完全在浏览器中运行。
- 打开 2of3.ente.com,或从存储位置打开
2of3-recovery.html(大多数操作系统上双击即可,会用默认浏览器打开)。 - 恢复部分有两个槽位:Card A和Card B,二者可互换。
- 对每个槽位,执行以下任一操作:
- Upload image(上传图片):选择保存的卡片 PNG/JPG,或纸质卡的照片。
- Drag and drop(拖放):把图片文件直接拖到槽位上。
- Paste code(粘贴代码):把以
2of3-开头的文本代码粘贴到Code A或Code B字段。
- 槽位一旦收到有效卡片,就会显示它是哪张卡,例如
Card 2 from ID A1B2C3D4。请确认两个槽位显示的是相同的 ID。 - 点击Recover secret。原始秘密会出现在Recovered secret框中。点击Copy复制到剪贴板。
恢复校验的每一步(ID 一致性、卡片编号不同、校验和比对)都可以在combineShares中逐行对应:先parseShare解析,再依次检查版本、ID 与长度、卡片编号,最后用 GF(256) 除法重建秘密字节并比对校验和,见 shamir.ts。
从手机照片恢复
纸质卡的照片可以直接使用,因为 2of3 在读取图片时会尝试多种裁剪。如果手机照片把整张卡拍进了画面、二维码对焦清晰,通常第一次就能解码。如果不行:
- 确保卡片受光均匀,二维码上没有强烈的反光。
- 尝试更紧的裁剪,尽量只包含二维码。
- 如果用了低质量扫描,换一张更高分辨率的照片。
背后的机制:恢复流程会对每张图片依次尝试"整图解码、整图加自动方形裁剪、三种逐级收紧的卡片形裁剪"(裁剪区域比例分别为[0.086, 0.213, 0.829, 0.591]、[0.135, 0.238, 0.73, 0.49]、[0.16, 0.255, 0.68, 0.52]),第一个成功的尝试胜出。这套"宽容"逻辑同样内嵌在离线恢复文件中。详细原理见 How it works 的 QR decoder 一节,实现见 export.ts 中的 decodeQrImage(对应源码为 offline-qr-source.ts 生成的内联解码器)。
从文本代码恢复
如果你只有文本代码(例如粘贴进了密码管理器),把每个代码分别粘贴到Code A或Code B字段即可。代码是以2of3-开头的长字符串。代码内部的空白和换行会被忽略,因此在笔记应用里折行显示的代码也没问题。这对应parseShare开头对输入执行的input.replace(/\s+/gu, ""),见 shamir.ts。
错误信息含义
恢复出错时,2of3 会说明发生了什么。常见的有:
- "These two cards are from different sets. Match the ID on both cards."(这两张卡来自不同套装,请匹配两张卡上的 ID。)你上传的两张卡由不同秘密生成(ID 不同)。检查每个槽位下方的
ID A1B2C3D4一行,两者必须一致。如果不一致,找到属于正确套装的那张卡。对应combineShares中的 ID/长度不一致检查(shamir.ts)。 - "Use two different cards from the same set."(请使用同一套装中两张不同的卡。)你上传了同一张卡两次,或同一张卡的两份拷贝(例如纸质卡和它的 PNG)。你需要同一套装中两个不同的卡号:1 和 2、1 和 3、或 2 和 3。对应 shamir.ts。
- "That code does not look like a 2of3 share."(该代码不像 2of3 的 share。)粘贴的文本不是 2of3 代码。真正的代码总是以
2of3-开头。对应 parseShare 的前缀检查。 - "That share looks incomplete."(该 share 看起来不完整。)或"This share was cut off."(该 share 被截断了。)代码末尾缺少字符。通常意味着复制粘贴丢了代码尾部,或代码换行后漏掉了最后一行。对应 parseShare 的长度校验 与 payload 长度检查。
- "These shares did not reconstruct a valid secret."(这些 share 未能重建出有效秘密。)两张卡解析正常,但没有组合出原始秘密。通常意味着至少一张卡损坏(二维码误读、文本代码被改过、或打印质量差)。试试另一对卡片;一套三张卡共有三种配对。对应 combineShares 的校验和比对失败。
- "Could not read that image."(无法读取该图片。)或"Could not read that QR code."(无法读取该二维码。)上传的图片不是可识别的二维码。尝试更清晰的照片、更紧的裁剪,或上传原始 PNG 而非截图。
替换或轮换一套卡片
如果需要更改底层秘密(例如轮换了密码),请把它当作一套全新的 2of3 卡:
- 用新秘密生成一整套三张新卡。
- 把新卡分发到原来的三个地方。
- 销毁旧卡(碎纸销毁纸质卡、删除 PNG、从笔记中清除文本)。
不要"以防万一"保留一张旧卡。一张旧卡和别处的另一张旧卡组合,仍然能恢复旧秘密——而这恰恰是你不再希望它可恢复的东西。正如 FAQ 所说,2of3 没有"就地编辑某一张卡"的概念,因为数学上三张卡通过同一个逐字节随机系数关联在一起,见 docs/docs/2of3/faq.md。
卡片上到底是什么
理解卡面内容有助于信任这套工具,也为日后自建恢复工具提供了依据。
文本代码格式
每张卡携带一份 share,编码为一小段文本代码(同一张卡上同时渲染为二维码,便于手机拍摄)。代码形如:
2of3-AQEA...2of3-前缀让读者一眼识别代码,也让 2of3 能对粘贴的非 share 文本给出明确报错("That code does not look like a 2of3 share.")。前缀之后是 base64url 编码的字节负载,包含 10 字节头部加 share 数据。
Share 格式字段
从开头读起,负载结构为:
| 字节 | 字段 | 含义 |
|---|---|---|
| 0 | Version(版本) | 当前格式恒为2。恢复同时支持版本 1 创建的卡片。 |
| 1 | Card index(卡片编号) | 1、2或3,即恢复时用于合并 share 的"x 值"。 |
| 2-3 | Length(长度) | 秘密字节数的 16 位大端长度。恢复时两份 share 必须一致。 |
| 4-9 | Random ID(随机 ID) | crypto.getRandomValues生成的 6 字节随机值,每套卡生成一次。同一秘密的三张卡共享此 ID。 |
| 10+ | Share data(份额数据) | 秘密字节的 share,后随 4 字节校验和。校验和与秘密受相同的"两张卡"门限保护。 |
以上字段与 How it works 的 Share format 一节 完全对应,并在源码 shamir.ts 的 splitSecret 头部构建 与 parseShare 的解析 中逐字节落实。卡片 UI 上显示的ID A1B2C3D4是人类可读指纹,即 base64url 编码 ID 的前 8 个字符(parseShare(value).id.slice(0, 8).toUpperCase(),见 index.tsx 中的 shareFingerprint 与 export.ts 中的指纹渲染)。
格式兼容性
早期版本创建的是版本 1 卡片,它公开存放校验和,允许单张卡对低熵秘密进行猜测确认。版本 2 把校验和保护在两张卡门限之后。恢复同时支持两种版本(如果你持有更老的离线恢复页面,请生成一套新卡以获得最新版,或直接在 2of3.ente.com。
离线恢复文件:十年后 2of3.ente.com 不在了怎么办
点击Download all cards时,2of3 会在三张 PNG 之外附带一个额外文件:2of3-recovery.html。这是恢复流程的离线副本,里面包含:
- 与 2of3.ente.com 相同的恢复界面(独立 HTML 页面)。
- 完整的 Shamir 合并逻辑,以内联 JavaScript 写就。
- 一个内嵌的二维码解码器,可以本地解码纸质卡的照片。
- 零网络请求,不依赖 Ente、2of3.ente.com 或任何 CDN。
双击即可打开,任何现代浏览器都能运行。用这个文件恢复与在 2of3.ente.com 上恢复完全一致:放入两张卡(图片或文本)、点击Recover secret、复制结果。
这个文件回答了"十年后 2of3.ente.com 消失了怎么办"的问题:答案是什么都不会发生。打开随卡片一起存储的 HTML 文件即可恢复。即使 Ente 公司不在了,这个文件也不需要从我们这里获取任何东西——它是几百行可审查的 JavaScript,行为与 2of3.ente.com 完全一致。从源码看,该文件由createOfflineRecoveryHtml生成,它把离线二维码解码器源码(来自qr-raw/decode.js与qr-raw/index.js,经stripSourceMapComment等处理后去依赖化)内联进globalThis.__twoOf3DecodeQR,并内联了与线上完全相同的 GF(256) 与合并逻辑,见 export.ts 中的 createOfflineRecoveryHtml 与 offline-qr-source.ts。
[!IMPORTANT]
把离线恢复文件当作备份对待。至少在某处存一份耐久的副本:U 盘、备份盘,或与卡片一起放进保险箱打印出来。卡片是秘密本身,恢复文件是打开它们的工具。
由于这个文件是自包含的纯 JavaScript,你还可以把它当作验证目标:通读、静态分析、或与开源仓库中的实现对照,来确认它确实做了它声称的事。这里没有需要信任的远程服务。
背后的数学:Shamir 秘密共享简述
2of3 建立在Shamir 秘密共享之上,这是 1979 年提出的经典密码学方案。2of3 使用其最简单而有意义的形态:把秘密拆成三份 share,任意两份即可还原原秘密,而单份 share 除字节长度外不泄露任何信息。
直觉模型是一条经过两点的直线:任取一条穿过 y 轴的直线,y 截距就是"秘密",斜率是随机数、用完即弃。选三个不同的 x 值(如 x = 1、2、3),算出直线上对应的 y 值。每个 (x, y) 对就是一份 "share":
- 给某人一份share(直线上一个点),他能画出无穷多条经过该点的直线,对 y 截距一无所知。
- 给某人任意两份share(两个点),有且仅有一条直线经过它们,可以算出 y 截距,即秘密。
如果在普通数域上做这件事会泄露信息(例如大秘密的 share 尺寸可预测),因此 Shamir 方案在有限域上做算术。2of3 使用GF(256)(含 256 个元素的有限域),其便利之处在于每个字节都是合法元素,从而可以逐字节拆分秘密,且每份 share 与秘密等长。
对于 2of3 使用的 (k=2, n=3) 方案,逐字节的数学可以完整写出:
- 对每个秘密字节s,2of3 挑选一个均匀随机的字节r。
- Share 1 存放sXORr。
- Share 2 存放sXOR (r × 2 in GF(256))。
- Share 3 存放sXOR (r × 3 in GF(256))。
GF(256) 乘法使用标准的 AES 多项式(0x11b)。上述三份中的任意两份加上它们的编号("x 值"1、2、3)即可逐字节恢复s,从而得到完整秘密。单独一份 share 则是sXOR(某个随机字节),与随机数不可区分。
这些公式可以在源码中逐行对应:splitSecret 用gfMul(coefficient, 2)与gfMul(coefficient, 3)生成三份 share,gfMul以GF_POLY = 0x11b做多项式约减(shamir.ts);combineShares 用gfDiv(基于费马小定理的gfPow(value, 254)求逆,shamir.ts)做拉格朗日插值合并。校验和是 32 位 FNV-1a 哈希(初始值0x811c9dc5、乘数0x01000193,见 checksumBytes):拆分前追加到秘密之后,合并后再计算比对——校验和同样被门限拆分保护,因此单张卡无法用它试探秘密。
[!NOTE]
2of3 特意只实现 2-of-3 这一种情形,以求清晰与简单。同样的方案也支撑着 Ente 账户版的 Legacy Kit:它在同样的 2-of-3 数学之上,叠加了与 Ente 账号绑定、可撤销的服务器中介恢复流程。如果需要通用的 k-of-n Shamir 共享用于研究项目,可以去看 SLIP-0039 等标准;2of3 的使命是让 2-of-3 对普通人零摩擦,而不是做一个通用密码学库。
隐私边界与典型问题
- 秘密会发给 Ente 吗?不会。拆分与恢复都在浏览器内完成,没有账号、没有登录,你的秘密不会离开页面。下载的离线恢复文件也完全离线工作。页面本身作为 ente.com 上的普通页面托管,存在常规的服务器请求日志(页面加载的 IP),但你输入的秘密与生成的卡片完全在设备本地处理,见 FAQ 的隐私章节。
- 单张卡能泄露秘密吗?不能。单张卡的 share 数据在数学上无害,看起来是随机的——因为它确实就是:受保护的数据与逐字节均匀随机的字节做 XOR。它只暴露秘密的字节长度,不暴露任何内容,见 FAQ。
- 和密码管理器有何区别?密码管理器用于日常使用的秘密;2of3 用于密码管理器自身所依赖的那一两个秘密(主密码、恢复密钥、助记词),两者互补而非替代。2of3 也不是存放密码的地方——它是一次性的单秘密拆分,没有 vault、没有同步、没有多条目概念,见 FAQ 与 FAQ。
- 聊天应用破坏了链接或文本会怎样?如果通过聊天应用发送文本代码导致换行、混入不可见字符或自动格式化,接收方可能拿到损坏的代码。2of3 忽略普通空白与换行,折行代码没问题;但如果应用把字符变成"智能引号"或剥离了
2of3-前缀,代码将无法解析。更稳妥的做法是分享卡片的PNG 图片、原始 PNG 附件,或通过不修改消息体的端到端加密渠道发送,见 FAQ。
深入阅读
- 2of3 总览:2of3 是什么、何时使用。
- How it works:Shamir 秘密共享、share 格式、离线恢复文件与 QR 解码器。
- FAQ:关于安全性、存储与边界情况的常见问题。
- 开源实现:web/apps/twoof3 下的 Next.js 应用,核心逻辑集中在 utils/shamir.ts(GF(256) 与拆分/合并)、utils/export.ts(卡片渲染与离线恢复文件生成)、utils/qr-code.ts(二维码编码,ECC 等级
medium、边框 4 模块)。
按照本文的完整流程操作一遍——拆分、分点存储、测试恢复、离线文件备份——你就拥有了一个不依赖任何在线服务、数学上可验证、十年后依然可用的秘密恢复体系。
【免费下载链接】ente💚 End-to-end encrypted cloud for everything.项目地址: https://gitcode.com/GitHub_Trending/en/ente
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考