Memories / 文档

本页目录

授权中心

自助账户与 SMTP

在托管模式下传入 --auth-config /etc/memories-center/auth.toml(或设置 MEMORIES_CENTER_AUTH_CONFIG)。参见 deploy/center/auth.example.toml。未提供认证配置时,Center 保持仅使用令牌的行为。提供配置后会启用账户登录;注册默认禁用,注册模式默认为电子邮件。

registration_enabled = true
registration_mode = "email" # or "username"
max_accounts = 10000

[smtp]
host = "smtp.example.com"
port = 587
tls = "starttls" # required STARTTLS; "tls" for implicit TLS, commonly port 465
from = "Memories <noreply@example.com>"
username = "smtp-user"
password_env = "MEMORIES_CENTER_SMTP_PASSWORD"

SMTP 密码从指定的环境变量读取。将该变量保存在受保护的服务环境中,切勿放入参数、URL、源代码管理或浏览器配置。不需要认证的中继可以省略 username;仍然必须使用经过证书验证的 TLS。配置无效会阻止启动。缺少 SMTP 时会禁用电子邮件注册;传输失败返回 503,且不会创建账户。公开配置响应从不包含 SMTP 设置。

设置 registration_enabled = false 可关闭新注册而不禁用现有账户。设置 registration_mode = "username" 可使用不需要电子邮件的用户名/密码注册;如果不使用 SMTP,可省略 [smtp]。修改设置后需要重启。切换模式后,两种类型的现有账户都可以继续登录。

公开端点(JSON,请求正文最大 4 KiB):

端点 正文 / 行为
GET /v1/auth/config 登录可用性、注册可用性/模式、密码最小长度、会话时长
POST /v1/auth/register 电子邮件模式:{identity,locale?} 发送验证码。用户名模式:{identity,password} 创建账户
POST /v1/auth/verify {identity,code,password} 验证邮箱所有权并创建账户
POST /v1/auth/login {identity,password} 返回有效期为一小时、具有限定权限范围的 bearer 授权
POST /v1/auth/logout 使用 bearer 认证;撤销该有期限的授权及其后代授权

电子邮件地址和用户名会去除首尾空白,并进行 ASCII 大小写归一化。电子邮件注册仅接受 ASCII 邮箱地址。用户名为 3-64 个 ASCII 字母、数字、连字符或下划线。密码为 12-256 个 UTF-8 字节,以加盐的 Argon2id PHC 记录保存(64 MiB、3 轮、并行度 1)。哈希计算在保险库锁之外运行,最多两个并发工作线程。用户在注册时不能选择保险库、权限范围或角色:每人都会获得一个随机的独立托管保险库。

验证码包含 256 位随机数据,15 分钟后过期,且只能使用一次。服务器内存中仅保留哈希;重启会使待处理验证码失效。在该时间窗口内重复发送会返回相同的通用接受响应,不会再发送电子邮件;请在过期后重试。密码在验证时选择,而不是在请求电子邮件时选择。待处理注册数量上限为 1024。 可选的 locale 选择邮件主题和正文:en、zh-CN、zh-TW、ja、fr、de 或 es。区域变体使用其基础语言;zh-Hant、zh-HK 和 zh-MO 选择繁体中文,显式指定 zh-Hans 则选择简体中文。缺失或不支持的语言区域会回退到英语,以兼容现有 API 客户端。语言区域按请求生效,不保存在账户中。在验证挑战尚未结束时更改语言,不会再发一封邮件或绕过其速率限制,也不会改变身份归一化或验证行为。 认证尝试的全局上限为 60 次/分钟;每个身份每 15 分钟最多 3 次注册请求或 10 次登录/验证尝试。这些进程内限制在重启后重置,在攻击下可能导致拒绝服务;较大规模的部署应使用上游滥用控制服务。它们不信任客户端提供的 IP 标头。

现有持久成员令牌仍受支持。members.json 格式现在具有可选的账户凭据:应像保护密码数据库一样保护其备份。旧版 Center 无法读取账户记录;注册后回滚需要兼容的二进制文件或匹配的升级前数据备份(这会丢弃后续数据)。尚未实现密码重置、邮箱更改和邮件恢复。管理员删除成员会禁用登录并使会话失效。

Center 是一个外部授权控制平面,向 Memories 端点(CLI、桌面守护进程、移动端,以及共享配置的其他客户端)分发注册和能力票据。

托管 Center 还支持 Memories Web,这是用于记忆、卡片和文件的浏览器客户端。新版本在 /app/ 提供该客户端。要从公共网站连接,需显式允许 --web-origin https://memories.ivjn.us(或 MEMORIES_CENTER_WEB_ORIGINS)。同源使用无需 CORS 设置;现有成员/授权认证仍然适用。设置与限制参见该指南。

规则

  1. 端点可以在 config.toml([[centers]])中配置一个或多个 Center。
  2. 如果配置了任何 Center,该端点即为 Center 管理的端点: - 禁止本地自行初始化(memories init / restore)。 - 创建设备使用 Center 签名的初始化票据(memories center enroll)。 - 禁止在本地签发 MCP 授权;应导入 Center 签名的授权票据。
  3. 设备保险库的密码学解锁仍在本地进行(设备密码)。仅提供票据的 Center 从不接收设备保险库密钥或明文秘密。可选的托管模式则拥有一个独立的、由运营者持有密钥的服务器保险库(见下文)。可选的签名解锁票据授权本地密码解锁;它既不携带密码,也不携带设备密钥。
  4. 可以列出多个 Center;票据必须通过签发它的 Center 的 id 和 public_key 验证。
  5. 监听令牌、Center 签名秘密和管理员令牌绝不保存在 TOML 中。

授权范围

MCP 授权票据携带显式的权限范围(权限级别)。传输协议 / 配置标识符使用 snake_case;CLI 和 HTTP 也接受便于阅读的别名。

范围 别名(示例) 允许的操作
allow_all allow all, all, full 此授权接口可以授权的全部操作(目前:普通资产的读取 + 创建/更新 + 删除;为未来同一接口上由 Center 控制的提权能力预留)
read r 列出 / 获取 / 搜索 / 检索 / RAG 读取 / 普通图谱——不允许修改
write w 仅创建/更新普通记忆和卡片(不允许读取,不允许删除)
read_write read+write, rw 读取 + 创建/更新
read_write_delete read+write+delete, rwd 读取 + 创建/更新 + 删除普通记忆/卡片

说明:

  • write 有意设计为仅写入,以便 Center 签发权限范围狭窄的代理。代理也需要搜索或读取时,优先使用 read_write。
  • 删除权限覆盖对 MCP 调用者执行的普通删除(非私密的记忆/卡片);私密内容/凭据删除仍需本地提权,不属于 MCP 授权。
  • 旧票据仅有旧的 write: bool 字段(没有 scope):write: false → read,write: true → read_write。
  • 未显式指定范围的新票据在签发时默认为 read(CLI / memories-center HTTP)。
  • 这些普通资产/提权限制描述的是守护进程 MCP,而非托管保险库 RPC。托管授权还覆盖私密卡片、附件和受保护配置档;其确切权限见下文。此功能不会提升守护进程 MCP 的权限,即使其范围为 allow_all。

本地票据导入现在检查 Admin/Owner 成员资格以及角色/范围上限,而不仅仅检查写入权限,且不需要终端提权。本地委派、请求、访问和审计行为,以及分阶段的 Center/CLI/MCP 推出,参见保险库授权。

与威胁模型的关系

关注点 行为
设备注册 Center 决定哪些设备可以初始化存储
MCP / 代理授权 Center 签发带权限范围和时限的授权票据;端点导入这些票据
设备保险库机密性 仍使用 Argon2 + 本地存储加密;仅提供票据的 Center 是策略控制,而非密钥托管
托管保险库机密性 服务器端加密的 Store;主机运营者持有密码,可以读取所有托管明文;不是端到端加密
Center 签名密钥泄露 攻击者可以为该 Center id 注册设备 / 签发授权——应轮换密钥,并从端点移除过时的 public_key
Center 管理员被攻破 可以签发带时限的设备解锁票据、锁定托管 Store,以及创建/更新/移除托管成员;没有本地密码就无法解密仅使用票据的设备 Store
端点被攻破 解锁后持有保险库材料;Center 无法撤销已经在磁盘上的密文(与目前相同)

端点配置

[[centers]]
id = "corp"
url = "https://center.example.com"
public_key = "…"   # from `memories center public-key` or memories-center /v1/public-key
mode = "ticket"    # default: enrollment/grants/unlock tickets only

[[centers]]
id = "team"
url = "http://127.0.0.1:9191"   # loopback http allowed for local Center
public_key = "…"

非回环 Center URL 必须使用 https://。SaaS 附加连接/直接 CLI URL 必须仅为源地址,不得包含用户信息、路径、查询或片段;票据模式配置保持现有 URL 兼容性。

SaaS 客户端附加连接

将恰好一个已配置 Center 设置为 mode = "saas",以使用其托管内容后端,而不是并行的本地保险库:

[[centers]]
id = "corp"
url = "https://center.example.com"
public_key = "…"
mode = "saas"

将 MEMORIES_CENTER_TOKEN="…" 注入守护进程环境:这是托管成员、静态授权或带时限的访问 bearer,绝不能使用管理员令牌。使用该配置启动 memories daemon。守护进程通过核心 TLS/HTTP 客户端,将内容请求发送到该 Center 的 /v1/rpc,发送过程在本地写入锁之外。不支持的托管操作会拒绝执行;不会回退到本地存储。使用绑定的保险库名称(例如 default)和托管 RPC 返回的规范资产 ID。本地状态/Center 列表仍在本地处理。初始化、恢复、本地 MCP 签发和本地注册仍被禁止;旧本地数据库不是此附加连接的内容后端。内容和权限属于托管 Center 的授权/成员。其他仅提供票据的 Center 可以共存。

守护进程使用配置中的 URL(没有环境变量 URL 覆盖)。经过认证的守护进程客户端共享守护进程 bearer;应保护 IPC 访问以及任何守护进程监听/中继令牌,将其视为该成员权限的委派。本地 MCP 能力不会随守护进程 bearer 转发;应改为通过直接 HTTP/MCP 适配器使用单独限定范围的托管成员。不会添加 Center 管理员 MCP 工具。

直接 CLI HTTP 调用选择 URL 的顺序是 --center-url、MEMORIES_CENTER_URL,最后是配置中的 SaaS Center。管理员调用需要 MEMORIES_CENTER_ADMIN_TOKEN;内容 RPC 需要 MEMORIES_CENTER_TOKEN。这些 bearer 令牌均不应放入 TOML。TLS 设置复用 --ca-file / --insecure;不要在生产环境禁用 TLS 验证。不会跟随重定向。直接调用辅助工具不需要运行中的守护进程:

export MEMORIES_CENTER_URL="https://center.example.com"
export MEMORIES_CENTER_TOKEN="…"
# request.json contains an ordinary hosted Request, e.g. {"op":"list","vault":"default"}
memories center rpc --request request.json

CLI

# Admin: derive public key for config.toml
export MEMORIES_CENTER_SECRET="…"   # 32-byte URL-safe base64 or 64 hex
memories center public-key

# Admin: issue tickets (or use memories-center HTTP service)
memories center issue-bootstrap --center corp --device-name laptop
memories center issue-grant --center corp --vault VAULT_ID --minutes 15
memories center issue-grant --center corp --vault VAULT_ID --scope read --minutes 15
memories center issue-grant --center corp --vault VAULT_ID --scope write --minutes 15
memories center issue-grant --center corp --vault VAULT_ID --scope read_write --minutes 15
memories center issue-grant --center corp --vault VAULT_ID --scope 'read+write+delete' --minutes 15
memories center issue-grant --center corp --vault VAULT_ID --scope allow_all --minutes 15
# Legacy: --write alone ⇒ read_write (when --scope omitted)

# Endpoint (daemon already running with centers in config)
memories center list
memories center enroll --ticket "$BOOTSTRAP_TICKET"
memories sudo
memories center import-grant --ticket "$GRANT_TICKET"

本地(非 Center)MCP 授权接受相同的范围:

memories mcp grant VAULT_ID ./grant.token --scope read --minutes 15
memories mcp grant VAULT_ID ./grant.token --scope read_write_delete --minutes 15
# Legacy: --write ⇒ read_write

Center HTTP 服务(memories-center)

export MEMORIES_CENTER_SECRET="…"
export MEMORIES_CENTER_ADMIN_TOKEN="…"
memories-center --listen http://127.0.0.1:9191 --center-id corp

Center 通过 TCP 提供明文 HTTP。--listen https://HOST:PORT 仅选择绑定地址;它不会启用原生 TLS。公开服务时,将 Center 绑定到回环地址或受保护的内部地址,置于 TLS 反向代理之后,并为客户端配置代理的公共 https:// URL。保护管理员令牌和签名秘密;不要直接暴露明文 HTTP 监听器。

  • GET /health
  • GET /v1/public-key
  • POST /v1/tickets/bootstrap(Bearer 管理员令牌)
  • POST /v1/tickets/mcp-grant(Bearer 管理员令牌)
  • POST /v1/tickets/unlock(Bearer 管理员令牌;{"ttl":300},1–3600 秒)

授权正文(JSON):

{
  "vault": "…",
  "scope": "read_write",
  "minutes": 30,
  "ttl": 3600
}

scope 接受与 CLI 相同的传输名称和别名。省略 scope 时,write: true 映射到 read_write,否则默认为 read。静态托管授权可以省略范围(TOKEN 或 TOKEN::VAULT),成员创建在省略范围时也默认为 read。请显式请求 allow_all,不要依赖以前宽松的默认值。

完整托管保险库(SaaS)

使用 --data DIR(或 MEMORIES_CENTER_DATA)启用托管保险库。通过服务管理器受保护的环境/秘密注入方式提供非空的 MEMORIES_CENTER_VAULT_PASSWORD。这里有意不提供密码命令行标志。只有使用 --data 时才需要密码;丢失密码即失去 Store 访问权限。使用独立生成的强密码,不要使用 bearer 令牌或签名密钥。

# Inject MEMORIES_CENTER_SECRET, MEMORIES_CENTER_ADMIN_TOKEN,
# MEMORIES_CENTER_VAULT_PASSWORD and MEMORIES_CENTER_GRANTS securely first.
memories-center --listen http://127.0.0.1:9191 --center-id corp --data ./center-data

从源码检出目录运行时,在选项前加上 cargo run -p memories-center --。没有 --data 时,进程保持为向后兼容的仅提供票据的 Center。两种模式都支持票据和注册。

托管模式复用 memories_core::service::Service、Request、Content 和守护进程的加密 SQLite Store。它可以取代托管保险库内容所需的 Center + 守护进程,包括:

  • 完整记忆/卡片字段:正文、标签、来源、别名、备注、类别、私密标志、链接、允许的主机和秘密引用。
  • Brain、persona、workflow 和 workflow-template 卡片;受保护的 account、env、token、login、TOTP/HOTP 和 otpauth 配置档。
  • 已签名的修订 DAG、历史、差异、历史读取和重新应用;同一保险库内的卡片链接和图谱。
  • Persona 世界快照、滚动归档、归档卡片、事物/属性、聊天会话/读取游标、自动化钩子、允许列表和触发器。
  • 加密文件和凭据附件、受保护秘密替换、显式凭据交付和 OTP 生成。

存储与迁移

DIR/
  store/
    memories.db       # Same encrypted Store format as the daemon
    memories.lock     # Exclusive process lock
  memories.json.bak   # Only if imported from the old notes library

SQLite 还可能创建正常的事务日志文件。Store 保存是事务性的;保险库身份、已签名修订和审计状态一起持久化。同一时间由一个 Center 进程持有 Store。默认保险库以及配置或签发的授权所使用的命名保险库按需创建。授权绑定在内部使用规范保险库 ID,无法枚举/访问其他保险库。Unix 目录仅所有者可访问(0700);Store 文件和旧版备份也仅所有者可访问(0600)。在所有平台上保护目录和服务账户。

如果存在 DIR/memories.json,启动时会在一个原子的内容事务中导入其中的普通笔记,保留 ID 和保险库名称,然后将输入重命名为 memories.json.bak。无效记录会导致启动失败,不会丢弃源文件或发布不完整的笔记批次。重命名中断后重试会跳过相同的已导入记录;记录冲突或备份已存在时会拒绝继续,交由运营者检查,而不是覆盖数据。旧版备份仍是明文:在验证导入并安排备份后,安全删除该文件或单独加密。新写入的内容绝不会保存为 JSON。

加密与威胁模型

Store 在进程启动时使用运营者持有的密码解锁(Argon2 密码封装,采用与本地 Store 相同的认证加密)。每次获授权的内容操作中,托管分发都会在空闲过期检查前刷新服务器会话;不会在十五分钟无活动后自动锁定。关闭和管理员锁定会释放内存中的 Store 密钥与缓存内容。管理员锁定/解锁使用独立的管理员 HTTP 路由,绝不使用授权内容 RPC。不存在托管 sudo 或密码更改 RPC。

这是服务器端加密,不是客户端端到端加密。 控制主机/进程/密码的运营者可以读取所有托管保险库,包括私密内容和凭据。任何同时持有密码和磁盘副本的人都可以解密 Store。主机内存、崩溃转储、环境检查、恶意授权客户端和被攻破的 TLS 终止设施均不在此机密性边界内。private 仍控制安全摘要/检索,但无法阻止已获授权读取该保险库的托管授权。

守护进程则使用设备密码,也可能使用本地操作系统封存的无人值守密钥。托管模式没有封存的设备密钥,也没有逐设备密码交互。设备保险库密钥不会自动上传或共享给该托管 Store。使用上文所述的 TLS 反向代理;对 Center 和 TLS 终止器而言,HTTP 请求与获授权的秘密交付都是明文。不要记录 bearer 令牌、RPC 正文、凭据响应或密码。

远程锁定/解锁

托管路由需要管理员 bearer,且仅在启用 --data 时存在:

路由 行为
GET /v1/admin/status 返回 unlocked、托管模式、成员数量和 restart_required,不包含秘密
POST /v1/admin/lock 丢弃 Store 密钥/状态、清除缓存,并暂停授权直到解锁
POST /v1/admin/unlock 接受 {"password":"…"},使用运营者密码重新打开同一个现有 Store

锁定时,内容路由和托管访问令牌签发返回 503;任何授权都无法提供明文。管理员状态、成员列出/移除和设备票据签发仍可使用。成员创建/范围/保险库更新需要已解锁的 Store。错误的托管密码返回 403;认证失败使用核心指数退避(退避期间的后续尝试返回 429)。成功解锁会恢复静态/成员访问,不会恢复已撤销的签发令牌。锁定前已经完成的请求,其返回的明文无法收回。启动仍然需要 MEMORIES_CENTER_VAULT_PASSWORD;远程解锁不会持久保存提供的密码,也不会更改启动配置。

export MEMORIES_CENTER_ADMIN_TOKEN="…"
memories center status --center-url https://center.example.com
memories center lock --center-url https://center.example.com
memories center unlock --center-url https://center.example.com  # password prompt

设备解锁有所不同: POST /v1/tickets/unlock 在仅提供票据和托管 Center 中均可使用。管理员签名一个短期 unlock 票据;本地守护进程验证其类型、签名、配置中的 Center 和有效期,然后使用本地提示输入的运营者设备密码解锁。它不授予 sudo/MCP 提权。memories center unlock --ticket 始终使用本地、经过操作系统认证的 IPC,而不是所选的远程守护进程或 Center HTTP API。没有自动设备密码交付、托管或推送通道。

# Admin: save the returned ticket securely; do not put it in logs.
memories center issue-unlock --center-url https://center.example.com --ttl 300
# On the device (daemon running with the same --home/config):
memories center unlock --ticket "…"  # local device password prompt

解锁票据在过期前可重复使用,授权信任该 Center 的端点解锁,但仍受每个端点密码约束;它们不是一次性票据,也不绑定设备。仅使用票据的受管设备仍可使用现有本地密码解锁。SaaS 附加连接不能使用票据注册或解锁并行的本地内容 Store。

密码保管: 托管运营者持有托管 Store 密码,仅在托管解锁时将其发送给可信 Center/TLS 终止器。票据模式下,设备密码和保险库密钥留在设备/本地守护进程。被攻破的 Center 可以签发解锁票据,但仍需设备密码才能解锁其密文。被攻破的托管管理员可以锁定 Store,并在 Store 解锁时签发拥有明文访问权限的成员。托管运营者本就控制进程,可读取全部已解锁明文;管理员/成员分离不是针对运营者的加密保护。

持久成员

托管成员是有名称的持久 bearer 主体,具有相同的五级 GrantScope,并非身份提供商账户。可选的 role 为 reader、editor、admin 或 owner;省略角色时,分别保留从 read、write/read_write、read_write_delete 和 allow_all 映射的旧行为。显式角色限制成员范围上限(Reader:read,Editor:read_write,Admin:read_write_delete,Owner:allow_all)。成员 CRUD 路由仍需运营者管理员 bearer,而不是成员的 Admin/Owner 角色:

路由 正文 / 响应
GET /v1/admin/members 包含 id、vault、scope、role、created 和 updated 的 members 数组;没有 bearer 令牌
POST /v1/admin/members {"id":"agent","vault":"default","scope":"read"};元数据及生成的 token,仅在此响应中显示
PUT /v1/admin/members/{id} 可选的 scope、vault 和/或 role;使用同一个 bearer,响应仅含元数据
DELETE /v1/admin/members/{id} 移除成员并立即使 bearer 失效

ID 是稳定的 1–64 字符名称(ASCII 字母/数字、-、_);center-admin 保留给运营者 Owner。重复 ID 返回 409。令牌随机、唯一,且不同于管理员/静态令牌。创建成员时使用受保护的终端/输出目标;CLI 按设计仅打印一次返回的 bearer。切勿将创建响应记录到日志。成员通过同一授权表在 /v1/memories 和 /v1/rpc 上认证;范围/保险库更改无需重启即可作用于后续请求。在成员资格变更前已通过认证的请求,会在内容分发前被拒绝。移除/重新创建成员就是令牌轮换流程。

DIR/members.json 是位于仅所有者可访问的托管目录下、原子替换且仅所有者可访问(Unix 上为 0600)的持久文件。它包含明文 bearer 令牌,但不包含密码、密钥或保险库内容;应像凭据一样保护它、纳入受保护备份,且不要提交到版本库。启动会重新加载/验证成员,遇到损坏或冲突的 bearer 时拒绝继续。持久成员保存失败会锁定内容并设置 restart_required:远程解锁返回 503,直到运营者修复文件系统并重启以重新加载持久授权。这可防止不确定的持久化结果使已撤销的 bearer 重新生效。

memories center member list --center-url https://center.example.com
memories center member add --center-url https://center.example.com --id agent --vault default --scope read
memories center member set --center-url https://center.example.com --id agent --scope read_write
memories center member remove --center-url https://center.example.com --id agent

这仍是一个由单一运营者持有密钥、通过逻辑方式按保险库绑定授权的 Store,不提供严格的多租户隔离、OIDC/SSO、用户密码、刷新令牌、配额或自助恢复。

授权与 API

静态授权使用可重复的 --grant TOKEN[:SCOPE[:VAULT]] 和/或逗号分隔的 MEMORIES_CENTER_GRANTS;省略范围表示 read,省略保险库表示 default(TOKEN::VAULT 以默认范围选择保险库)。其生命周期为进程生命周期。管理员认证的 POST /v1/tickets/mcp-grant 返回未改变的签名票据,以及绑定到其保险库/范围的托管 access_token。该访问令牌在 minutes(1–60)后过期,并可跨托管重启保留。票据的 ttl 单独控制票据导入有效期。托管签发授权原子持久化到仅所有者可访问(0600)的 DIR/grants.json:仅保存 SHA-256 bearer 哈希和授权元数据,从不保存签发令牌的明文。Bearer 哈希以恒定时间比较。撤销持久保留,成员移除在重启后仍沿恢复的授权祖先关系级联。过期授权及其后代在加载时、每分钟和认证/签发时清除。此文件及其备份由所有者控制,应视为安全敏感文件;回滚该文件或 Store 可能恢复旧状态。持久化失败会锁定服务,需要修复/重启。

锁定托管 Store 会丢弃其密钥,使授权无法使用,但不会撤销授权;未过期的签发授权、静态授权和持久成员在解锁后恢复。每个持久成员都有持久化的代际计数器。签发者和接收者代际在签发时固定,认证时绝不刷新;公开的 generation 字段是接收者的签发时代际。范围/角色/保险库更新或移除只增加该成员的代际;被移除成员的计数器会保留,因此重新创建 ID 不会重置它。移除/更新会显式撤销由该成员签发或签发给该成员的授权,以及所有传递后代;不相关的授权仍可使用,重启后也一样。静态/持久凭据在显式策略变更时重新建立,而不是在认证时建立。静态主体和持久成员父 ID 在重启间保持稳定,因此恢复的委派链仍然连通。

仅使用管理员 bearer 会收到 401,即使是读取 /v1/memories 和 /v1/rpc。它不能直接修改保险库内容。管理员仍可通过票据或授权 API 签发独立授权;这种管理能力不构成针对管理员的机密性屏障。

授权 HTTP API

这些路由仅存在于托管模式。它们认证运营者管理员 bearer(跨全部保险库的 Owner)或托管主体的 bearer。运营者控制服务器锁定/解锁/成员 CRUD;托管 Admin/Owner 只能在 bearer 的保险库和范围内管理授权。不需要终端提权。Reader 和 Editor 不能签发或委派,即使被授予更广泛的内容访问权限。角色/范围/保险库/有效期违规返回 403 和明确的权限错误;处理器验证失败返回 400,对象缺失返回 404,重复决定返回 409,过期/撤销/代际过时的 bearer 返回 401。格式错误或类型不正确的 JSON 可能被 Axum JSON 提取器以 400 或 422 拒绝。

路由 字段和响应
GET /v1/access/{vault} Admin/Owner;vault、按 owner、admin、editor、reader 分组的 principals、包含有效期的活动 grants,以及 pending_requests 和 approved_unclaimed_requests(id、requester、scope、approvers 列表或 "broadcast"、以 Unix 秒表示的 created 和 expires)
POST /v1/grants Admin/Owner;vault、可选 scope(默认 read)、duration_minutes(默认 30,别名 minutes)、grantee;授权元数据及一次性 token
GET /v1/grants 可选 vault;Admin/Owner 可见保险库内全部签发授权,其他当前主体只能看到自己签发的授权;grants 数组,不包含静态/持久凭据和过期/失效授权
DELETE /v1/grants/{id} 签发者或保险库内的 Admin/Owner;撤销签发授权及后代,返回 revoked 和 count
POST /v1/requests 任何成员;vault、可选 scope(默认 read)、reason、duration_minutes(默认 30,别名 duration)、approvers;待处理请求元数据
GET /v1/requests 可选 vault;属于请求者或合格审批者的待处理请求,以及请求者已批准但未领取的请求;不包含令牌
POST /v1/requests/{id}/approve 合格的 Admin/Owner;无正文;仅返回已批准但未领取的 request 元数据和经过验证的边界,不包含授权或令牌
POST /v1/requests/{id}/claim 仅请求者的持久成员 bearer;无正文;request 及新签发的 grant,含一次性 token
POST /v1/requests/{id}/reject 合格的 Admin/Owner;可选 JSON reason;状态为 rejected 的 request
POST /v1/requests/{id}/cancel 仅请求者;无正文;状态为 cancelled 的 request
GET /v1/audit Admin/Owner;可选 vault、format=json|jsonl(默认 json);经过验证的 entries/verified,或以换行分隔的导出

所有签发/请求时长均为 1–60 分钟。同时最多可有 10,000 个活动授权(memories_core::service::MAX_ACTIVE_GRANTS);本地守护进程使用相同上限。签发者不得超出角色上限或 bearer 范围、离开其保险库或延长父授权有效期。接收者默认为签发的成员;显式 grantee 名称必须是所选保险库的现有主体。保留的 center-admin 接收者始终被 HTTP 403 拒绝;此前持久保存且使用该主体的授权也无法使用。运营者未指定接收者的签发(包括托管票据签发)创建一个匿名主体,其主体值为授权 ID,而不是 center-admin。接收者的角色不得高于签发者的有效角色,即使是只读授权也如此。

Center 和守护进程均定义 pub const MAX_DELEGATION_DEPTH: u32 = 2。直接成员/运营者签发的深度为 1;使用授权签发时深度为 parent.depth + 1,深度 3 返回 HTTP 403。持久成员凭据是直接成员权限(内部深度 0);它们签发的授权深度为 1,没有授权父节点。缺少 depth 的持久化旧授权会根据完整父链重建,包括配置的静态和持久凭据。祖先缺失、成环或超出限制的授权无法使用,并在启动时清除。

满足 subject != issued_by 的授权权限属于指定的授权调用者。此类调用者不能批准、拒绝、取消或领取任何权限请求,即使是自己的请求(HTTP 403);它们也不是合格审批者。它们可以创建请求,但必须使用真实成员凭据处理权限流程。委派签发保留真实签发者,因此指定 bearer 无法将自己洗成自行签发的授权。自行签发的授权保留现有请求规则,领取仍需请求者的持久成员 bearer。使用指定接收者授权作出的旧版延迟批准无法授权领取;需要获得合格成员权限的新批准。

破坏性变更:角色高于签发者的接收者、center-admin 接收者、指定接收者的权限决定/取消/领取,以及深于两层授权的委派均被拒绝。运营者签发的主体现在是匿名主体,而不是 center-admin;成员签发的根授权不再将持久成员凭据列为委派父节点。应重新签发合适的根授权或使用成员凭据,不要依赖这些旧行为。

授权记录 id(独立的不透明 ID)、subject、scope、vault、以 Unix 秒表示的 expiry、parent、issued_by、generation、depth 和 role。静态主体 ID 为 static:OPAQUE_ID。GET /v1/grants 仅返回已签发的带时限授权,包含 id、parent、issued_by、grantee、scope、vault、以 Unix 秒表示的 expiry、generation、depth,以及可用时以 Unix 秒表示的 created。它绝不返回令牌或 bearer 哈希。 grantee 是接收者(签发/访问响应中的 subject)。代际保持为签发时的代际。此已签发授权视图与未改变的 GET /v1/access/{vault} 主体/访问视图不同:静态和持久凭据只出现在后者中,通过配置/成员 CRUD 移除,而不是 DELETE /v1/grants/{id}。签发者可列出自己签发的授权;仅收到授权并不会让非管理员接收者能够看到它。托管 bearer 被限制在其保险库内;省略 vault 会选择该保险库,而运营者可跨保险库列出。 memories center grant list [VAULT] 返回同样的已签发授权响应。

approvers 为空/省略的请求会通过合格的待处理列表广播;非空列表包含具名的 Admin/Owner 审批者 ID(或 center-admin)。批准和拒绝均受审批者范围、保险库、角色和剩余授权寿命限制。请求者不能批准或拒绝自己的请求,即使其是 Admin/Owner 或具名审批者;仍可取消自己的请求。决定在同一个授权锁下串行执行,且只能发生一次。请求状态为 pending、approved、rejected 或 cancelled。批准会记录已批准的授权参数以及审批者的范围/保险库/角色/有效期边界,但不签发任何内容,也不返回令牌。请求成员使用其持久成员凭据调用 POST /v1/requests/{id}/claim 或 memories center request claim ID。审批者、其他成员以及同一请求者主体的签发授权均被拒绝。领取串行执行:只有一次领取成功,后续尝试返回 409,重启后也如此。授权的代际和创建/有效期时钟从领取时开始;有效期受记录的批准权限有效期限制。父节点过期/撤销,以及签发者/请求者代际变化都会阻止领取。只有令牌哈希随授权元数据持久保存。request.claim 审计请求者操作主体和授权 ID,绝不记录令牌。如果一次性响应丢失,请重新请求批准。已批准的内容授权不会更改请求者的成员角色或委派权限。

破坏性变更:批准响应不再包含 grant 或其令牌;客户端必须使用请求者成员 bearer 显式领取。请求列表显示已批准但未领取的元数据,而不是令牌。缺少延迟批准边界的旧版已批准请求无法领取;应创建新请求。

Center 审计与请求文件

DIR/requests.json 以 0600 模式原子替换,跨重启保存请求元数据和已完成的决定。DIR/audit.jsonl 以 0600 模式追加打开,每个条目之后执行 fsync,Center 从不重写它。两者都位于模式为 0700 的托管目录中。原因属于审计元数据:不要在其中放入密码或其他秘密。审计记录不包含 bearer 令牌、密码、密钥或保险库内容。

审计条目公开 sequence、timestamp、prev_hash、hash、action,以及通过授权进行的操作的可选 issued_by / via_grant。前者是真实签发者,后者是调用者的授权 ID;actor 仍是授权主体,但 grant.issue 将真实签发者记录为 actor,将接收者记录为 subject。这些字段默认不存在,不存在时省略,因此旧审计日志保留现有哈希。共享 Store 的已签名托管内容审计也记录这些字段。现有字段包括 actor、subject、scope、vault、expiry、parent、reason、request_id 和 object。第一个 prev_hash 为 null;后续值引用前一个哈希。SHA-256 覆盖 JSON 元组 (sequence, timestamp, prev_hash, event_fields)。crates/center/src/saas/audit.rs 中的 verify_audit_chain(path) 验证连续性、哈希和完整 JSONL 行。启动会验证文件,并根据它核对请求决定与领取,因此在审计决定或领取后、替换请求元数据前崩溃,不会允许重复操作。追加和读取对照当前进程内链头验证链;审计读取在应用保险库过滤前验证整条链。

操作包括 device.authorize(初始化票据签发)、grant.issue、grant.revoke、member.add、member.set、member.remove、session.lock、session.unlock、request.create、request.approve、request.claim、request.reject 和 request.cancel。锁定和成员变更撤销也会审计。JSONL 导出使用 Content-Type: application/x-ndjson;过滤后的导出保留原始哈希/序号,不是独立的完整链。绑定保险库的 Admin/Owner 调用者只能看到其保险库条目;运营者还可看到全局事件,并可导出完整日志。任何审计追加/请求持久化失败都会锁定服务、设置 restart_required,并禁用远程解锁,直到修复和重启。设置 restart_required 时,授权变更和权限决定返回 503;运营者只读检查仍可使用。将这些文件保存在受保护的备份中,不要纳入源代码管理。

这是一条无密钥链,不是设备签名或外部锚点:被攻破的运营者可以重写/重新计算它、省略事件,或一起回滚全部文件。没有外部已知链头,就无法检测启动前的完整尾部截断。不要将链验证视为独立安全审计。

托管内容范围与 API

托管内容范围作用于操作,而不仅仅作用于 HTTP 方法。授权管理路由还会按上文所述检查主体角色;单独的内容 scope 绝不授予主机管理权限:

范围 绑定保险库内的托管权限
read 安全列出/搜索/图谱;完整的授权读取、文件读取、历史/差异/审计及显式 credential_copy
write 内容创建/更新、受保护/OTP 注册、文件/凭据、世界/聊天/自动化写入;不允许读取已有内容或删除
read_write 两者兼有;还包括 otp_code(需要读取 + 写入,因为 HOTP 会推进状态)
read_write_delete 两者兼有,加上资产、事物、属性和自动化钩子删除
allow_all 仅内容的托管接口内的全部上述权限,不包括服务器/设备管理

与守护进程 MCP 不同,托管分发仅在封闭的操作允许列表、范围和保险库绑定检查通过后提供操作内提权。守护进程 check_mcp 及其本地提权限制保持不变。托管代理/MCP 适配器可在完整 RPC 内容接口上使用其限定范围的 bearer;无需本地终端提权或守护进程连接。跨保险库 brain/archive 引用不能通过授权解析另一保险库的内容。

  • GET /v1/session → 当前保险库、角色、范围、有效期和读取/写入/删除能力,不包含 bearer 材料。仅写入令牌可查看其会话;运营者管理员令牌不能。参见移动端托管模式。
  • GET /v1/memories → {"memories":[...]}。普通非私密笔记记录保留 id、vault、title、body;卡片/私密/文件/凭据记录使用安全摘要,绝不包含凭据正文或解析后的秘密。
  • GET /v1/memories/{id} → 完整的安全客户端内容 + id、vault。凭据需要显式 credential_copy,而非通用读取。
  • POST /v1/memories 和 PUT /v1/memories/{id} 接受 title/body 及完整 Content 元数据。PUT 保留省略的字段。创建时添加卡片元数据且未提供 kind,会推断为 kind: "card"。结构化/受保护配置档创建使用专用 RPC,而非通用 PUT。仅写入 PUT 只返回提交的字段和记录身份,不返回已有私密字段。
  • 可选 JSON vault / ?vault=… 必须匹配授权名称或规范 ID;省略时选择授权的保险库。DELETE /v1/memories/{id} 需要删除权限。
  • POST /v1/rpc 接受守护进程 Request JSON(op 使用 snake_case),返回其直接 JSON 结果,并独立检查每个操作。使用响应中的规范资产/修订 ID;vault 接受授权的名称或规范 ID。没有 vault 的 list 只选择授权的保险库。错误使用 HTTP 401(未知/管理员/过期令牌)、403(范围/接口/保险库)、404(缺失)、409(冲突)、400(无效输入)或 422(Request JSON/模式格式错误)。

示例(使用授权 bearer 发送,而非管理员 bearer):

{"op":"brain_card","vault":"default","title":"Research","abstract":"Summary","links":[]}
{"op":"history","vault":"default","asset":"CANONICAL_ASSET_ID"}
{"op":"persona_world_get","vault":"default","asset":"PERSONA_ASSET_ID"}
{"op":"put","vault":"default","content":{"title":"attachment.bin","kind":"file","private":true,"body":"BASE64URL_BYTES"}}

文件/凭据使用现有 URL 安全的 base64 正文约定,不使用第二个 blob 存储。核心的 16 MiB 内容正文限制仍适用;HTTP JSON 正文限制为 24 MiB。protect_secret 原子维护凭据引用。通用读取/列表/差异绝不解析存储的凭据字节;获授权的 credential_copy 是有意设计的显式明文交付端点。

托管 archive_history/archive_read 访问绑定的活动保险库的已签名修订(包括历史/墓碑修订),而不是守护进程恢复归档。archive_audit 访问该保险库的审计。托管 reapply 将已签名的历史或隔离修订追加为新链头,包括结构化/受保护元数据;重新应用墓碑还需要删除权限。与通过通用 PUT 凭空构造受保护引用不同,这会恢复保险库签名历史中已经认证的引用。它不会自动恢复已删除的关联凭据;如有需要,单独重新应用该凭据修订。

与守护进程的刻意区别

  • POST /v1/bootstrap 和 /v1/restore 始终返回 403:这仍是 Center 管理的内容库,不是设备自助注册/恢复。
  • 不提供 sudo、托管内容内的设备注册、保险库策略成员管理、授权签发/撤销内容 RPC、全存储备份/恢复 RPC、远程会话/代理、云/中继同步、无人值守保管、执行或加密作业执行。即使使用 allow_all,此类请求也会被拒绝。
  • 托管锁定/解锁/状态和持久成员管理是独立的管理员授权 HTTP 路由,不是 allow_all 授予的能力。
  • 不暴露恢复归档和跨保险库内容解析。支持托管保险库自身修订的历史读取/重新应用。
  • 语义/RAG 模型配置/生成及模型安装不是托管端点。词法 search 和同步 retrieve 路径复用核心检索策略(不包含私密内容、文件正文或凭据正文)。
  • 自动化通过显式 RPC 和现有内容触发事件运行;没有后台调度器。出站 webhook 使用核心的精确 URL 允许列表;将可以配置 webhook 的托管写入者视为已获授权的出站网络访问,并针对不受信任租户执行运营者出站限制。
  • 配置 SaaS 的守护进程和轻量 CLI 辅助工具使用托管 /v1/rpc;原生移动端使用独立的 hosted_* FFI 路径,具有更小的内容允许列表。仅票据的本地 IPC 保持完整。没有 JSON-RPC MCP 管理员工具服务器。

Store 持久化仍使用全状态事务保存和串行服务操作;这不意味着独立租户密钥加密,也不声称具有生产多租户隔离/安全认证。

移动端 / FFI

发布的 Flutter 应用在自身界面中选择独立模式、Center 或 SaaS。它不加载守护进程的 config.toml,也不配置由票据管理的本地服务。其 Center 选项连接到具有托管存储的自托管服务器,SaaS 则连接到 https://memories.ivjn.us。两者均使用下文的托管 FFI 操作。

调用者在共享服务上配置 Center 时,该服务会执行票据管理的 Init/Restore 和授权限制。这与手机的模式选择器是独立的。移动 FFI 继续拒绝 Run/CryptoJob/本地 MCP 授权管理/远程/代理,以及本地授权/权限请求 RPC;选择托管模式不会扩大该本地接口。

后续工作

远程指令批准和其他提权策略接口未来可能接受 Center 签名的批准票据。在此之前,Center 管理的端点仍使用本地 sudo 进行保险库提权操作,MCP 授权签发和初始化除外。allow_all 预留给这些接口扩展使用,无需重命名范围。

权限请求限制、过期与保留

同时适用于守护进程(请求保存在加密 Store 中)和 Center(requests.json):

  • MAX_PENDING_PER_REQUESTER_PER_VAULT = 20 和 MAX_PENDING_PER_VAULT = 200 仅计算待处理请求。已决定、已取消、已过期及已批准但未领取的请求不计入这些限制。(这取代了旧的 1000 个请求全局上限,旧上限计算历史上所有请求。)超出限制时,守护进程返回限制错误,Center 返回 HTTP 429。
  • PENDING_REQUEST_TTL = 7 days:超过此时间的待处理请求转为 expired 状态。其审计操作为 request.expire,操作主体为 system,之后无法再批准。已批准的范围请求若在批准后的 7 天内未领取,也会以相同方式过期,过期后无法领取。
  • REQUEST_RETENTION = 30 days:已拒绝、已取消、已过期或已领取请求,在最后一次状态转换(decided 时间戳,缺少该字段的旧记录使用 created)后的这段时间被删除。
  • 过期和清理采用惰性执行:守护进程在任何授权请求开始时执行,Center 在请求创建、列出、决定和领取时执行。启动时不会执行。
  • Center 启动核对会忽略已清理请求的审计条目,并接受 request.expire 条目。expired 请求必须具有过期审计。请求 ID 是永不复用的随机 128 位值,因此已清理请求(包括已领取请求)不再存在,也不能领取。因此,Center 不维护独立的墓碑集合。

移动端托管模式(手机客户端)

移动应用的 Center 和 SaaS 模式直接连接到使用 --data DIR 运行的 Center;手机不需要守护进程的 mode = "saas" 条目。首次连接和切换参见移动端用法,配置档迁移参见配置。FFI memories_call API 接受这些 hosted_* 操作:

  • {"op":"hosted_connect","center_url":"https://…","bearer":"…","ca_file":null,"insecure":false} → {"session":N,"url":…,"server_readable":true}
  • {"op":"hosted_rpc","session":N,"request":{…}} 使用下文的内容允许列表
  • {"op":"hosted_status","session":N} 通过 GET /v1/session 认证
  • {"op":"hosted_requests","session":N}、hosted_approve / hosted_reject(id)、hosted_access(vault)
  • {"op":"hosted_disconnect","session":N};memories_lock_all 也会丢弃所有托管会话

crates/core/src/center.rs 中的 HostedClient::rpc 允许 list、read、search、delete、type_list、type_show、type_create、type_update、type_delete、card_create、card_fields、card_field、credential_set、credential_show,以及 content.kind 为 memory、card 或 credential 时的 put。服务器仍会应用成员的保险库、角色和范围。此 FFI 允许列表不保证每个操作都有 Flutter 界面:托管知识支持列表、服务器搜索、详情、普通记忆/卡片编辑、创建和确认删除。结构化配置档、受保护卡片、类型模板和封存卡片需要专用编辑器。授权请求和已配对设备控制仍可使用。

GET /v1/session 返回 vault、vault_id、scope、role、expires_at(Unix 秒或 null)、can_read、can_write、can_delete 和 server_readable。它接受任何活动的限定范围内容令牌,包括仅写入令牌,但拒绝运营者管理员令牌。它不返回 bearer 或秘密材料。每个内容请求仍会重新检查授权;该响应是 UI 快照,不是授权。升级应用前先部署此端点:缺少它的旧服务器无法验证新应用连接。

搜索使用共享检索服务,排除私密内容、凭据和文件。未过滤的知识列表可以包含授权读者可见的私密卡片。保存失败时保留草稿;删除需要确认,且仅在服务器成功后才移除条目。更新使用现有修订服务,没有原子的预期修订前置条件;同时编辑可能覆盖彼此的最新字段。

托管模式下服务器可以读取内容: Store 静态加密,但运营者持有密码,可以读取托管内容。应用将其标记为“服务器可读取”。Rust 将每个活动客户端的 bearer 保存在内存中,在 Debug 和错误中隐去;Flutter 将每种模式的 URL/令牌组合持久保存在平台安全存储中。切换会结束活动会话,同时保留配置档。断开连接会移除该配置档保存的 bearer;不会撤销服务器令牌。应用进入后台会丢弃托管会话和可见内容;返回时需要显式重新连接。

设备间端到端秘密(托管 Center)

托管保险库对服务器可读。对于 Center 绝不能看到的秘密,应配对设备并交付封存信封(crypto::Envelope:X25519 + 由发送者的 Ed25519 密钥签名)。Center 只存储公钥和密文。

配对流程

  1. CLI:memories device hosted init --backend <keychain|secret-service|dpapi|owner-file> 创建设备身份(32 字节种子由操作系统保管;派生 X25519 + Ed25519)。
  2. CLI(成员令牌位于 MEMORIES_CENTER_TOKEN):memories device hosted pair --vault V → 携带两种公钥和自签名调用 POST /v1/devices/pairings。Center 创建携带设备指纹的待处理 PermissionRequest,并返回 {code, request_id, qr: {pairing_id, fingerprint, center}}(center 是 Center 的 --center-id)。配对在 5 分钟后过期。CLI 在终端(stderr)打印 {"type":"memories-pair","v":1,"pairing_id","fingerprint","center","code"} 的二维码——包含一次性代码,因此扫描即可;stdout JSON 保持不变。重新配对已活动的密钥返回 409;撤销后同一密钥可再次配对。
  3. 手机(保险库管理员)扫描二维码(或输入代码)→ GET /v1/devices/pairings/{code}。手机拒绝非 Memories 配对二维码、center 与已连接 Center 不同的二维码,或指纹与获取设备不同的二维码。未知、过期或已使用的代码均返回 404。代码只能使用一次且会过期,每分钟 10 次失败查询 → 429。界面显示设备指纹。
  4. 手机通过 POST /v1/requests/{id}/approve 批准,正文为 {"attestation": DeviceAttestation},由手机的 Ed25519 密钥对设备指纹、两种设备密钥、配对 id、审批者、范围和有效期签名。
  5. CLI:memories device hosted claim --request-id ID(一次)。设备限定的授权令牌保存在操作系统保管区;Center 只保存其哈希。
  6. 比较 SAS:手机在批准前显示它(按 123 456 分组);批准后,memories device hosted sas --pairing-id P 在 CLI 打印相同数字。数字不同:撤销设备并重新配对。

移动端权限:Android CAMERA(摄像头功能可选),iOS NSCameraUsageDescription。如果摄像头访问被拒绝,手机会说明如何在系统设置中启用,并提供手动代码输入。CLI 和手机将 Center 状态转换为可执行的提示(404 已过期/已使用/其他 Center,409 已完成或已配对,403 不允许或设备已撤销,422 拒绝端到端内容,429 速率受限)。

SAS = u64_be(SHA-256("memories/device-sas/v1\0" ‖ phone_ed25519_pub ‖ "\0" ‖ cli_ed25519_pub ‖ "\0" ‖ pairing_id)[0..8]) mod 10^6 的前 6 位十进制数字,补足前导零。手机从获取操作获得 CLI 密钥;CLI 从证明获得手机密钥。不匹配意味着密钥被替换:应立即撤销。

规则

  • 设备授权绑定设备(未撤销、已证明)及成员代际;委派深度 ≤ 2。设备不能签发授权或决定请求。
  • 增加成员代际或移除成员会使设备失效,并删除其信封。
  • PUT|GET|DELETE /v1/sealed/{vault}/{card}/{device}、GET /v1/sealed?device=:仅允许已证明设备的信封,发送到设备的 X25519 密钥,来自已知签名者;版本必须递增。审计只记录指纹。
  • 带有 e2e: true 的卡片在托管 Center 上拒绝明文秘密内容。
  • /v1/access/{vault} 列出设备。MCP:device_list、sealed_reveal(需要 intent: "reveal_plaintext" 和原因)。

威胁模型

Center 运营者无法读取封存秘密,但可以扣留或重放旧信封(版本按设备单调递增),并能看到元数据(谁、哪张卡片、何时)。配对信任 SAS 比较和审批者的手机密钥。撤销无法收回已经交付给设备的明文:撤销后,在上游轮换秘密。

端到端加固(P2)

  • 持久保持的 e2e 标志。 卡片一旦为 e2e: true,移除该标志的写入会被拒绝(422)。只有 Owner 授权(不能是设备授权)可以清除它,且写入不能携带任何秘密材料;此后的写入按正常规则处理。
  • OTP 参数。 封存的 OTP 秘密可以是纯 Base32 种子(SHA1、6 位、30 秒),也可以是完整 otpauth://totp/... URI;memories device hosted sealed-get --otp 遵循 URI 中的 algorithm(SHA1/SHA256/SHA512)、digits(6/8)和 period(30/60)。device hosted pair 也会将配对载荷打印为终端二维码(stderr)。
  • 撤销后重新封存。 撤销(或使设备失效)会删除其信封,并将每张受影响卡片标记为 needs_rotation(显示在 GET /v1/sealed?device=、GET /v1/sealed/rotation?vault=、访问列表和手机的重新封存页面中)。撤销无法收回已经交付的明文:先在上游轮换秘密,然后使用手机的为所有设备重新封存(hosted_reseal_all),以新版本为每个当前获批设备封存,并清除该标志。绝不包含已撤销设备。
  • 手机恢复。 Center 从不持有手机身份。创建时,手机会显示一次性恢复代码(32 字节种子 + 2 字节校验和的 Base32,每四字符分组),供用户记录;从恢复代码恢复(hosted_phone_restore)重建相同身份。
  • 身份轮换。 迁移到新手机密钥(hosted_identity_rotate)创建新身份,并向 POST /v1/devices/identity-rotations 提交由旧密钥和新密钥共同签名的记录;只有管理员成员(不能是设备)可以提交,且旧密钥必须已被某个设备信任。CLI 固定首次批准的手机密钥(hosted-device-trust.json,首次使用时信任),只接受可通过已验证轮换从该根到达的信封签名者。
  • 硬件支持的存储。 手机种子使用 flutter_secure_storage 及最强的内置选项保存:iOS/macOS Keychain 的 passcode 可访问性(仅本设备、不同步、需要设备密码);Android Keystore 封装存储,使用生物识别或设备凭据门控,且设置 resetOnError: false。限制:种子仍会导出到应用内存用于签名/封存(Secure Enclave / StrongBox 无法原生执行 X25519/Ed25519),因此它保护静态数据,无法抵御被攻破的运行中应用。
  • 透明日志。 Center 在 GET /v1/devices/log 保持仅追加、哈希链接的设备日志(device.pair、device.approve、identity.rotate、device.revoke)。每个条目嵌入 CLI 配对自签名、手机证明或轮换记录。memories device hosted log --verify 检查链链接、每个签名,以及日志是否延续上次验证的链头(hosted-device-log.json);历史被改写会报告错误。