Skip to content

feat(backup): add user-passphrase encryption - #170

Closed
squarepots wants to merge 35 commits into
Lampese:mainfrom
squarepots:feat/passphrase-backup-v2
Closed

squarepots wants to merge 35 commits into
Lampese:mainfrom
squarepots:feat/passphrase-backup-v2

Conversation

@squarepots

@squarepots squarepots commented Sep 18, 2026

Copy link
Copy Markdown

依赖 #169;本 PR 的完整导入事务复用该 PR 提供的 account-store mutation boundary。

本 PR 为 full backup 增加用户控制的 passphrase 加密,并让完整导入遵循共享的 account-store mutation boundary。

新导出始终使用 v2:每个文件生成独立的 16-byte random salt 和 24-byte random nonce,用 PBKDF2-HMAC-SHA256(210,000 次迭代)从用户 passphrase 派生 32-byte key,再使用 XChaCha20-Poly1305 做 authenticated encryption。v1 文件继续使用内置兼容路径只读导入,不能再导出。

passphrase 只存在于当前 command 和短暂的 React 遮罩输入状态中,不写入持久状态或日志。只拒绝空或全空白输入;其他 passphrase(包括首尾空格)按原样传给加密和解密。导入先完成解码和校验,再通过现有 mutate_accounts 在同一事务中读取最新 account store、合并并写回;文件导入和浏览器 bytes 导入使用同一条路径。完整导入只增加缺失 profile,保留最新 store 的有效 active_account_id,不会把备份中的历史 active id 变成新的运行时激活;无效的当前 projection 会被清除。并发回归测试确认导入不会覆盖事务开始前已提交的其他最新状态。

验证

  • pnpm run test:backup-passphrase:3 个测试通过
  • pnpm run test:web-boundary:1 个测试通过
  • pnpm build:通过
  • rustfmt --edition 2021 --check src/auth/storage.rs src/commands/account.rs:通过
  • cargo check --locked:通过
  • cargo test --locked:61 个 Rust 测试通过
  • git diff --check:通过

Closes #160


Depends on #169; full-import transactions in this PR reuse the account-store mutation boundary introduced there.

This PR adds user-controlled passphrase encryption for full backups and makes full imports use the shared account-store mutation boundary.

New exports always use v2: each file gets an independent 16-byte random salt and 24-byte random nonce, PBKDF2-HMAC-SHA256 derives a 32-byte key from the user passphrase with 210,000 iterations, and XChaCha20-Poly1305 provides authenticated encryption. Existing v1 files remain read-only import-compatible and are no longer exportable.

The passphrase exists only in the current command and transient masked React input state; it is never written to durable state or logs. Only empty or all-whitespace input is rejected. Every other passphrase, including leading or trailing whitespace, is passed unchanged to encryption and decryption. Import completes decoding and validation before using the existing mutate_accounts transaction to read the latest account store, merge, and write it back. File and browser byte imports use the same path. Full import adds only missing profiles, preserves a valid active_account_id from the latest store, never turns the backup’s historical active id into a new runtime activation, and clears an invalid current projection. A concurrent regression test confirms that an unrelated update committed before the import transaction is preserved.

Validation

  • pnpm run test:backup-passphrase: 3 tests passed
  • pnpm run test:web-boundary: 1 test passed
  • pnpm build: passed
  • rustfmt --edition 2021 --check src/auth/storage.rs src/commands/account.rs: passed
  • cargo check --locked: passed
  • cargo test --locked: 61 Rust tests passed
  • git diff --check: passed

Closes #160

@squarepots
squarepots marked this pull request as ready for review September 18, 2026 16:09
@squarepots
squarepots force-pushed the feat/passphrase-backup-v2 branch from f78a7b7 to aafdac3 Compare September 18, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Protect full backups with a user passphrase

1 participant