news 2026/9/11 21:24:40

Ente Photos Legacy 账户继承功能详解:如何通过可信联系人移交你的回忆

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ente Photos Legacy 账户继承功能详解:如何通过可信联系人移交你的回忆

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)"流程:

  1. 可信联系人发起恢复请求(Initiate Recovery);
  2. 账户所有者收到通知,拥有 7、14 或 30 天(在添加联系人时配置)的时间窗口来**阻止(block)**这次恢复;
  3. 若在等待期内无人阻止,联系人即可重设账户密码,从而解密并访问账户中的回忆。

这个设计在"传递遗产"与"防止恶意接管"之间取得了平衡:既保证联系人最终能够接管,又给了所有者充足的知情与反悔空间。

需要特别强调的是,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 方案端到端加密属性的根基——服务端与攻击者都无法在中途窃取恢复钥匙。

接受可信联系人邀请

添加请求发出后,必须由对方接受,委托关系才正式生效。对方(可信联系人)需要:

  1. 在 Ente Photos 移动端进入Settings -> Account -> Legacy
  2. Legacy accounts(Legacy 账户)分区中点击你的邮箱地址;
  3. 在弹出的确认框中接受邀请。

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 Contactsinfo.contacts你添加的可信联系人列表,可点击进行撤销/移除/修改通知期
Legacy Accountsinfo.othersEmergencyContact你作为可信联系人的其他账户列表,可点击接受邀请或发起恢复

其中"Legacy Accounts"分区即文档中提到的 "Legacy accounts sections",是联系人接受邀请与发起恢复的统一入口。

作为可信联系人恢复账户

发起恢复

作为已接受邀请的可信联系人,恢复他人账户的入口同样在Settings -> Account -> Legacy

  1. Legacy Accounts分区中点击该账户的邮箱地址;
  2. 进入账户详情页后点击Start Recovery(开始恢复)按钮。

在客户端,发起恢复的确认与执行位于 other_contact_page.dart:点击后弹出确认底部弹窗(startRecoveryDesc会明确展示目标邮箱),确认后调用EmergencyContactService.instance.startRecovery(contact),最终经由 emergency_service.dart 的startRecovery把请求发送到服务端。

服务端的恢复会话创建与通知

服务端StartRecovery(见 recovery_contact.go)的执行逻辑非常严谨,包含三重校验:

  1. 联系人不能是自己contact and user can not be same
  2. 只有紧急联系人本人才可发起only the emergency contact can start recovery(校验EmergencyContactID == actorUserID);
  3. 必须是仍然有效的(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)协议流程:

  1. 生成新的随机 SRP 用户名(UUID v4)与 16 字节随机盐;
  2. 使用 RFC 5054 4096 位群参数 + SHA-256 生成 SRP 验证子(verifier);
  3. 调用initPasswordChange向服务端提交 SRP 握手的第一条消息(srpA),服务端返回srpB
  4. 客户端计算会话密钥与证据消息M1,调用changePassword提交新密码的 SRP 证据与新密钥属性(updatedKeyAttr)。

服务端对应实现见 recovery.go:InitChangePasswordChangePassword均会先通过checkRecoveryAndGetContact校验"当前操作者必须是该恢复会话的紧急联系人"且"会话允许恢复(CanRecover)",成功修改密码后还会把恢复会话状态更新为Recovered并发出通知。

至此,联系人可以用新密码登录该账户,访问其中所有加密的回忆数据。

账户所有者如何阻止恢复

当可信联系人发起恢复后,账户所有者的 App 会立刻进入"戒备"状态:

  1. 打开Settings > Account > Legacy,页面顶部会出现恢复已发起的警告横幅(客户端对应_WarningBanner,见 emergency_page.dart,展示recoveryWarning文案并以警示色高亮);
  2. 点击该警告项,会弹出确认对话框,其中包含目标联系人的邮箱信息;
  3. 确认后调用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状态、通知所有者。

移除可信联系人

作为账户所有者,你可以随时移除已建立的可信联系人:

  1. 进入Settings > Account > Legacy
  2. 点击要移除的可信联系人;
  3. 在弹出的操作菜单中选择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 份额分发的思路,引入了noticePeriodInHoursactiveRecoverySession、恢复挑战(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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 21:23:36

拉丁超立方抽样:从分层采样到不确定性传播的工程实践

简介&#xff1a;面向数据分析、模拟预测与风险评估场景&#xff0c;这份资料包聚焦不确定性处理中的拉丁超立方抽样&#xff08;LHS&#xff09;技术&#xff0c;并结合数据正态分布与超立方抽样概念。不确定性在复杂系统和模拟中普遍存在&#xff0c;传统蒙特卡洛往往需要大量…

作者头像 李华
网站建设 2026/9/11 21:23:07

CMake核心知识体系梳理:从目标、缓存到生成器,告别构建困扰

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 21:20:16

SerenityOS 中的 futimens 与 utimensat:文件时间戳更新机制全解析

SerenityOS 中的 futimens 与 utimensat&#xff1a;文件时间戳更新机制全解析 【免费下载链接】serenity The Serenity Operating System &#x1f41e; 项目地址: https://gitcode.com/GitHub_Trending/se/serenity 导读 本文基于 SerenityOS 官方手册页 utimensat(3…

作者头像 李华