按主流密码管理器的标准方案设计:浏览器用 CSV 导入,Windows 凭据管理器走官方 API,导出分加密包和明文 CSV 两种,同步沿用现有的 vault 端到端加密同步。所有导入导出都在 crates/core 里实现,CLI 和手机端共用。
| 新增项 | 位置 | 说明 |
|---|---|---|
Profile::Login / CardInput::Login | cards.rs | 新的受保护卡片类型:urls(一个或多个站点)、username;密码存成独立凭据。现有 Account 是邮箱账号(SMTP),不适合网站登录。 |
Request::LoginImport | service.rs | 来源:浏览器 CSV 文本、加密导出包,或 CLI 组装好的条目(Windows 凭据)。一次提交,单次审计。 |
Request::LoginExport | service.rs | 格式:encrypted(推荐)或 csv(明文,兼容 Chrome / Edge / Firefox 导入)。 |
login_csv 模块 | core/src/login_csv.rs | 解析和生成 CSV。按表头识别 Chrome/Edge(name,url,username,password,note)和 Firefox(url,username,password,httpRealm,…)两种格式,严格按 RFC 4180 处理引号和换行。 |
memories login import|export | crates/cli | 新子命令。Windows 凭据管理器读取放在 CLI(windows-sys 增加 Win32_Security_Credentials)。 |
用户先在浏览器里自己导出密码 CSV,我们不读浏览器内部的密码库。Chrome 127 起启用了 App-Bound Encryption,第三方程序直接解密只能用窃密木马的手法。
flowchart TD A["用户在浏览器导出 CSV
Chrome / Edge:设置 → 密码 → 导出
Firefox:about:logins → 导出"] --> B["memories login import FILE
--from chrome|edge|firefox --vault V"] B --> C{"终端已 sudo?"} C -- 否 --> X1["拒绝:请先 memories sudo"] C -- 是 --> D["CLI 读取文件
大小上限 16 MiB"] D --> E["本地 IPC 发送 LoginImport
携带 CSV 文本"] E --> F{"服务端再次校验
sudo + vault 写权限
拒绝 MCP / 远程"} F -- 不通过 --> X2["拒绝并审计失败"] F -- 通过 --> G["login_csv 解析
按表头识别格式"] G --> H0{"表头可识别?
行数不超上限?"} H0 -- 否 --> X3["整体拒绝
不回显任何内容"] H0 -- 是 --> H{"逐行检查"} H -- 坏行 --> X4["记入失败列表
只给行号和原因"] H -- 好行 --> I["URL 原样保存
能解析成 http(s) 才取 host 作显示名"] I --> J{"重复检查
host + 用户名"} J --> K["一次提交:每条生成
login 卡片 + 凭据"] K --> L["审计 login.import
只记条数和来源格式"] L --> M["返回摘要:导入 N / 跳过 M
冲突 K / 失败行号"] M --> N["提醒用户删除 CSV 明文文件"] G -. note 列 .-> P{"note 列怎么处理"} class H0,H,J,P pending classDef pending stroke:#b45309,stroke-width:2px,stroke-dasharray:6 4
LoginImport 就行,不需要在 Flutter 里重复实现。cards::prepare 的校验:如果标题里出现了密码,就拒绝。android://…@com.package/ 这类安卓应用条目,Firefox 的 HTTP 认证条目只有 origin,都原样保存,不改写也不拒绝。只读当前用户的「通用凭据」(CRED_TYPE_GENERIC)。域凭据的密码系统不允许应用读取。「Web 凭据」存在 Windows Vault 里,只能通过未公开的接口访问,不做。
flowchart TD
A["memories login import --from windows --vault V"] --> B{"终端已 sudo?"}
B -- 否 --> X1["拒绝:请先 memories sudo"]
B -- 是 --> C["CredEnumerateW
当前用户,只取 GENERIC"]
C --> D["列出 target / 用户名 / 修改时间
不显示密码"]
D --> E{"用户选择要导入的条目"}
E --> F["读取 CredentialBlob
按 UTF-16LE 或 UTF-8 解码
解码失败的跳过并计数"]
F --> G["映射:target → 标题和 URL
UserName → 用户名"]
G --> H["组装条目,发送 LoginImport"]
H --> I["服务端:与流程 1 相同的
校验、重复检查、提交、审计"]
I --> J["CLI 清零本地缓冲
CredFree 释放系统内存"]
class E pending
classDef pending stroke:#b45309,stroke-width:2px,stroke-dasharray:6 4
git:https://github.com、MicrosoftAccount:…,不一定是网站密码。所以默认不全选,由用户按 target 过滤或交互勾选。加密导出是推荐方式,用于迁移到另一台 memories 或长期备份。明文 CSV 只用于导入回浏览器或其他密码管理器,会放宽现有的「密码不出库」边界,所以限制最严。
flowchart TD A["memories login export --vault V
--format encrypted|csv OUT"] --> B{"终端已 sudo?"} B -- 否 --> X1["拒绝"] B -- 是 --> C{"OUT 已存在?"} C -- 是 --> X2["拒绝覆盖"] C -- 否 --> D{"仅本设备 vault?"} D -- 是 --> X3["拒绝导出"] D -- 否 --> E{"格式"} E -- encrypted --> F["隐藏输入导出密码两次"] F --> G["服务端读取 login 卡片
Argon2id + XChaCha20-Poly1305 封装
AAD memories/logins/v1"] G --> H["审计 login.export.encrypted"] H --> I["CLI 写文件
仅所有者可读"] E -- csv --> J{"调用方白名单
caller 以 cli: 开头?"} J -- 否 --> X4["一律拒绝
MCP / 远程 / 手机 / 未知调用方"] J -- 是 --> K["警告明文风险
要求输入 EXPORT 确认"] K --> L["服务端先提交审计交付意图
login.export.plaintext"] L --> M["返回明文条目"] M --> N["CLI 写 Chrome 兼容 CSV
以 = + - @ 开头的单元格加前缀防公式注入
仅所有者可读"] class D,J pending classDef pending stroke:#b45309,stroke-width:2px,stroke-dasharray:6 4
credential_copy 的做法:先写审计交付意图,审计写入失败就不返回明文。LoginExport 加进 crates/ffi/src/lib.rs 的拦截列表,做双重保护。SECURITY.md。flowchart TD A["memories login import FILE
--from memories --vault V"] --> B{"终端已 sudo?"} B -- 否 --> X1["拒绝"] B -- 是 --> C["隐藏输入导出密码"] C --> D["服务端解封
校验版本和 AAD"] D --> E{"密码正确、内容完整?"} E -- 否 --> X2["统一报错:密码错误或文件已损坏"] E -- 是 --> F["与流程 1 相同的
重复检查、提交、审计"]
不新增同步通道。login 卡片和凭据是 vault 里的普通资产,跟着现有的 relay 端到端同步、OneDrive / Google Drive 云同步和设备配对一起走。
flowchart TD A["login 卡片写入 vault
签名修订"] --> B{"vault 类型"} B -- 仅本设备 --> X1["不同步、不配对、不导出"] B -- 共享 vault --> C["relay 端到端同步 / 云同步 / 设备配对"] C --> D{"对端客户端认识 login 类型?"} D -- 否 --> X2["对端解码失败
需要先升级"] D -- 是 --> E["对端可以查看元数据
sudo 后复制密码或导出"] class D pending classDef pending stroke:#b45309,stroke-width:2px,stroke-dasharray:6 4
Profile 和 CardInput 都标了 deny_unknown_fields。旧版客户端(包括旧版手机 App)收到 login 卡片会解码失败。
接受这个限制:版本号升一档,README 和发布说明写明「所有设备先升级,再同步 login 卡片」。
判断依据是「站点 origin + 用户名」。
判重键用 host(解析不出 host 时用原始 URL)+ 用户名,安卓条目和 HTTP 认证条目也能正确判重。
站点、用户名、密码都相同的跳过;站点和用户名相同但密码不同的,新建一张卡片并在摘要里列为冲突,默认不覆盖旧卡片。另外提供 --update,显式指定时才覆盖。
备注里可能写着安全问题答案之类的敏感信息,但卡片的 notes 是普通字段,会进全文索引。
默认不导入 note 列;加 --with-notes 才导入为普通 notes,并在导入前提示会被索引。
默认一条都不选。提供交互式勾选(只显示 target 和用户名),也可以用 --filter 按 target 匹配,--all 全部导入。
按白名单放行:只有调用方以 cli: 开头的本地 CLI 可以导出,要求 sudo、输入 EXPORT 确认,并写审计。其余调用方一律拒绝,包括 MCP、HTTP / relay 远程(ws:remote / relay:remote)、手机 FFI 和 Share,以及以后新增的调用方。
加密导出和明文导出都拒绝,和现有的 deny_local_export 保持一致。
现有的 backup 是整库备份,只能恢复成新的「(recovered)」vault,不能导入到已有的 vault。
新增 memories/logins/v1 格式:独立的导出密码,Argon2id + XChaCha20-Poly1305,只包含登录卡片,可以导入任意 vault。
不做。标准方案里没有这一项。要导入回浏览器,用明文 CSV 就够了。
第一期只做 core 和 CLI。手机端的 CSV 导入界面放到第二期,接口已经共用。Firefox 直接读 logins.json / key4.db 不做,统一走 CSV。
浏览器导出常有几百到上千行,难免夹着几行格式不对的。每行会生成卡片和凭据两个资产,而整个加密状态是整体重写的。按 docs/performance.md 的实测,1000 个资产时数据库约 9 MB,完整同步约 4 MB;relay 的完整同步包上限是 32 MiB。
尽量导入:好行照常导入,坏行跳过并按行号报告,不因个别坏行整体失败。单次导入上限 2000 行(约 4000 个资产),超过就整体拒绝,提示分批导入。