Skip to content

feat(keyring): OS keychain for secrets with dev-mode file backend#2513

Merged
senamakel merged 9 commits into
tinyhumansai:mainfrom
senamakel:feat/keychain-secrets
May 23, 2026
Merged

feat(keyring): OS keychain for secrets with dev-mode file backend#2513
senamakel merged 9 commits into
tinyhumansai:mainfrom
senamakel:feat/keychain-secrets

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented May 23, 2026

Summary

  • New `src/openhuman/keyring/` module wraps the OS keychain (macOS Keychain / Windows Credential Manager / Linux Secret Service) behind a small typed API. All entries are namespaced by `user_id` so the same key (`secret.master`, `auth:<profile_id>`, `wallet.mnemonic`) is scoped per user.
  • Auto-selects a dev-only `FileBackend` (JSON at `{workspace}/dev-keychain.json`) in debug builds or when `OPENHUMAN_APP_ENV=dev|staging`, so `cargo run` / `tauri dev` does NOT trigger macOS keychain permission prompts on every launch. Production release builds use the real OS keychain. Override with `OPENHUMAN_KEYRING_BACKEND=os|file|mock`. Selected backend is logged once at startup.
  • Migrates three on-disk secret stores into the keychain on first launch, transparently and idempotently: `~/.openhuman/.secret_key` (plaintext master key), `auth-profiles.json` (`enc2:` ChaCha20-encrypted OAuth/API tokens), and `wallet-state.json` (Argon2id+AES-GCM `encrypted_mnemonic`).
  • Wallet keeps password wrapping for defense-in-depth — the keychain holds the still-encrypted ciphertext, not plaintext.
  • `OPENHUMAN_CORE_TOKEN` intentionally untouched (ephemeral per-launch, no keychain benefit).

Problem

Sensitive material lived next to its ciphertext on disk: `~/.openhuman/.secret_key` (0600) was the ChaCha20 root that decrypted every OAuth token and API key in `auth-profiles.json`. Anyone with file read on the user's home directory could exfiltrate all provider credentials in one step. The wallet mnemonic ciphertext sat in a JSON state file. No OS-level secret management was in play despite all three desktop platforms shipping mature keychain APIs.

Solution

Adopt the `keyring` crate (`apple-native`, `windows-native`, `sync-secret-service`, `crypto-rust` features) and route all secret persistence through a new `src/openhuman/keyring/` module with a backend trait, OS / File / Mock implementations, and these functions:

```rust
pub fn get(user_id, key) -> Result<Option>;
pub fn set(user_id, key, value) -> Result<()>;
pub fn delete(user_id, key) -> Result<()>;
pub fn is_available() -> bool;
pub fn get_or_create_random(user_id, key, len_bytes) -> Result;
pub fn migrate_from_file(user_id, key, path) -> Result;
```

Backend selection precedence: `OPENHUMAN_KEYRING_BACKEND` env override > `cfg!(debug_assertions)` > `OPENHUMAN_APP_ENV=dev|staging` > OS keychain. The dev FileBackend is a plain JSON map (it's a dev artifact, not a production secret store) at `{workspace}/dev-keychain.json`, gitignored, file mode 0600 on Unix.

Callers updated:

  • `SecretStore::load(user_id)` now keychain-backed via `get_or_create_random("secret.master", 32)` with one-shot file migration from `~/.openhuman/.secret_key`.
  • `AuthProfile` token/token_set fields move to keychain at `"auth:{profile_id}"`; metadata stays in plaintext JSON. Legacy `enc2:` JSON is decrypted with the keychain-backed master key and re-stored as keychain entries; JSON is rewritten without secret fields. Profile deletion deletes the keychain entries.
  • Wallet ciphertext moves from `wallet-state.json` `encrypted_mnemonic` to keychain at `"wallet.mnemonic"`; field dropped from JSON post-migration.

Tradeoffs / decisions:

  • Linux release builds without a Secret Service daemon: `is_available()` returns false and callers fall back to legacy encrypted JSON. Worth a follow-up if we ship to headless Linux.
  • Windows FileBackend relies on workspace dir under AppData rather than an explicit `icacls` ACL — acceptable for a dev artifact only.
  • Per-profile keychain entries (vs single blob) chosen for granular revocation and to avoid Windows Credential Manager's ~2.5KB per-entry limit.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • N/A: Diff coverage tooling not run locally; CI will gate. New tests added for FileBackend (round-trip, user isolation, migration outcomes).
  • N/A: behaviour-only change to secret storage location — no new feature rows in coverage matrix.
  • N/A: no feature IDs added/removed.
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: no release-cut surface touched.
  • N/A: no linked issue.

Impact

  • Desktop (mac/Windows/Linux): first launch after upgrade migrates `.secret_key`, `auth-profiles.json` enc-fields, and wallet `encrypted_mnemonic` into the OS keychain. Idempotent — safe to launch repeatedly. Old files are deleted only after the keychain write is verified by read-back.
  • Security: removes plaintext master key from disk in production; reduces blast radius of file-read attacks on `~/.openhuman`. Wallet still requires user password (defense-in-depth preserved).
  • DX: `tauri dev` / `cargo run` no longer spam macOS keychain auth prompts — debug builds auto-use the FileBackend.
  • Migration: idempotent and one-way (no rollback once keychain has the entries).
  • Pre-push hook auto-fix commit: `chore: apply pre-push auto-fixes (cargo fmt)` followup commit added formatting fixes the hook produced.

Related

  • Closes: N/A
  • Follow-up PR(s)/TODOs: Linux headless fallback story; Windows FileBackend ACL if we ever ship it to prod; consider a manual "resync to keychain" admin command.

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: `feat/keychain-secrets`
  • Commit SHA: `20deb4d19` (auto-fix) on top of `6366c4eae` (main implementation)

Validation Run

  • N/A: `pnpm --filter openhuman-app format:check` — not separately invoked; pre-push hook ran fmt and applied fixes (committed).
  • N/A: `pnpm typecheck` — Rust-only change; no TS surface touched.
  • Focused tests: `cargo check` clean, `pnpm rust:check` clean, 9 FileBackend tests pass.
  • Rust fmt/check (if changed): cargo fmt applied via pre-push hook; `cargo check` clean.
  • N/A: Tauri fmt/check (if changed) — `app/src-tauri/` not touched.

Validation Blocked

  • `command:` N/A
  • `error:` N/A
  • `impact:` N/A

Behavior Changes

  • Intended behavior change: secret storage relocates from on-disk files to OS keychain (with dev FileBackend fallback). User-facing behavior unchanged.
  • User-visible effect: macOS users may see a one-time keychain access prompt for the new `openhuman` service in release builds. Dev builds see no prompts. Old secret files are removed from `~/.openhuman` after successful migration.

Parity Contract

  • Legacy behavior preserved: ChaCha20-Poly1305 for transit-on-disk still available via dev FileBackend path; Argon2id+AES-GCM wallet wrapping preserved; `OPENHUMAN_CORE_TOKEN` untouched.
  • Guard/fallback/dispatch parity checks: `is_available()` gates keychain use; callers fall back to legacy encrypted files when false (Linux headless).

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: N/A
  • Resolution (closed/superseded/updated): N/A

Summary by CodeRabbit

  • New Features

    • OS keychain integration for credentials and wallet mnemonic with workspace-scoped namespaces, automatic migration from on-disk JSON, and keychain-aware status reporting.
  • Public APIs

    • Exposed keyring operations: get/set/delete, availability probe, get-or-create random IDs, and file-to-keyring migration outcomes.
  • Tests

    • Added comprehensive tests for OS and file backends, migration flows, permissions, and idempotent random-key behavior.
  • Chores

    • Added keyring dependency and gitignore entry for a dev keychain file.

Review Change Stack

senamakel added 2 commits May 22, 2026 21:44
…ychain migration

Phase 1 — Dev FileBackend (NEW):
- Add `src/openhuman/keyring/backend.rs` with `KeyringBackend` trait, `OsBackend`
  (wraps `keyring` crate), `FileBackend` (plain JSON at `{workspace}/dev-keychain.json`),
  and `MockBackend` (test-only in-memory map).
- Backend selected once at startup via priority: `OPENHUMAN_KEYRING_BACKEND` env var >
  `cfg!(debug_assertions)` > `OPENHUMAN_APP_ENV=dev|staging` > OS keychain.
- Logged once at init: `[keyring] backend=file path=...` or `[keyring] backend=os`.
- `dev-keychain.json` is NOT encrypted (dev artifact); added to `.gitignore`.
- FileBackend: atomic write (tmp+rename), Unix mode 0600, JSON map format.
- Rewrite `mod.rs` to route `get`/`set`/`delete`/`is_available` through the
  selected backend. `is_available()` always returns true for file/mock backends.
- `init_workspace(path)` registers workspace dir for FileBackend path resolution;
  falls back to `OPENHUMAN_WORKSPACE` env var or home-dir defaults.
- Remove now-unused `entry.rs`.

Phase 2 — Auth profiles keychain migration (credentials/profiles.rs):
- `AuthProfilesStore` probes keychain availability at construction and caches it.
- On load: keychain entries take priority; legacy enc2:/enc: JSON fields are
  decrypted and promoted to keychain on first read (idempotent); JSON secret
  fields are cleared and file rewritten.
- On save: secrets stored in keychain (JSON gets no secret fields); falls back
  to ChaCha20-Poly1305 encrypted JSON on keychain error.
- Profile deletion removes keychain entries (idempotent).

Phase 3 — Wallet mnemonic keychain migration (wallet/ops.rs):
- Encrypted mnemonic ciphertext moves from `wallet-state.json` to keychain at
  `"wallet.mnemonic"` (user-id-scoped).
- On load: keychain first; if missing and JSON has `encrypted_mnemonic`, migrates
  to keychain and rewrites JSON without that field.
- Password unlock (Argon2id+AES-GCM) continues to work post-migration.

Tests:
- 9 new FileBackend tests: round-trip, overwrite, user isolation, multi-key,
  delete-nonexistent, migrate-from-file, Unix mode 0600, env-var selection,
  get_or_create_random idempotency.
- OS backend tests skip when Secret Service unavailable (Linux headless/CI).
@senamakel senamakel requested a review from a team May 23, 2026 05:01
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a pluggable keyring module (OS/file/mock backends) and integrates OS keychain storage and migration for auth-profile secrets and the wallet mnemonic, with fallbacks to encrypted JSON and comprehensive unit tests.

Changes

Keyring-backed secret storage for profiles and wallet

Layer / File(s) Summary
Keyring backend abstraction and error types
src/openhuman/keyring/backend.rs, src/openhuman/keyring/error.rs
The KeyringBackend trait unifies OS (OsBackend), file (FileBackend), and mock (MockBackend) implementations. OsBackend wraps the keyring crate, FileBackend persists to atomic dev-keychain.json with 0600 attempts, and KeyringError enumerates operation, migration, and verification failures.
Keyring module core APIs and initialization
src/openhuman/keyring/mod.rs, src/openhuman/keyring/ops.rs, src/openhuman/keyring/store.rs
Global OnceLock manages workspace and lazy backend selection (env/debug/app-env rules). Provides init_workspace, namespaced get/set/delete, is_available (probe), get_or_create_random (CSPRNG + verify), and migrate_from_file (verify-then-delete). Module re-exports the public API and adds a test backend swap helper.
Auth profiles keychain integration
src/openhuman/credentials/profiles.rs
Adds KeychainSecrets payload, derives per-user keychain namespace, caches use_keychain in AuthProfilesStore, prefers keychain on load, promotes legacy encrypted JSON into keychain when missing, clears JSON secrets after keychain success, and deletes keychain entry on profile removal when enabled.
Wallet keychain integration and status
src/openhuman/wallet/ops.rs
Adds keychain helpers and deterministic user-id; load prefers keychain and can promote JSON mnemonic into keychain; save prefers keychain and strips mnemonic from JSON on success; to_status is keychain-aware and RPC/tests are updated.
Keyring test suite
src/openhuman/keyring/tests.rs
Unit tests for FileBackend (round-trip, idempotent delete, overwrite, namespace isolation, permissions), migration helper tests, get_or_create_random idempotency, and conditional OS-backend tests including migration happy path.
Module exports and dependencies
.gitignore, Cargo.toml, src/openhuman/mod.rs
Adds keyring crate with platform-native features, exposes pub mod keyring, and .gitignore excludes dev-keychain.json.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • graycyrus

"I hopped through keys both near and far,
Secret tokens tucked beneath the OS star,
From JSON burrows to atomic files—so neat,
I guard the bytes with paws and tiny feet,
Migration snug, the rabbit hops discreet."

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main feature: adding OS keychain support with a file-based backend for development. It is concise, clear, and directly reflects the primary objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

@senamakel
Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added feature Net-new user-facing capability or product behavior. working A PR that is being worked on by the team. labels May 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/openhuman/credentials/profiles.rs (1)

529-533: ⚡ Quick win

Log the actual keychain error for diagnostics.

The error _e is captured but not included in the log message, which loses diagnostic information when investigating keychain I/O issues.

Suggested fix
-                    Err(_e) => {
+                    Err(e) => {
                         // Keychain I/O error — fall through to JSON decrypt path.
                         log::warn!(
-                            "[auth] keychain error for profile_id={id}; falling back to JSON"
+                            "[auth] keychain error for profile_id={id}: {e}; falling back to JSON"
                         );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/credentials/profiles.rs` around lines 529 - 533, Replace the
ignored error variable in the keychain Err arm so the actual error is included
in the log; change Err(_e) to Err(e) and add the error to the log::warn! call in
the same block (e.g., include {e:?} or a formatted error) so the keychain I/O
error is emitted alongside the existing message in the profile handling code
that contains the Err(_) branch.
src/openhuman/keyring/mod.rs (1)

35-36: ⚡ Quick win

Align test-module wiring with repository Rust test pattern.

Prefer a sibling *_test.rs file wired via explicit #[path = "..._test.rs"] rather than plain mod tests;.

As per coding guidelines, "When extracting Rust tests out of an implementation file, prefer a sibling *_test.rs file wired in with #[cfg(test)] #[path = \"..._test.rs\"] mod tests;."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/keyring/mod.rs` around lines 35 - 36, Replace the bare test
module declaration in mod.rs ("#[cfg(test)] mod tests;") with an explicit
path-wired module using the repository test pattern: change it to #[cfg(test)]
#[path = "mod_test.rs"] mod tests; and add the sibling mod_test.rs file
containing the tests (ensuring the test functions remain inside the tests
module). This updates the test wiring for the module declared in mod.rs to the
preferred `*_test.rs` sibling convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/keyring/backend.rs`:
- Around line 200-210: The FileBackend's set and delete perform unsynchronized
read-modify-write (see methods set, delete, and helpers read_map/write_map)
which makes them race-prone; protect the full read-modify-write critical section
by introducing a shared in-process Mutex (e.g., a Mutex<Field> on the
FileBackend struct) and/or an OS file lock and acquire it at the start of
set/delete (and any other mutating paths referenced around lines 126/153) before
calling read_map/modify/write_map, releasing after write completes so concurrent
callers are serialized and lost updates are prevented.
- Around line 161-165: The code currently uses
serde_json::to_vec_pretty(map).unwrap_or_default() which can silently drop data
on serialization failure; replace the unwrap_or_default with proper error
propagation/handling (e.g., use serde_json::to_vec_pretty(map)? or .map_err(|e|
/* convert to your crate error */ ) to return an Err) so that the failing call
to to_vec_pretty returns a visible error instead of producing an empty payload;
update the surrounding function (the caller that assigns to `json` in
backend.rs) to propagate or handle that Result accordingly and include context
in the error message.

In `@src/openhuman/keyring/mod.rs`:
- Around line 64-400: The mod.rs currently contains heavyweight operational
logic; extract runtime code into focused sibling modules (e.g. create ops.rs and
store.rs) and keep mod.rs export-only. Move functions and types such as
build_backend, backend, workspace_dir_for_file_backend, workspace_dir init logic
(init_workspace / WORKSPACE_DIR), MigrationOutcome, get, set, delete,
is_available, get_or_create_random, migrate_from_file, namespaced_key, and
hex_encode into appropriate new modules (e.g. store.rs for backend selection and
workspace handling; ops.rs for
get/set/delete/migrate/is_available/get_or_create_random), keeping their
signatures and visibility unchanged; ensure the static BACKEND / WORKSPACE_DIR
state is moved or referenced safely (preserve initialization semantics) and any
helper functions remain accessible. In mod.rs replace the moved definitions with
pub mod declarations and pub use re-exports (e.g. pub mod store; pub use
store::{get,set,delete,MigrationOutcome, ...}) so external callers keep the same
API and adjust any internal references/imports to use the new module paths.
- Around line 412-417: The function force_backend_for_test currently swallows
the OnceLock::set failure which can silently leave a stale BACKEND; change it to
fail loudly by checking the result of BACKEND.set(b) and panicking if it returns
Err. Specifically, update the body of force_backend_for_test to call
BACKEND.set(b) and either .expect(...) or assert on is_ok() with a clear message
(e.g., "force_backend_for_test called after BACKEND already initialized") so
tests cannot continue with a silently unchanged backend.

In `@src/openhuman/keyring/tests.rs`:
- Around line 125-152: The test file_backend_migrate_from_file_happy_path is
reimplementing migration logic instead of invoking the production function;
replace the manual steps with a direct call to super::migrate_from_file(...)
(using the same temp source file and FileBackend instance) and assert the
returned MigrationOutcome and resulting backend state (e.g.,
MigrationOutcome::Migrated and fb.get(...) == Some("migrated_value")). Do the
same change for the other migration-style tests (the ones around the second
block indicated) so they call migrate_from_file and assert the correct
MigrationOutcome variants and error/cleanup behavior (e.g., file removed or
preserved) rather than duplicating the migration implementation.

---

Nitpick comments:
In `@src/openhuman/credentials/profiles.rs`:
- Around line 529-533: Replace the ignored error variable in the keychain Err
arm so the actual error is included in the log; change Err(_e) to Err(e) and add
the error to the log::warn! call in the same block (e.g., include {e:?} or a
formatted error) so the keychain I/O error is emitted alongside the existing
message in the profile handling code that contains the Err(_) branch.

In `@src/openhuman/keyring/mod.rs`:
- Around line 35-36: Replace the bare test module declaration in mod.rs
("#[cfg(test)] mod tests;") with an explicit path-wired module using the
repository test pattern: change it to #[cfg(test)] #[path = "mod_test.rs"] mod
tests; and add the sibling mod_test.rs file containing the tests (ensuring the
test functions remain inside the tests module). This updates the test wiring for
the module declared in mod.rs to the preferred `*_test.rs` sibling convention.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4edb5e72-def3-4a09-aabf-4d3268fe274c

📥 Commits

Reviewing files that changed from the base of the PR and between bbec0d6 and 20deb4d.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • .gitignore
  • Cargo.toml
  • src/openhuman/credentials/profiles.rs
  • src/openhuman/keyring/backend.rs
  • src/openhuman/keyring/error.rs
  • src/openhuman/keyring/mod.rs
  • src/openhuman/keyring/tests.rs
  • src/openhuman/mod.rs
  • src/openhuman/wallet/ops.rs

Comment thread src/openhuman/keyring/backend.rs Outdated
Comment thread src/openhuman/keyring/backend.rs
Comment thread src/openhuman/keyring/mod.rs Outdated
Comment thread src/openhuman/keyring/mod.rs Outdated
Comment thread src/openhuman/keyring/tests.rs Outdated
senamakel added 4 commits May 22, 2026 22:19
…s and silent errors

- Fix 1: propagate serde_json serialisation failure in FileBackend::write_map
  instead of silently returning empty data via unwrap_or_default; add
  KeyringError::Backend variant to carry the message.
- Fix 2: add parking_lot::Mutex to FileBackend covering the full
  read→modify→write cycle in set() and delete() for in-process safety.
- Fix 3: split heavy mod.rs into ops.rs (get/set/delete/migrate/helpers)
  and store.rs (BACKEND/WORKSPACE_DIR OnceLocks, backend selection);
  mod.rs is now re-exports only.
- Fix 4: force_backend_for_test panics loudly if BACKEND is already
  initialised instead of silently ignoring the set() failure.
- Fix 5: rewrite migration tests to call the production migrate_from_file
  logic (via run_migrate helper driving FileBackend directly) and assert
  on MigrationOutcome variants + post-conditions (file removed, value stored).
…IN=1

Default cargo test runs were blocking on the macOS Keychain GUI permission
prompt because os_keychain_available() actually probed the real backend.
Now opt-in only: set OPENHUMAN_TEST_OS_KEYCHAIN=1 to exercise the OS path,
otherwise tests use the FileBackend path and finish in milliseconds.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/openhuman/keyring/backend.rs (1)

166-199: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Map FileBackend write-path I/O failures to non-migration errors.

Line 166 and Line 180/Line 195 currently classify normal file-backend write failures as migration errors (MigrationReadFailed / MigrationDeleteFailed), which makes runtime diagnostics misleading for set/delete paths.

💡 Proposed fix
-        if let Some(parent) = self.path.parent() {
-            std::fs::create_dir_all(parent).map_err(|e| KeyringError::MigrationReadFailed {
-                path: parent.display().to_string(),
-                source: e,
-            })?;
-        }
+        if let Some(parent) = self.path.parent() {
+            std::fs::create_dir_all(parent).map_err(|e| {
+                KeyringError::Backend(format!(
+                    "failed to create keyring parent directory {}: {e}",
+                    parent.display()
+                ))
+            })?;
+        }
@@
-        std::fs::write(&tmp_path, &json).map_err(|e| KeyringError::MigrationDeleteFailed {
-            path: tmp_path.display().to_string(),
-            source: e,
-        })?;
+        std::fs::write(&tmp_path, &json).map_err(|e| {
+            KeyringError::Backend(format!(
+                "failed to write keyring temp file {}: {e}",
+                tmp_path.display()
+            ))
+        })?;
@@
-        std::fs::rename(&tmp_path, &self.path).map_err(|e| {
-            KeyringError::MigrationDeleteFailed {
-                path: self.path.display().to_string(),
-                source: e,
-            }
-        })?;
+        std::fs::rename(&tmp_path, &self.path).map_err(|e| {
+            KeyringError::Backend(format!(
+                "failed to atomically replace keyring file {}: {e}",
+                self.path.display()
+            ))
+        })?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/keyring/backend.rs` around lines 166 - 199, The write-path I/O
errors in the FileBackend (the create_dir_all(parent) error mapping, the
std::fs::write(&tmp_path, &json) mapping, and the std::fs::rename(&tmp_path,
&self.path) mapping) are incorrectly converted to migration-specific variants
(MigrationReadFailed / MigrationDeleteFailed); update those map_err closures to
return non-migration errors (e.g. KeyringError::Backend with a concise message
including the operation and path and the source error) so normal set/delete
failures are classified correctly; specifically change the map_err on
create_dir_all(parent) and the map_err closures around std::fs::write and
std::fs::rename to produce KeyringError::Backend (or the appropriate generic
write/IO KeyringError variant used elsewhere) with path/tmp_path/self.path and
include the original source error.
🧹 Nitpick comments (1)
Cargo.toml (1)

118-118: Validate keyring 3.x version/features and security posture.

  • crates.io latest is 4.0.1; version = "3" intentionally stays on 3.x (won’t automatically pick up 4.x).
  • Features apple-native, windows-native, linux-native are present for keyring 3.x.
  • No security advisories were reported for keyring in the queried vulnerability feed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` at line 118, The Cargo.toml dependency for keyring is pinned to
"3" but crates.io has 4.0.1; update the dependency by changing keyring = {
version = "4.0.1", features = ["apple-native", "windows-native", "linux-native"]
} and verify the feature names remain valid in keyring 4.0.1; if you
intentionally must stay on 3.x instead, replace version = "3" with an explicit
pinned 3.x release (e.g., "3.2.0") and add a brief comment above the dependency
explaining why we are not upgrading.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/keyring/ops.rs`:
- Around line 118-135: Add a guard at the start of get_or_create_random to
reject len_bytes == 0 before allocating or generating bytes: check the len_bytes
parameter, log a debug/error and return an appropriate KeyringError (e.g. an
InvalidInput/InvalidLength variant or create one) instead of proceeding to
allocate the zero-length vector and persisting an empty secret; place this check
before the vec![0u8; len_bytes] allocation and before OsRng.fill_bytes to
prevent creating empty secrets.

In `@src/openhuman/keyring/tests.rs`:
- Around line 140-141: The test silently swallows backend read errors by calling
fb.get(&nk).unwrap_or(None) which treats failures as None; replace those
unwrap_or(None) usages (both the occurrence checking for
MigrationOutcome::AlreadyMigrated and the later one at lines 161-162) with
proper error propagation from FileBackend::get (e.g., use the ? operator or
map_err to return the Err up the test's Result) so that backend read failures
are surfaced instead of treated as missing values.

---

Outside diff comments:
In `@src/openhuman/keyring/backend.rs`:
- Around line 166-199: The write-path I/O errors in the FileBackend (the
create_dir_all(parent) error mapping, the std::fs::write(&tmp_path, &json)
mapping, and the std::fs::rename(&tmp_path, &self.path) mapping) are incorrectly
converted to migration-specific variants (MigrationReadFailed /
MigrationDeleteFailed); update those map_err closures to return non-migration
errors (e.g. KeyringError::Backend with a concise message including the
operation and path and the source error) so normal set/delete failures are
classified correctly; specifically change the map_err on create_dir_all(parent)
and the map_err closures around std::fs::write and std::fs::rename to produce
KeyringError::Backend (or the appropriate generic write/IO KeyringError variant
used elsewhere) with path/tmp_path/self.path and include the original source
error.

---

Nitpick comments:
In `@Cargo.toml`:
- Line 118: The Cargo.toml dependency for keyring is pinned to "3" but crates.io
has 4.0.1; update the dependency by changing keyring = { version = "4.0.1",
features = ["apple-native", "windows-native", "linux-native"] } and verify the
feature names remain valid in keyring 4.0.1; if you intentionally must stay on
3.x instead, replace version = "3" with an explicit pinned 3.x release (e.g.,
"3.2.0") and add a brief comment above the dependency explaining why we are not
upgrading.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40d35401-f175-42e5-a8ba-16d5b10b3c4f

📥 Commits

Reviewing files that changed from the base of the PR and between 7b3f6c4 and 0508bd5.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • src/openhuman/keyring/backend.rs
  • src/openhuman/keyring/error.rs
  • src/openhuman/keyring/mod.rs
  • src/openhuman/keyring/ops.rs
  • src/openhuman/keyring/store.rs
  • src/openhuman/keyring/tests.rs

Comment thread src/openhuman/keyring/ops.rs
Comment thread src/openhuman/keyring/tests.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/openhuman/keyring/tests.rs (1)

5-8: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the module header to match the new opt-in OS test behavior.

The header says OS backend tests run unconditionally on macOS/Windows, but the current gate requires OPENHUMAN_TEST_OS_KEYCHAIN=1. Please align the header text to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/keyring/tests.rs` around lines 5 - 8, Update the module doc
comment in the tests module to reflect the new opt-in behavior: change the
statement that OS backend tests run unconditionally on macOS/Windows to note
they only run when OPENHUMAN_TEST_OS_KEYCHAIN=1 is set (and on Linux they remain
skipped if the Secret Service daemon is unavailable); keep the note about test
keys being prefixed with `__openhuman_test__` for cleanup and ensure the module
header (the top-level doc comment in src/openhuman/keyring/tests.rs) mentions
the OPENHUMAN_TEST_OS_KEYCHAIN env var requirement.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/openhuman/keyring/tests.rs`:
- Around line 5-8: Update the module doc comment in the tests module to reflect
the new opt-in behavior: change the statement that OS backend tests run
unconditionally on macOS/Windows to note they only run when
OPENHUMAN_TEST_OS_KEYCHAIN=1 is set (and on Linux they remain skipped if the
Secret Service daemon is unavailable); keep the note about test keys being
prefixed with `__openhuman_test__` for cleanup and ensure the module header (the
top-level doc comment in src/openhuman/keyring/tests.rs) mentions the
OPENHUMAN_TEST_OS_KEYCHAIN env var requirement.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 837d80a3-0ab7-44cb-b025-4742373d3a42

📥 Commits

Reviewing files that changed from the base of the PR and between 0508bd5 and fb342c9.

📒 Files selected for processing (1)
  • src/openhuman/keyring/tests.rs

…ad errors in tests

Two CodeRabbit follow-ups on PR tinyhumansai#2513:
- get_or_create_random now rejects len_bytes == 0 (would otherwise persist
  an empty 'secret').
- run_migrate test helper uses ? instead of unwrap_or(None), so backend
  read failures surface as test errors instead of silently being treated
  as 'no entry'.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 23, 2026
Two profile-store tests still asserted the legacy enc2: on-disk format
that the keychain migration replaced:

- store_roundtrip_with_encryption: dropped 'raw must contain enc2:'.
  Under the keychain model secret fields are omitted from the JSON
  entirely; remaining no-plaintext assertions still prove no leak.

- load_drops_profiles_whose_decryption_fails_under_rotated_key: delete
  the freshly-written keychain entry before planting a corrupt enc2:
  blob in the JSON, so load falls back to the legacy decrypt path and
  exercises the drop-on-failure behavior the test is named for.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant