Skip to content

fix: separate vault key blob from root folder IPNS name - #349

Merged
FSM1 merged 9 commits into
mainfrom
fix/vault-key-ipns-separation
Mar 24, 2026
Merged

fix: separate vault key blob from root folder IPNS name#349
FSM1 merged 9 commits into
mainfrom
fix/vault-key-ipns-separation

Conversation

@FSM1

@FSM1 FSM1 commented Mar 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Bug: The vault IPNS name stored a v2 key blob on init, but updateFolderMetadata overwrote it with v1 JSON on any root folder operation, breaking login after the first folder modification
  • Fix: Derive a dedicated IPNS name for the vault key blob using HKDF info cipherbox-vault-key-ipns-v1 (distinct from root folder's cipherbox-vault-ipns-v1). Root folder uses standard v1 JSON like all other folders.
  • All folder metadata (including root) is now v1 JSON — v2 blob format is exclusively for the vault key blob at its own IPNS name

What changed

Component Change
@cipherbox/crypto Added deriveVaultKeyIpnsKeypair with dedicated HKDF context
useAuth.ts Login reads key from vault key IPNS; new user init publishes to both IPNS names
vault.rs (desktop) Same two-IPNS-name separation for init and fetch
fuse/mod.rs Removed encrypt_root_metadata_to_v2_blob — root uses v1 like all folders
fuse/decrypt.rs Removed v2 blob detection — all folders are v1
folder.service.ts Removed v2 blob detection from fetchAndDecryptMetadata
sdk-core Removed v2 blob detection from folder reader
recovery.html Derives vault key IPNS name (not root folder) for IPFS-direct recovery
hkdf.rs (Rust) Added derive_vault_key_ipns_keypair matching TypeScript

Test plan

  • 165/165 core tests pass
  • 50/50 API vault tests pass
  • Web builds cleanly (0 TypeScript errors)
  • Rust cargo check passes
  • sdk-core builds cleanly
  • SDK E2E tests (CI)
  • Staging deploy + manual login/folder-create/re-login cycle

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • Vault key blob is now published and resolved separately from root folder metadata: the key blob is published once at initialization and fetched on login, while root folder metadata is updated independently on changes.
    • Recovery and login flows now fetch the vault key and folder metadata via distinct addresses, reducing overwrite/conflict risks.
  • Documentation

    • Metadata schemas updated (v1.0 → v1.1) to reflect the separate vault-key blob and IPNS addressing.

FSM1 and others added 2 commits March 24, 2026 06:03
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3753c1b0a245
The vault IPNS name was serving double duty: storing the v2 key blob
on init, then being overwritten with v1 JSON on any root folder
operation. This broke login after the first folder modification.

Fix: derive a dedicated IPNS name for the vault key blob using HKDF
info "cipherbox-vault-key-ipns-v1" (distinct from the root folder's
"cipherbox-vault-ipns-v1"). The root folder now uses standard v1 JSON
like all other folders.

Changes:
- crypto: add deriveVaultKeyIpnsKeypair (TS + Rust)
- web useAuth: login reads key from vault key IPNS; new user init
  publishes to both vault key IPNS (v2 blob) and root folder IPNS (v1)
- desktop vault.rs: same two-IPNS-name init and fetch separation
- desktop fuse: remove encrypt_root_metadata_to_v2_blob, root folder
  uses encrypt_metadata_to_json like all subfolders
- folder.service.ts + sdk-core: remove v2 blob detection (all v1 now)
- recovery.html: derive vault key IPNS name for IPFS-direct recovery

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 22bb08999bbb
@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@FSM1 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 17 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aea23022-2e39-4773-a285-8a28daaa536b

📥 Commits

Reviewing files that changed from the base of the PR and between 2e59cb0 and da32c3a.

📒 Files selected for processing (3)
  • apps/desktop/src-tauri/src/commands/vault.rs
  • apps/web/src/hooks/useAuth.ts
  • packages/core/src/vault/blob.ts

Walkthrough

Splits the vault-key blob publication from root-folder metadata by introducing a dedicated HKDF-derived IPNS identity for the vault key blob (v2 key-only envelope containing the ECIES-wrapped rootFolderKey) while publishing root folder metadata as v1 JSON under a separate root-folder IPNS name. Documentation, crypto derivations, init/fetch flows, and blob formats updated accordingly.

Changes

Cohort / File(s) Summary
Planning & Docs
\.planning/todos/pending/2026-03-24-separate-vault-key-blob-from-root-folder-ipns-name.md, docs/METADATA_SCHEMAS.md
New TODO describing separation and metadata schema updates documenting VaultKeyBlob (v2 key-only), new HKDF/IPNS derivation, and removal of server-stored EncryptedVaultKeys.
Desktop: vault init & fetch
apps/desktop/src-tauri/src/commands/vault.rs
Init/fetch split into two HKDF-derived IPNS identities: a vault-key IPNS for the v2 key-only blob and a root-folder IPNS for v1 JSON metadata; backend /vault/init deferred until both publishes complete; logging/messages updated.
Desktop: crypto HKDF & blob
apps/desktop/src-tauri/src/crypto/hkdf.rs, apps/desktop/src-tauri/src/crypto/vault_blob.rs
Added VAULT_KEY_HKDF_INFO and derive_vault_key_ipns_keypair; changed v2 vault blob format and serialize/deserialize signatures to be key-only.
Desktop: FUSE metadata handling
apps/desktop/src-tauri/src/fuse/decrypt.rs, apps/desktop/src-tauri/src/fuse/mod.rs
Removed v2 metadata extraction and encrypt_root_metadata_to_v2_blob; FUSE now encrypts/parses all folder metadata exclusively as v1 JSON.
Web: auth & recovery flow
apps/web/src/hooks/useAuth.ts, apps/web/public/recovery.html
Introduced deriveVaultKeyIpnsKeypair use; split new-vault initialization into vault-key publish then root publish; recovery expects key-only v2 blob for rootFolderKey and v1 JSON metadata for traversal.
Web: folder metadata fetch
apps/web/src/services/folder.service.ts
Removed detectBlobVersion/deserializeVaultBlobV2 usage; fetchAndDecryptMetadata always treats fetched IPFS bytes as v1 JSON encrypted payload.
Packages: crypto exports & derive
packages/crypto/src/vault/derive-ipns.ts, packages/crypto/src/vault/index.ts, packages/crypto/src/index.ts
Added deriveVaultKeyIpnsKeypair with new HKDF info; re-exported through crypto package surface.
Packages: core vault blob/types & tests
packages/core/src/vault/blob.ts, packages/core/src/vault/types.ts, packages/core/src/index.ts, packages/core/src/__tests__/*
v2 blob format changed to key-only; removed VaultBlobV2 type; serialize/deserialize signatures and tests updated to key-only envelope.
SDK: folder fetch
packages/sdk-core/src/folder/index.ts
Removed v2 parsing branch; fetchAndDecryptMetadata simplified to decode fetched bytes directly as v1 JSON metadata.
Exports: public API surface
packages/crypto/src/index.ts, packages/core/src/index.ts
Crypto re-exports deriveVaultKeyIpnsKeypair; core removed VaultBlobV2 type from exports.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Web as Web Client
    participant Crypto as Crypto Package
    participant IPFS as IPFS/IPNS

    User->>Web: Initialize vault

    Note over Web,Crypto: Stage 1 — Vault Key Blob (v2 key-only)
    Web->>Crypto: deriveVaultKeyIpnsKeypair(userPrivateKey)
    Crypto-->>Web: vaultKeyPriv, vaultKeyIpnsName
    Web->>Web: ECIES-wrap rootFolderKey -> v2 key blob
    Web->>IPFS: Upload v2 vault-key blob
    IPFS-->>Web: IPFS hash
    Web->>IPFS: Publish hash to vaultKeyIpnsName
    IPFS-->>Web: Publish success

    Note over Web,Crypto: Stage 2 — Root Folder Metadata (v1 JSON)
    Web->>Crypto: deriveVaultIpnsKeypair(userPrivateKey)
    Crypto-->>Web: rootPriv, rootIpnsName
    Web->>Web: Encrypt root metadata as v1 JSON
    Web->>IPFS: Upload v1 metadata JSON
    IPFS-->>Web: IPFS hash
    Web->>IPFS: Publish hash to rootIpnsName
    IPFS-->>Web: Publish success

    User->>Web: Login (existing vault)
    Web->>Crypto: deriveVaultKeyIpnsKeypair(userPrivateKey)
    Crypto-->>Web: vaultKeyPriv, vaultKeyIpnsName
    Web->>IPFS: Resolve vaultKeyIpnsName -> v2 blob
    IPFS-->>Web: v2 blob
    Web->>Web: Decrypt ECIES -> rootFolderKey

    Web->>Crypto: deriveVaultIpnsKeypair(userPrivateKey)
    Crypto-->>Web: rootPriv, rootIpnsName
    Web->>IPFS: Resolve rootIpnsName -> v1 JSON
    IPFS-->>Web: v1 JSON
    Web->>Web: Decrypt metadata using rootFolderKey
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: separate vault key blob from root folder IPNS name' accurately and concisely summarizes the main architectural change across the changeset: deriving a dedicated IPNS name for the vault key blob distinct from the root folder IPNS name to prevent overwrites.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/vault-key-ipns-separation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Mar 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 24.03846% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.17%. Comparing base (6aa4114) to head (da32c3a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
apps/desktop/src-tauri/src/commands/vault.rs 0.00% 52 Missing ⚠️
packages/crypto/src/vault/derive-ipns.ts 5.00% 19 Missing ⚠️
apps/desktop/src-tauri/src/crypto/hkdf.rs 0.00% 5 Missing ⚠️
apps/desktop/src-tauri/src/fuse/mod.rs 0.00% 2 Missing ⚠️
apps/desktop/src-tauri/src/fuse/decrypt.rs 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #349      +/-   ##
==========================================
- Coverage   51.20%   51.17%   -0.03%     
==========================================
  Files         115      117       +2     
  Lines        9259    10167     +908     
  Branches      745      747       +2     
==========================================
+ Hits         4741     5203     +462     
- Misses       4339     4785     +446     
  Partials      179      179              
Flag Coverage Δ
api 82.87% <20.83%> (-0.27%) ⬇️
api-client 82.87% <20.83%> (-0.27%) ⬇️
core 82.87% <20.83%> (-0.27%) ⬇️
crypto 82.87% <20.83%> (-0.27%) ⬇️
desktop 29.86% <25.00%> (+3.76%) ⬆️
sdk 82.87% <20.83%> (-0.27%) ⬇️
sdk-core 82.87% <20.83%> (-0.27%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
apps/desktop/src-tauri/src/crypto/vault_blob.rs 100.00% <100.00%> (ø)
packages/core/src/vault/blob.ts 94.59% <100.00%> (+12.37%) ⬆️
apps/desktop/src-tauri/src/fuse/decrypt.rs 0.00% <0.00%> (ø)
apps/desktop/src-tauri/src/fuse/mod.rs 20.88% <0.00%> (+20.88%) ⬆️
apps/desktop/src-tauri/src/crypto/hkdf.rs 89.36% <0.00%> (-10.64%) ⬇️
packages/crypto/src/vault/derive-ipns.ts 58.69% <5.00%> (-41.31%) ⬇️
apps/desktop/src-tauri/src/commands/vault.rs 0.00% <0.00%> (ø)

... and 8 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Add VaultKeyBlob (v2) section with binary format, lifecycle, and
  cross-platform notes
- Mark EncryptedVaultKeys as removed (DB crypto columns dropped)
- Add vault key blob IPNS to derivation summary with separation rationale
- Add vault_blob to cross-implementation parity table
- Renumber sections to accommodate new VaultKeyBlob entry
- Bump document version to 1.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f83dad09c4e5

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a vault-bricking bug by separating the vault key blob (v2 envelope containing the ECIES-wrapped rootFolderKey) from the root folder IPNS record, so normal root folder metadata publishes can’t overwrite key material.

Changes:

  • Added a dedicated HKDF context + derivation helper for a “vault key blob IPNS” (cipherbox-vault-key-ipns-v1) alongside the existing “root folder IPNS” derivation.
  • Updated web + desktop initialization/login/recovery flows to use two IPNS names (vault-key-blob vs root-folder-metadata) and removed folder-side v2 blob detection.
  • Simplified desktop FUSE metadata publishing/decryption to treat all folder metadata payloads as v1 JSON {iv,data}.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/sdk-core/src/folder/index.ts Removes v2 blob detection when reading folder metadata from IPFS.
packages/crypto/src/vault/index.ts Re-exports the new vault-key-blob IPNS derivation helper.
packages/crypto/src/vault/derive-ipns.ts Adds deriveVaultKeyIpnsKeypair() using HKDF info cipherbox-vault-key-ipns-v1.
packages/crypto/src/index.ts Exposes deriveVaultKeyIpnsKeypair from the top-level crypto package.
apps/web/src/services/folder.service.ts Removes v2 blob detection from web folder metadata fetch/decrypt.
apps/web/src/hooks/useAuth.ts Login reads root key from vault-key-blob IPNS; init publishes both key blob + root folder metadata.
apps/web/public/recovery.html Updates recovery to derive the vault-key-blob IPNS name (not root folder IPNS).
apps/desktop/src-tauri/src/fuse/mod.rs Removes root-folder v2 blob encryption path; root publishes use v1 JSON like other folders.
apps/desktop/src-tauri/src/fuse/decrypt.rs Removes v2 blob detection for folder metadata decryption.
apps/desktop/src-tauri/src/crypto/hkdf.rs Adds Rust derive_vault_key_ipns_keypair mirroring TypeScript derivation.
apps/desktop/src-tauri/src/commands/vault.rs Desktop init publishes both key blob + root metadata; fetch reads key from vault-key-blob IPNS.
.planning/todos/pending/2026-03-24-separate-vault-key-blob-from-root-folder-ipns-name.md Tracks the design/implementation plan for the two-IPNS separation.
Comments suppressed due to low confidence (2)

apps/desktop/src-tauri/src/fuse/decrypt.rs:21

  • By removing v2 blob detection here, decrypt_metadata_from_ipfs_public will fail to parse legacy root-folder payloads that are still stored as v2 vault blobs (JSON parse error on binary). If existing vaults may still have root metadata in v2 format, consider keeping v2 detection as a backwards-compatible fallback (deserialize_vault_blob_v2 → use metadata_json) until all vaults have been migrated to v1 JSON root metadata.
/// All folder metadata (including root) uses v1 JSON `{"iv":"...","data":"..."}`.
/// The v2 blob format is only used for the vault key blob (separate IPNS name).
pub fn decrypt_metadata_from_ipfs_public(
    encrypted_bytes: &[u8],
    folder_key: &[u8],
) -> Result<crate::crypto::folder::FolderMetadata, String> {
    let json_portion = encrypted_bytes;

    #[derive(serde::Deserialize)]
    struct EncryptedFolderMetadata {
        iv: String,
        data: String,
    }

    let encrypted: EncryptedFolderMetadata = serde_json::from_slice(json_portion)
        .map_err(|e| format!("Failed to parse encrypted metadata JSON: {}", e))?;

apps/web/public/recovery.html:575

  • deriveVaultIpnsName() now uses the vault key blob HKDF info (cipherbox-vault-key-ipns-v1), but the function name and docstring still say it derives the "vault root IPNS" and matches deriveVaultIpnsKeypair. This is misleading for recovery and could cause using the wrong IPNS name; rename/update the comment to reflect that it derives the vault-key-blob IPNS (matches deriveVaultKeyIpnsKeypair).
    /**
     * Derive the vault root IPNS name from user's secp256k1 private key.
     * Matches the derivation in @cipherbox/crypto deriveVaultIpnsKeypair.
     *
     * Path: secp256k1 privateKey -> HKDF-SHA256 -> 32-byte Ed25519 seed
     *       -> Ed25519 public key -> IPNS name (k51...)
     */
    async function deriveVaultIpnsName(userPrivateKey) {
      // 1. HKDF-SHA256 to derive 32-byte Ed25519 seed
      const keyMaterial = await crypto.subtle.importKey(
        'raw', userPrivateKey, 'HKDF', false, ['deriveBits']
      );
      const derivedBits = await crypto.subtle.deriveBits(
        { name: 'HKDF', hash: 'SHA-256', salt: VAULT_HKDF_SALT, info: VAULT_KEY_HKDF_INFO },
        keyMaterial, 256
      );
      const ed25519Seed = new Uint8Array(derivedBits);

      // 2. Derive Ed25519 public key from seed
      const ed25519PublicKey = await ed.getPublicKeyAsync(ed25519Seed);

      // 3. Derive IPNS name from Ed25519 public key
      return ed25519PublicKeyToIpnsName(ed25519PublicKey);
    }

Comment thread apps/desktop/src-tauri/src/fuse/mod.rs
Comment thread apps/web/src/hooks/useAuth.ts Outdated
Comment thread apps/web/src/services/folder.service.ts
Comment thread apps/web/src/hooks/useAuth.ts
Comment thread packages/sdk-core/src/folder/index.ts
Comment thread apps/desktop/src-tauri/src/commands/vault.rs
Comment thread apps/desktop/src-tauri/src/commands/vault.rs
The vault key blob v2 format now stores only the ECIES-wrapped
rootFolderKey — no folder metadata. Root folder metadata is created
separately as standard v1 JSON during vault init.

Format: 0x02 | uint16_BE(key_len) | ECIES_encrypted_key

- serialize/deserialize take/return a single Uint8Array (no metadata)
- Remove VaultBlobV2 type (deserialize returns Uint8Array directly)
- Update cross-platform test vectors (TS + Rust) for key-only format
- Simplify init flows: key blob and folder metadata are independent
- Remove v2 detection from recovery tool folder traversal
- Update METADATA_SCHEMAS.md format table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 37632b4f3079

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/desktop/src-tauri/src/commands/vault.rs (1)

90-95: Consider aborting on vault key blob publish conflict.

A conflict at sequence 0 during vault init indicates a serious issue (e.g., race condition with another device or corrupted state). The current code logs a warning but continues, potentially leaving the vault in an inconsistent state if the subsequent folder publish or backend registration fails.

For vault initialization, consider returning an error on conflict rather than proceeding:

     match crate::api::ipns::publish_ipns(&state.api, &key_publish_req).await? {
         crate::api::ipns::PublishResult::Success => {}
         crate::api::ipns::PublishResult::Conflict { .. } => {
-            log::warn!("Unexpected conflict on vault key blob publish (sequence 0)");
+            return Err("Unexpected conflict on vault key blob publish (sequence 0) - vault may already exist".into());
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src-tauri/src/commands/vault.rs` around lines 90 - 95, The match
handling the result of crate::api::ipns::publish_ipns(&state.api,
&key_publish_req) currently swallows PublishResult::Conflict and only logs a
warning; change it to abort vault initialization by returning an Err when
PublishResult::Conflict is returned (instead of just log), so the calling init
routine fails fast; update the match arm handling
crate::api::ipns::PublishResult::Conflict { .. } to construct and return a
descriptive error (propagate the error type used in the surrounding function)
referencing publish_ipns, PublishResult::Conflict, key_publish_req and
state.api.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/METADATA_SCHEMAS.md`:
- Line 210: The cross-reference text "Section 14" in the sentence referencing
IPNS key derivation is out of sync with the anchor
`#15-ipns-key-derivation-summary`; update the display text to "Section 15" (or
alternatively change the anchor to `#14-ipns-key-derivation-summary` if you
intended Section 14) so the link and label match; locate the string in
METADATA_SCHEMAS.md that contains "Section 14" and
`#15-ipns-key-derivation-summary` and make the label/anchor consistent.

---

Nitpick comments:
In `@apps/desktop/src-tauri/src/commands/vault.rs`:
- Around line 90-95: The match handling the result of
crate::api::ipns::publish_ipns(&state.api, &key_publish_req) currently swallows
PublishResult::Conflict and only logs a warning; change it to abort vault
initialization by returning an Err when PublishResult::Conflict is returned
(instead of just log), so the calling init routine fails fast; update the match
arm handling crate::api::ipns::PublishResult::Conflict { .. } to construct and
return a descriptive error (propagate the error type used in the surrounding
function) referencing publish_ipns, PublishResult::Conflict, key_publish_req and
state.api.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47af1885-0040-464a-81cf-b74d2d9bebf1

📥 Commits

Reviewing files that changed from the base of the PR and between 6aa4114 and ad6e0fc.

📒 Files selected for processing (13)
  • .planning/todos/pending/2026-03-24-separate-vault-key-blob-from-root-folder-ipns-name.md
  • apps/desktop/src-tauri/src/commands/vault.rs
  • apps/desktop/src-tauri/src/crypto/hkdf.rs
  • apps/desktop/src-tauri/src/fuse/decrypt.rs
  • apps/desktop/src-tauri/src/fuse/mod.rs
  • apps/web/public/recovery.html
  • apps/web/src/hooks/useAuth.ts
  • apps/web/src/services/folder.service.ts
  • docs/METADATA_SCHEMAS.md
  • packages/crypto/src/index.ts
  • packages/crypto/src/vault/derive-ipns.ts
  • packages/crypto/src/vault/index.ts
  • packages/sdk-core/src/folder/index.ts

Comment thread docs/METADATA_SCHEMAS.md Outdated
- Remove dead user_public_key param from spawn_metadata_publish and
  both call sites (no longer needed after v2 root removal)
- Use vaultKeyKeypair.ipnsName directly instead of re-deriving via
  deriveIpnsName (login + init paths)
- Fix section cross-reference "Section 14" -> "Section 15" in
  METADATA_SCHEMAS.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 50257f9b7a08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

apps/web/public/recovery.html:572

  • deriveVaultIpnsName() is now using VAULT_KEY_HKDF_INFO, but the function name/docs still say it derives the root folder vault IPNS name and that it matches deriveVaultIpnsKeypair. This will make the recovery flow derive the vault-key-blob IPNS name while later treating it as the root folder IPNS name (breaking folder traversal). Update the recovery tool to derive and use two distinct names: vault-key-blob IPNS for fetching the v2 key blob, and root-folder IPNS for fetching v1 folder metadata.
    // Crypto: Derive Vault IPNS Name from Private Key (HKDF)
    // =========================================================================
    const VAULT_HKDF_SALT = new TextEncoder().encode('CipherBox-v1');
    // Vault KEY blob IPNS (stores ECIES-wrapped rootFolderKey) — NOT the root folder IPNS
    const VAULT_KEY_HKDF_INFO = new TextEncoder().encode('cipherbox-vault-key-ipns-v1');

    /**
     * Derive the vault root IPNS name from user's secp256k1 private key.
     * Matches the derivation in @cipherbox/crypto deriveVaultIpnsKeypair.
     *
     * Path: secp256k1 privateKey -> HKDF-SHA256 -> 32-byte Ed25519 seed
     *       -> Ed25519 public key -> IPNS name (k51...)
     */
    async function deriveVaultIpnsName(userPrivateKey) {
      // 1. HKDF-SHA256 to derive 32-byte Ed25519 seed
      const keyMaterial = await crypto.subtle.importKey(
        'raw', userPrivateKey, 'HKDF', false, ['deriveBits']
      );
      const derivedBits = await crypto.subtle.deriveBits(
        { name: 'HKDF', hash: 'SHA-256', salt: VAULT_HKDF_SALT, info: VAULT_KEY_HKDF_INFO },
        keyMaterial, 256
      );
      const ed25519Seed = new Uint8Array(derivedBits);

      // 2. Derive Ed25519 public key from seed
      const ed25519PublicKey = await ed.getPublicKeyAsync(ed25519Seed);

      // 3. Derive IPNS name from Ed25519 public key
      return ed25519PublicKeyToIpnsName(ed25519PublicKey);
    }

Comment thread docs/METADATA_SCHEMAS.md Outdated
Comment thread docs/METADATA_SCHEMAS.md Outdated
Comment thread apps/web/public/recovery.html
Comment thread docs/METADATA_SCHEMAS.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/public/recovery.html (1)

546-563: ⚠️ Potential issue | 🟠 Major

Derive the root-folder IPNS name separately from the vault-key IPNS name.

deriveVaultIpnsName() now uses cipherbox-vault-key-ipns-v1, so the IPFS-direct path is deriving the vault-key blob location and later reusing it as rootIpnsName. Step 4 then resolves the key blob as if it were folder metadata, so migrated vault recovery breaks as soon as it starts traversing folders.

🔧 Proposed fix
 const VAULT_HKDF_SALT = new TextEncoder().encode('CipherBox-v1');
-// Vault KEY blob IPNS (stores ECIES-wrapped rootFolderKey) — NOT the root folder IPNS
-const VAULT_KEY_HKDF_INFO = new TextEncoder().encode('cipherbox-vault-key-ipns-v1');
+const VAULT_ROOT_HKDF_INFO = new TextEncoder().encode('cipherbox-vault-ipns-v1');
+const VAULT_KEY_HKDF_INFO = new TextEncoder().encode('cipherbox-vault-key-ipns-v1');

-async function deriveVaultIpnsName(userPrivateKey) {
+async function deriveVaultRootIpnsName(userPrivateKey) {
+  return deriveVaultIpnsNameWithInfo(userPrivateKey, VAULT_ROOT_HKDF_INFO);
+}
+
+async function deriveVaultKeyIpnsName(userPrivateKey) {
+  return deriveVaultIpnsNameWithInfo(userPrivateKey, VAULT_KEY_HKDF_INFO);
+}
+
+async function deriveVaultIpnsNameWithInfo(userPrivateKey, info) {
   const keyMaterial = await crypto.subtle.importKey(
     'raw', userPrivateKey, 'HKDF', false, ['deriveBits']
   );
   const derivedBits = await crypto.subtle.deriveBits(
-    { name: 'HKDF', hash: 'SHA-256', salt: VAULT_HKDF_SALT, info: VAULT_KEY_HKDF_INFO },
+    { name: 'HKDF', hash: 'SHA-256', salt: VAULT_HKDF_SALT, info },
     keyMaterial, 256
   );
   const ed25519Seed = new Uint8Array(derivedBits);
   const ed25519PublicKey = await ed.getPublicKeyAsync(ed25519Seed);
   return ed25519PublicKeyToIpnsName(ed25519PublicKey);
 }

Then fetch the v2 key blob with deriveVaultKeyIpnsName(privateKeyBytes), but keep exportData.rootIpnsName sourced from deriveVaultRootIpnsName(privateKeyBytes).

Also applies to: 1266-1320

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/public/recovery.html` around lines 546 - 563, deriveVaultIpnsName is
currently using the VAULT_KEY_HKDF_INFO (cipherbox-vault-key-ipns-v1) so the
code derives the vault-key IPNS name and later incorrectly reuses it as the
root-folder IPNS name; split the derivation into two distinct functions and
uses: keep deriveVaultKeyIpnsName (uses VAULT_KEY_HKDF_INFO) to locate/fetch the
v2 key blob, and introduce/restore deriveVaultRootIpnsName (uses the root-folder
HKDF info) to produce the actual root folder IPNS name; ensure
exportData.rootIpnsName is assigned from
deriveVaultRootIpnsName(privateKeyBytes) while any key-blob fetch uses
deriveVaultKeyIpnsName(privateKeyBytes) so folder resolution uses the correct
IPNS name.
apps/web/src/hooks/useAuth.ts (1)

102-146: ⚠️ Potential issue | 🟠 Major

Only treat vaultApi.getVault() 404 as “new user”.

This catch now wraps the dedicated key-blob resolve/fetch too. If an existing vault has a missing or lagging vault-key CID and one of those downstream calls returns 404, we’ll re-enter initialization against the same deterministic IPNS names instead of surfacing a broken/missing vault-key record.

🔧 Proposed fix
-    try {
+    let existingVaultLoaded = false;
+    try {
       // Try to fetch existing vault
       const existingVault = await vaultApi.getVault();
+      existingVaultLoaded = true;
@@
-      if (is404) {
+      if (is404 && !existingVaultLoaded) {
         // New user -- initialize vault with separate key blob + folder metadata
         console.log('[Auth] New user -- initializing vault');
         const newVault = await initializeVault(userKeypair.privateKey);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/useAuth.ts` around lines 102 - 146, The catch block is too
broad and treats any downstream 404 (e.g., from resolveIpnsRecord or
fetchFromIpfs) as "new user"; isolate the vaultApi.getVault() call so only its
404 triggers initialization. Concretely, wrap vaultApi.getVault() in its own
try/catch (or check error origin) and if that call returns a 404 run
initializeVault(userKeypair.privateKey); for all other errors from subsequent
calls (deriveVaultKeyIpnsKeypair, resolveIpnsRecord, fetchFromIpfs, unwrapKey,
etc.) rethrow or let them propagate so they are not misclassified as new user;
keep references to vaultApi.getVault, deriveVaultKeyIpnsKeypair,
resolveIpnsRecord, fetchFromIpfs, deserializeVaultBlobV2, unwrapKey,
deriveVaultIpnsKeypair, and initializeVault to locate the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/METADATA_SCHEMAS.md`:
- Around line 437-443: Update the table row for "Vault key blob IPNS" so its
"Stores" column accurately reflects that the v2 blob is the ECIES-wrapped
rootFolderKey only (no appended metadata); align this with the VaultKeyBlob
section and the implementation in derive-ipns.ts
(packages/crypto/src/vault/derive-ipns.ts) and ensure any mention of "ECIES key
+ metadata" is replaced with "ECIES-wrapped rootFolderKey (key-only, v2
format)".

---

Outside diff comments:
In `@apps/web/public/recovery.html`:
- Around line 546-563: deriveVaultIpnsName is currently using the
VAULT_KEY_HKDF_INFO (cipherbox-vault-key-ipns-v1) so the code derives the
vault-key IPNS name and later incorrectly reuses it as the root-folder IPNS
name; split the derivation into two distinct functions and uses: keep
deriveVaultKeyIpnsName (uses VAULT_KEY_HKDF_INFO) to locate/fetch the v2 key
blob, and introduce/restore deriveVaultRootIpnsName (uses the root-folder HKDF
info) to produce the actual root folder IPNS name; ensure
exportData.rootIpnsName is assigned from
deriveVaultRootIpnsName(privateKeyBytes) while any key-blob fetch uses
deriveVaultKeyIpnsName(privateKeyBytes) so folder resolution uses the correct
IPNS name.

In `@apps/web/src/hooks/useAuth.ts`:
- Around line 102-146: The catch block is too broad and treats any downstream
404 (e.g., from resolveIpnsRecord or fetchFromIpfs) as "new user"; isolate the
vaultApi.getVault() call so only its 404 triggers initialization. Concretely,
wrap vaultApi.getVault() in its own try/catch (or check error origin) and if
that call returns a 404 run initializeVault(userKeypair.privateKey); for all
other errors from subsequent calls (deriveVaultKeyIpnsKeypair,
resolveIpnsRecord, fetchFromIpfs, unwrapKey, etc.) rethrow or let them propagate
so they are not misclassified as new user; keep references to vaultApi.getVault,
deriveVaultKeyIpnsKeypair, resolveIpnsRecord, fetchFromIpfs,
deserializeVaultBlobV2, unwrapKey, deriveVaultIpnsKeypair, and initializeVault
to locate the code to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a0eed504-8825-4e03-aede-f110d3a6ba5a

📥 Commits

Reviewing files that changed from the base of the PR and between ad6e0fc and 2a72057.

📒 Files selected for processing (12)
  • apps/desktop/src-tauri/src/commands/vault.rs
  • apps/desktop/src-tauri/src/crypto/vault_blob.rs
  • apps/desktop/src-tauri/src/fuse/mod.rs
  • apps/web/public/recovery.html
  • apps/web/src/hooks/useAuth.ts
  • docs/METADATA_SCHEMAS.md
  • packages/core/src/__tests__/vault-blob-vectors.test.ts
  • packages/core/src/__tests__/vault-blob.test.ts
  • packages/core/src/index.ts
  • packages/core/src/vault/blob.ts
  • packages/core/src/vault/index.ts
  • packages/core/src/vault/types.ts

Comment thread docs/METADATA_SCHEMAS.md Outdated
Recovery.html was using the vault key blob IPNS name for folder metadata
traversal. Now derives both IPNS names separately — vault key IPNS for
fetching the encrypted rootFolderKey, root folder IPNS for recovery
folder traversal. Also updates METADATA_SCHEMAS.md to reflect key-only
v2 blob format (no bundled metadata).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: d2b9f4987bb4
The catch block was too broad — any 404 from resolveIpnsRecord or
fetchFromIpfs would be misclassified as "new user" and trigger vault
re-initialization. Now getVault() is in its own try/catch so only its
404 triggers the init path; IPNS/IPFS errors propagate correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 716c90051044
@FSM1

FSM1 commented Mar 24, 2026

Copy link
Copy Markdown
Owner Author

Addressed 2 outside-diff-range comments from CodeRabbit review:

  1. recovery.html (lines 546-563, 1266-1320) — Already fixed in 74066a5. Recovery now derives both IPNS names separately (directVaultKeyIpnsName for key blob fetch, directRootFolderIpnsName for folder traversal).

  2. useAuth.ts (lines 102-146) — Fixed in 50de1b8. Isolated getVault() in its own try/catch so only its 404 triggers new-user initialization. Downstream IPFS/IPNS errors now propagate correctly instead of being misclassified as "new user".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.

Comment thread apps/desktop/src-tauri/src/commands/vault.rs
Comment thread apps/desktop/src-tauri/src/commands/vault.rs
Comment thread docs/METADATA_SCHEMAS.md Outdated
Comment thread apps/web/public/recovery.html Outdated
Comment thread apps/web/src/hooks/useAuth.ts
Comment thread apps/web/src/hooks/useAuth.ts
Comment thread apps/web/src/hooks/useAuth.ts
- METADATA_SCHEMAS: distinguish immutable blob content from IPNS record
  republishing (handled by TEE every 3h)
- recovery.html: update label to "v2 vault key blob + v1 folder metadata"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 41ca9c6985ab

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Comment thread apps/desktop/src-tauri/src/commands/vault.rs Outdated
Comment thread apps/web/src/hooks/useAuth.ts Outdated
Comment thread packages/core/src/vault/blob.ts Outdated
Comment thread apps/desktop/src-tauri/src/commands/vault.rs Outdated
- Clarify "v1 folder metadata" comments to distinguish IPFS envelope
  format (v1 {iv, data}) from FolderMetadata schema version (v2)
- Use subarray() instead of slice() in deserializeVaultBlobV2 for
  zero-copy (matches Rust borrowed-slice behavior)
- Abort vault init on sequence-0 conflict instead of warn-and-continue
  to prevent mismatched rootFolderKey

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: ed4ac2357b62
@FSM1
FSM1 enabled auto-merge (squash) March 24, 2026 06:37
@FSM1
FSM1 merged commit f04ba16 into main Mar 24, 2026
24 checks passed
@FSM1
FSM1 deleted the fix/vault-key-ipns-separation branch March 24, 2026 06:47
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.

2 participants