Ente Photos Legacy 账户继承功能详解:如何通过可信联系人移交你的回忆
【免费下载链接】ente💚 End-to-end encrypted cloud for everything.项目地址: https://gitcode.com/GitHub_Trending/en/ente
Legacy 是 Ente Photos(端到端加密云相册)内置的账户继承机制,允许账户所有者指定可信联系人,在其缺席(如离世、遗忘密码或丢失恢复密钥)时安全接管账户并访问加密回忆。本文以 Ente 官方文档为主体,结合 移动端实现 与服务端 emergency 控制器 源码,完整讲解可信联系人的添加、邀请接受、恢复发起、恢复阻止与联系人移除全流程,并深入剖析其底层的通知期(7/14/30 天)倒计时与 SRP 密码重设原理。
Legacy 是什么:为"缺席"而设计的账户继承方案
Legacy 的核心能力是:允许你信任的联系人,在你无法再管理账户时恢复你的 Ente Photos 账户。官方文档明确其最主要的应用场景是"在你去世后把回忆传递给你爱的人",同时也覆盖其他常见情形——例如当你忘记密码和恢复密钥时,可信联系人同样可以帮你找回账户。
整个机制的关键设计是一套"冷静期(cooling-off period)"流程:
- 可信联系人发起恢复请求(Initiate Recovery);
- 账户所有者收到通知,拥有 7、14 或 30 天(在添加联系人时配置)的时间窗口来**阻止(block)**这次恢复;
- 若在等待期内无人阻止,联系人即可重设账户密码,从而解密并访问账户中的回忆。
这个设计在"传递遗产"与"防止恶意接管"之间取得了平衡:既保证联系人最终能够接管,又给了所有者充足的知情与反悔空间。
需要特别强调的是,Ente 是端到端加密服务,服务端并不持有用户的主密钥。因此 Legacy 恢复的本质不是"服务端直接放行",而是联系人通过受保护的密钥交接流程,合法地完成一次密码与密钥的重置(详见后文源码分析)。
添加可信联系人
操作入口
使用 Ente Photos 移动端 App,按以下路径进入:
Settings(设置) > Account(账户) > Legacy点击"Add Trusted Contact"(添加可信联系人)按钮。
填写联系人信息
进入添加页面后,你需要:
- 输入要添加的可信联系人的邮箱地址,或
- 从 Ente 联系人列表中直接选择。
文档特别强调了一个硬性前提:可信联系人必须是 Ente 用户。这一点在服务端有严格校验——在 account_owner.go 的AddContact实现中,服务端会先通过邮箱执行用户查找(LookupUserID),若目标邮箱在 Ente 中不存在(sql.ErrNoRows),会直接返回ErrNotFound并附带错误信息 "invited member is not on ente"(受邀成员不在 Ente 上),请求即被拒绝。
通知期(Recovery Notice)的默认值
从源码看,服务端在添加联系人时会计算恢复通知期:
noticeInHrs := 24 * 30 // 默认 30 天 if request.RecoveryNoticeInDays != nil { noticeInHrs = *request.RecoveryNoticeInDays * 24 }即:客户端若未显式传入天数,默认通知期为 30 天(24 × 30 小时);若传入天数则以天数为准换算成小时存储。7 天、14 天、30 天这些选项即来自该字段,服务端最终以小时为单位持久化,并在响应中按NoticePeriodInHrs / 24折算回天数返回给客户端展示(见 account_owner.go)。
密钥交接:恢复钥匙如何安全到达联系人
在添加联系人的请求中,客户端会携带一个关键字段EncryptedKey——这是用可信联系人的公钥加密后的账户恢复密钥(recovery key)密文。服务端只负责存储这段密文(见 AddContact 中的request.EncryptedKey落库),自身无法解密。
恢复发生时,联系人通过GetRecoveryInfo接口取回这段密文,再用自己的私钥解密:
final decryptedKey = CryptoUtil.openSealSync( CryptoUtil.base642bin(encryptedKey), CryptoUtil.base642bin(_config.getKeyAttributes()!.publicKey), _config.getSecretKey()!, ); final String hexRecoveryKey = CryptoUtil.bin2hex(decryptedKey);以上代码来自移动端 emergency_service.dart:openSealSync即 X25519 + Secretbox 的非对称封包解密,用联系人自己的密钥对解开"仅发给他的"恢复密钥。这正是整个 Legacy 方案端到端加密属性的根基——服务端与攻击者都无法在中途窃取恢复钥匙。
接受可信联系人邀请
添加请求发出后,必须由对方接受,委托关系才正式生效。对方(可信联系人)需要:
- 在 Ente Photos 移动端进入
Settings -> Account -> Legacy; - 在Legacy accounts(Legacy 账户)分区中点击你的邮箱地址;
- 在弹出的确认框中接受邀请。
Ente 同时会向可信联系人发送邮件通知,提醒其处理这份邀请。
从客户端源码看,接受/拒绝动作在 emergency_page.dart 的showAcceptOrDeclineDialog中实现:
- 点击Accept→ 调用
legacy.updateContact(state: LegacyContactState.accepted),将关系状态更新为accepted; - 点击Decline→ 调用
legacy.updateContact(state: LegacyContactState.contactDenied),拒绝并移除该请求。
对应地,服务端关系状态机(以LegacyContactState枚举体现)至少包含:invited(待接受)、accepted(已接受)、revoked(被所有者撤销)、contactDenied(被联系人拒绝)、contactLeft(联系人主动退出)等状态——客户端 model.dart 中的isPendingInvite()即通过state == LegacyContactState.invited判断请求是否仍处于待处理状态。
移动端 Legacy 页面的三种分区
从 emergency_page.dart 的实现可以看到,Legacy 页面实际由三个逻辑分区构成,这有助于你理解整个界面的信息组织:
| 分区 | 对应源码字段 | 说明 |
|---|---|---|
| 恢复警告区 | info.recoverSessions | 有恢复会话在进行时,顶部出现警告横幅(recoveryWarning),所有者可在此点击进入"拒绝恢复"流程 |
| Trusted Contacts | info.contacts | 你添加的可信联系人列表,可点击进行撤销/移除/修改通知期 |
| Legacy Accounts | info.othersEmergencyContact | 你作为可信联系人的其他账户列表,可点击接受邀请或发起恢复 |
其中"Legacy Accounts"分区即文档中提到的 "Legacy accounts sections",是联系人接受邀请与发起恢复的统一入口。
作为可信联系人恢复账户
发起恢复
作为已接受邀请的可信联系人,恢复他人账户的入口同样在Settings -> Account -> Legacy:
- 在Legacy Accounts分区中点击该账户的邮箱地址;
- 进入账户详情页后点击Start Recovery(开始恢复)按钮。
在客户端,发起恢复的确认与执行位于 other_contact_page.dart:点击后弹出确认底部弹窗(startRecoveryDesc会明确展示目标邮箱),确认后调用EmergencyContactService.instance.startRecovery(contact),最终经由 emergency_service.dart 的startRecovery把请求发送到服务端。
服务端的恢复会话创建与通知
服务端StartRecovery(见 recovery_contact.go)的执行逻辑非常严谨,包含三重校验:
- 联系人不能是自己:
contact and user can not be same; - 只有紧急联系人本人才可发起:
only the emergency contact can start recovery(校验EmergencyContactID == actorUserID); - 必须是仍然有效的(active)联系人关系:通过
GetActiveEmergencyContact从数据库中取出仍处于有效状态的委托记录。
随后服务端将恢复会话插入数据库(InsertIntoRecovery),并向账户所有者发送"恢复已发起"的通知邮件(sendRecoveryNotification),邮件中附带换算后的通知期天数:recoveryNoticeInDays := contact.NoticePeriodInHrs / 24。
等待期与倒计时
恢复发起后,账户所有者会获得 7、14 或 30 天(以配置为准)的时间来阻止这次恢复。这个等待期在客户端以"距可恢复时间"的倒计时形式呈现:other_contact_page.dart 用当前时间加上waitTill(服务端返回的剩余微秒数)计算出可恢复的精确时刻并格式化显示。
服务端对"等待期"的管理由一个后台定时任务(SendRecoveryReminder,见 recovery.go)驱动,其核心逻辑为:
- 当
WaitTill已过且会话仍处于Waiting状态时,自动将状态更新为Ready(可恢复),并向双方发送通知; - 在等待期内按剩余天数梯度发送提醒邮件:剩余>9 天时,下次提醒安排在 7 天后;剩余2~9 天时,下次提醒安排在到期前 2 天;剩余 <2 天时不再追加提醒(避免骚扰);
- 该定时任务通过 Redis 分布式锁(
_recoveryReminderLock,锁定 1 小时)保证多实例下只有一个节点在执行,防止重复发信。
正是这套"状态机 + 定时任务"确保了7/14/30 天倒计时不依赖客户端在线,无论所有者是否登录 App,到期后都会自动变为可恢复状态。
到期后修改密码并接管
等待期结束、会话状态变为Ready后,联系人回到同一页面,App 会提示Recover Account(恢复账户)按钮(见 other_contact_page.dart)。点击后流程分两步:
第一步:获取恢复密钥。getRecoveryInfo从服务端取回密文形式的恢复密钥与当前KeyAttributes,并用联系人自己的私钥解出明文恢复密钥(前文已述)。
第二步:执行密码重设(SRP)。客户端 emergency_service.dart 的changePasswordForOther走的是标准的 SRP(Secure Remote Password)协议流程:
- 生成新的随机 SRP 用户名(UUID v4)与 16 字节随机盐;
- 使用 RFC 5054 4096 位群参数 + SHA-256 生成 SRP 验证子(verifier);
- 调用
initPasswordChange向服务端提交 SRP 握手的第一条消息(srpA),服务端返回srpB; - 客户端计算会话密钥与证据消息
M1,调用changePassword提交新密码的 SRP 证据与新密钥属性(updatedKeyAttr)。
服务端对应实现见 recovery.go:InitChangePassword与ChangePassword均会先通过checkRecoveryAndGetContact校验"当前操作者必须是该恢复会话的紧急联系人"且"会话允许恢复(CanRecover)",成功修改密码后还会把恢复会话状态更新为Recovered并发出通知。
至此,联系人可以用新密码登录该账户,访问其中所有加密的回忆数据。
账户所有者如何阻止恢复
当可信联系人发起恢复后,账户所有者的 App 会立刻进入"戒备"状态:
- 打开
Settings > Account > Legacy,页面顶部会出现恢复已发起的警告横幅(客户端对应_WarningBanner,见 emergency_page.dart,展示recoveryWarning文案并以警示色高亮); - 点击该警告项,会弹出确认对话框,其中包含目标联系人的邮箱信息;
- 确认后调用
EmergencyContactService.instance.rejectRecovery(session)(emergency_service.dart)拒绝本次恢复。
服务端RejectRecovery(见 recovery_contact.go)同样有严格鉴权:只有账户所有者本人(req.UserID == userID)才能拒绝,且拒绝的会话必须精确匹配该所有者与该联系人(getRecoverySessionMatchingRequest)。拒绝成功后,会话状态置为Rejected并通知联系人,恢复流程即终止。
客户端还保留了一个调试专用的 "Approve recovery(to be removed)" 按钮(仅在
kDebugMode下出现),用于开发者验证批准路径,普通用户不可见。
联系人侧可随时取消
作为发起方,联系人在等待期内也可以自行取消:在账户详情页点击Cancel Recovery按钮(other_contact_page.dart),调用stopRecovery。服务端StopRecovery会校验"只有紧急联系人本人可停止",并将会话置为Stopped状态、通知所有者。
移除可信联系人
作为账户所有者,你可以随时移除已建立的可信联系人:
- 进入
Settings > Account > Legacy; - 点击要移除的可信联系人;
- 在弹出的操作菜单中选择Remove(移除)。
从客户端源码看,点击联系人后弹出的是showTrustedContactSheet操作面板(见 emergency_page.dart),它支持两类操作:
- Revoke / Cancel invite(撤销邀请):若该联系人还处于
invited待接受状态,可取消邀请(state: LegacyContactState.revoked); - Remove contact(移除联系人):对已接受的联系人执行移除,同样通过
legacy.updateContact将状态更新为revoked。
需要特别提醒的是:修改通知期(Update Recovery Notice)在存在进行中的恢复会话时会被拒绝——客户端会捕获LegacyError_ActiveRecoverySession异常并弹出 "cannot update recovery time" 提示(emergency_page.dart)。服务端 account_owner.go 的UpdateRecoveryNotice逻辑也印证了这一点:存在activeSessions时直接返回ErrActiveRecoverySession,且合法通知期被限制在1~60 天之间。
作为联系人,你也可以主动退出委托关系:在账户详情页点击 "Remove Contact"(或 "Or remove yourself"),调用legacy.updateContact(state: LegacyContactState.contactLeft)(见 other_contact_page.dart),将自己从该账户的可信联系人列表中移除。
从源码看 Legacy 的完整状态流转
综合上述文档与源码,Legacy 功能可归纳为两条独立的状态机:
联系人类(LegacyContactState):invited(待接受)→accepted(已建立)→ 终态:revoked(所有者撤销)/contactDenied(联系人拒绝)/contactLeft(联系人退出)。
恢复会话类(RecoveryStatus):Initiated(已发起)→Waiting(等待期内,所有者可Rejected拒绝、联系人或所有者可Stopped停止)→ 到期自动Ready(可恢复)→ 改密成功后Recovered(完成)。
两个状态机的联动关系清晰体现在数据层:恢复会话与联系人关系、所有者、联系人三元组严格绑定,每次操作都做"操作者身份 + 会话匹配"双重校验(getRecoverySessionMatchingRequest,recovery_contact.go)。
值得一并提及的是,当前仓库还在演进一个更新的继承方案——Legacy Kit(见 server/ente/legacy_kit.go),它基于 Shamir 份额分发的思路,引入了noticePeriodInHours、activeRecoverySession、恢复挑战(LegacyKitChallengeRequest)等更精细的模型。从类型定义(LegacyKitVariantTwoOfThree)可以推断,这是面向"2-of-3 多方共同接管"的更灵活继承机制,可作为理解 Legacy 未来演进的参考。
小结
Ente Photos 的 Legacy 功能用一套**"加密密钥交接 + 延迟生效 + 双向通知"**的机制,在端到端加密的严格约束下实现了安全的账户继承:
- 安全前提:恢复密钥始终以收件人公钥加密存储,服务端不可见;所有操作均有身份与会话双重鉴权;
- 冷静期设计:7/14/30 天可配置等待期配合服务端定时任务自动推进状态,既允许所有者阻止,又保证委托人最终能够接管;
- 完整生命周期:从添加联系人、接受邀请,到发起恢复、阻止恢复、移除联系人,所有环节在移动端与 服务端控制器 中均有对应实现与校验。
无论你是想为家人配置一份"数字遗产",还是想了解端到端加密体系下账户恢复的工程实践,Ente Photos 的 Legacy 都是一个值得研究且可直接上手的完整参考实现。
【免费下载链接】ente💚 End-to-end encrypted cloud for everything.项目地址: https://gitcode.com/GitHub_Trending/en/ente
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考