Skip to content

Feat/secrets multi source sync#427

Open
0oyun wants to merge 16 commits into
berabuddies:masterfrom
0oyun:feat/secrets-multi-source-sync
Open

Feat/secrets multi source sync#427
0oyun wants to merge 16 commits into
berabuddies:masterfrom
0oyun:feat/secrets-multi-source-sync

Conversation

@0oyun

@0oyun 0oyun commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds credential import to puffer's encrypted SecretVault, so an agent can sync the
user's saved logins from Chrome, Firefox, and 1Password into secrets.json
(AES-256-GCM at rest) and use them with permission — instead of silently stalling at
login walls.

Import is one click per source from Settings → Secrets, decrypting each store
locally on the user's machine:

  • Chrome / Chromium — macOS Keychain (PBKDF2-AES-128-CBC), Windows DPAPI v10/v11
    (AES-256-GCM) and v20 App-Bound Encryption (per-machine CNG key), Linux Secret
    Service. Reads both Login Data and the Google-account Login Data For Account store.
  • Firefox — NSS key4.db + logins.json (legacy 3DES and modern AES-256-CBC).
  • 1Password — two paths: the op CLI desktop-app integration (Touch ID /
    Windows Hello, no token to hunt for) and a .1pux export file (no CLI needed).
    Both import every accessible vault's Login + Password items, resolved to plaintext
    and stored encrypted at rest. When the 1Password app is installed but the op CLI is
    not, the CLI is auto-installed and the user is pointed at the app's "Integrate with
    1Password CLI" toggle to retry.

On Windows, Chrome v20 keys are SYSTEM-protected, so a one-click import briefly
self-elevates (a single UAC prompt) to decrypt them and drops elevation immediately
after.

Changes

  • puffer-secrets: SecretVault + BrowserSource framework; Chromium decryptors
    for macOS / Windows (incl. v20 App-Bound Encryption) / Linux; Firefox NSS decryptor;
    1Password import via the op CLI and via .1pux (both cover Login + Password items).
    The op fetch is per-item tolerant (one flaky/locked item no longer discards the whole
    batch); the op CLI is auto-installed (brew / winget) when the 1Password app is present
    but the CLI is missing. The self-elevating Windows v20 import helper. Sources reduced
    to Chrome / Firefox / 1Password.
  • puffer-cli: daemon RPCs (import_browser_secrets, import_onepassword_export,
    list_secret_sources) and the hidden __win-chrome-import self-elevating subcommand.
  • puffer-desktop: Settings → Secrets UI with per-source sync buttons
    (Sync from Chrome / Firefox / 1Password, and Import 1Password export (.1pux)); tauri
    no-daemon fallback that handles the same import RPCs in-process.

macOS code signing (1Password CLI)

The 1Password CLI's macOS app integration only grants op a delegated session when
the process that invoked it is code-signedcargo build produces ad-hoc binaries
that 1Password rejects, so the daemon must be signed for the op-CLI import path to work.
Production macOS builds are signed with an Apple Developer ID certificate (and
notarized) in the release pipeline. For local test reproduction only,
scripts/sign-dev-macos.sh signs the daemon + desktop host with a persistent
self-signed certificate (re-run after each cargo build); that dev cert is never shipped.

Test Plan

Automated:

  • cargo test -p puffer-secrets — decryptor, .1pux parser, and op-path unit tests.
  • cargo check -p puffer-cli and cargo check -p corbina.
  • cargo check -p puffer-secrets --target x86_64-pc-windows-gnu (Windows-only paths).
  • npm run check (svelte-check — 0 errors) and vite build.

Manually verified end-to-end on real machines:

  • macOS 26.3.1 — Chrome (macOS Keychain) and Firefox (NSS)
    import; 1Password via the op CLI (Touch ID) and via a .1pux export; the
    app-installed/CLI-missing path auto-installs the op CLI.
  • Windows Server 2022 Datacenter (x64, build 10.0.20348), Chrome 149 — Chrome v20
    App-Bound Encryption import through the self-elevating UAC helper, plus DPAPI v10/v11
    and the Google-account store.

0oyun and others added 11 commits June 17, 2026 22:02
…mium + Edge/Brave)

Generalize the macOS-only "sync from Chrome" into a multi-source importer.

puffer-secrets:
- BrowserSource registry (chrome/edge/brave/firefox) + available_browser_sources()
  + sync_browser_source(); back-compat ChromeImportReport / import_chrome_saved_credentials.
- firefox.rs: pure-Rust NSS decrypt (legacy 3DES + modern PBES2 key wrap, master-
  password detection) + der.rs minimal DER reader. End-to-end unit tests.
- chrome.rs -> chromium.rs parameterized by variant:
  * macOS: Keychain Safe Storage + AES-128-CBC (v10/v11)
  * windows: Local State DPAPI key + AES-256-GCM (v10/v11) and App-Bound
    Encryption (v20) via SYSTEM double-DPAPI unwrap
  * linux: peanuts fallback + AES-128-CBC (Secret Service TODO)
- examples/sync_probe.rs dev/test harness.

Surface:
- daemon RPC import_browser_secrets{source} + list_secret_sources; sources[] in the
  secrets settings DTO (cli + tauri backend).
- desktop: per-source sync buttons; importBrowserSecrets API; SecretSource type.
- Playwright fakeDaemon + spec updated.

Tested: 11 puffer-secrets unit tests pass; puffer-cli builds; Windows target
cross-compiles clean. Follow-ups: 1Password (resolve-on-demand), Linux Secret
Service, live-VM verification of Windows v20 ABE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add 1Password as an import source that stores op://vault/item/field references
(never plaintext) and resolves them on demand at RequestSecret time, so 1Password
stays the source of truth (rotation/revocation/audit) and no plaintext persists
in Puffer or reaches the model.

- onepassword.rs: is_op_reference / resolve_op_reference (op read --no-newline) /
  op_cli_available / import_login_references (op item list -> op:// refs).
  Unit-tested with a fake `op` binary (no real account needed).
- request_secret runtime: resolve op:// references after the permission gate,
  before masking, so the resolved value is masked + redacted like any secret.
- SecretVault::sync_onepassword_references(); 1Password added to the import
  sources list (available when the `op` CLI is present); daemon + tauri route
  source "1password" to the reference importer.
- Resolution keys off the op:// value prefix, so a secret added via the existing
  "Add secret" form (value = op://...) already resolves — no import required.

Auth is headless via OP_SERVICE_ACCOUNT_TOKEN (read by `op`); no token handling
in-tree. 17 puffer-secrets unit tests pass; puffer-cli builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ument v20 gap

Add examples/seed_win_login.rs — a Windows-only fixture tool that writes a real
v10 credential into a Chromium Login Data using the browser's real DPAPI-protected
os_crypt key, so the decryptor can be validated end-to-end without driving the GUI.

Validated live on a from-scratch ARM Windows 11 25H2 VM (Edge 145):
- v10 path WORKS: seeder DPAPI-unwrapped Edge's real os_crypt key + wrote a v10
  AES-256-GCM blob; sync_probe independently DPAPI-unwrapped the key and decrypted
  it (imported=1, errors=0). The x86_64 probe runs on ARM via x64 emulation.

Document the v20/App-Bound-Encryption gap in decode_app_bound_key, now confirmed
empirically: ABE's two DPAPI layers use different security contexts (SYSTEM outer,
interactive-user inner), so the naive two-call unwrap fails on real ABE data —
a SYSTEM process must impersonate the user for the inner layer, and current builds
may add a further AES-GCM wrap (elevation-service path). v20 rows are detected and
reported as skipped, never silently dropped. Full v20 support is a separate effort.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…c tests

Replace the earlier (incorrect) v20 stub with the real ABE key recovery, per the
runassu/xaitax research and confirmed on a live ARM Win11 VM (Edge 145 / Chrome 149):

- unwrap_abe_key_material(): after both DPAPI layers are peeled, the 32-byte key
  is still AEAD-encrypted under a browser-hardcoded key selected by a flag byte.
  Parses [u32 hdr_len][hdr][u32 content_len][flag|iv(12)|ct(32)|tag(16)] and
  decrypts ct||tag under the flag's key (0x01 AES-256-GCM, 0x02 ChaCha20-Poly1305;
  0x03 per-machine CNG not yet supported). Pure crypto, unit-tested with synthetic
  round-trip fixtures using the real published constants.
- decode_app_bound_key() (windows): SYSTEM-DPAPI outer + user-DPAPI inner, then the
  flag-based final unwrap. The two DPAPI layers use different security contexts
  (SYSTEM outer, interactive-user inner) — the caller must run as SYSTEM
  impersonating the user.
- examples/abe_unwrap.rs: Windows dev harness (SYSTEM + impersonation) for the
  live path. Add chacha20poly1305 dep.

Validated: 20 puffer-secrets unit tests pass (incl. flag1/flag2 ABE round-trip);
Windows target cross-compiles. The LIVE on-box extraction (running as SYSTEM to
pull the real key) is intentionally NOT automated here — it is privilege-gated and
left to an explicitly-authorized run. Edge's key differs (COM-only route).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Edge key layout

FULLY GENUINE v20 validation on a live ARM Win11 VM (user-authorized): Edge 145
saved a real password via its UI; the pipeline extracted the ABE key by dual-context
DPAPI (SYSTEM-context outer unwrap via a SYSTEM scheduled task + interactive-user
inner unwrap) and AES-256-GCM-decrypted Edge's real v20 blob, recovering the exact
password ("V20_DECRYPT_OK ... password=puffersecret123").

Empirical finding now in code: Edge's post-DPAPI content is the 32-byte AES key
DIRECTLY (no Chrome-style flag/AEAD wrap) — unwrap_abe_key_material() now returns
the content as the key when content_len == 32, else does the Chrome flag-based
unwrap. Unit-tested (Edge raw-content + Chrome flag1/flag2). examples/decrypt_v20.rs
added (final v20 GCM decrypt given a recovered key).

21 puffer-secrets tests pass; Windows cross-compiles. v20 ABE is now validated
end-to-end against a real browser-encrypted password.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… etc.)

The open-source Chromium browser is common on Linux but was unmapped (only
Chrome/Edge/Brave existed). Add Chromium::Chromium across all OS paths
(Linux ~/.config/chromium, macOS Library/Application Support/Chromium +
"Chromium Safe Storage" keychain, Windows Chromium/User Data) and a
BrowserSource::Chromium so it shows up as an import source. Decryption reuses
the existing per-OS Chromium logic (validated on real macOS data).

21 puffer-secrets unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sting

Lists Login items as op:// refs (import path) and resolves each (resolve path)
against the real op CLI; prints metadata + value lengths only. Needs op on PATH
and OP_SERVICE_ACCOUNT_TOKEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surfaced by the native Linux Chromium build. Linux Chromium path validated live
in an arm64 container: chromium profile (~/.config/chromium) + peanuts key
(PBKDF2-SHA1, basic store) + AES-128-CBC v10 -> imported=1, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Modern Firefox (NSS) stores logins.json fields under AES-256-CBC with a
32-byte master key, not the legacy 3DES-CBC the decryptor assumed. The
old code hardcoded 3DES (24-byte key, 8-byte IV), so on a real Firefox
151 profile decode_login_field failed at cipher init and the import
silently yielded 0 credentials (key4.db / master key were fine).

- decode_login_field now branches on the field's algorithm OID:
  aes256-CBC (32-byte key, 16-byte IV) vs des-ede3-cbc (24-byte key).
- load_master_key keeps the full PKCS#7-stripped key (24 or 32 bytes)
  via a new pkcs7_unpad helper instead of truncating to [..24].
- Add a modern-AES-256 logins end-to-end test (the case the live test
  exposed); legacy 3DES + PBES2-key-blob tests still pass.

Validated live: real Firefox 151 saved login decrypted in a Windows VM
(imported=1, 0 errors) via sync_probe.
Adversarial review of the new Firefox path surfaced two defects:

- pkcs7_unpad was applied unconditionally to the decrypted nssPrivate.a11
  master key, but the legacy 3DES key is stored UNPADDED (exactly 24 bytes).
  A 24-byte key ending in pad-like bytes (~1/256 of legacy profiles) was
  truncated below 24 and bailed "too short", silently dropping the whole
  profile (the swallowed-error class). Now accept the stripped form only at a
  valid key length (24/32), else fall back to the raw plaintext. Regression
  test added (legacy key ending in 0x01).

- The PBES2 PBKDF2 iteration count is read verbatim from key4.db (an
  attacker-influenceable file) and fed straight to pbkdf2 with no bound; a
  crafted blob (iterations=0xFFFFFFFF) could hang import for hours. Bound it
  to 1..=10_000_000 (real profiles use ~10k-650k).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Linux, Chromium derives its AES-128 key (PBKDF2-SHA1, salt "saltysalt",
1 iteration) from a "Safe Storage" password held in the Secret Service
(gnome-keyring / kwallet) or, when the basic/text store is used, the
well-known constant "peanuts". Query the Secret Service via the `secret-tool`
CLI and always also try "peanuts", decrypting each v10 blob with whichever
candidate key works.

Validated live in an arm64 Linux container: a v10 blob encrypted under a
Secret-Service key decrypts only when secret-tool returns that key
(imported=1) and not via the peanuts fallback alone (imported=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

@0oyun is attempting to deploy a commit to the fuzzland Team on Vercel.

A member of the Team first needs to authorize it.

0oyun added 4 commits June 22, 2026 11:54
…tial import

Extend the credential-vault crate with the 1Password and Windows App-Bound
Encryption import paths:

- 1Password via the op CLI app-integration (Touch ID / Windows Hello, no token)
  and via a `.1pux` export file (no CLI). Both resolve every accessible vault's
  Login (001) + Password (005) items to plaintext, stored encrypted at rest.
  The op fetch is per-item tolerant (one flaky item no longer aborts the batch).
  When the 1Password app is present but the op CLI is missing, the CLI is
  auto-installed (brew / winget) and the user is pointed at the app's
  "Integrate with 1Password CLI" toggle to retry; the 1Password source is offered
  whenever the app OR the CLI is present.
- Windows Chrome/Chromium v20 App-Bound Encryption (CNG flag 0x03) decryption,
  plus the import-helper module used by the self-elevating subcommand.
- Keep Chrome / Firefox / 1Password sources only.
- import_browser_secrets / import_onepassword_export daemon RPCs wire the vault
  crate's Chrome/Firefox/1Password import into the desktop backend.
- Hidden `__win-chrome-import` subcommand self-elevates (one UAC prompt) to
  import Windows Chrome v20 App-Bound rows into the same vault, then drops
  elevation — elevation lasts only for the import.
…word

- Per-source sync buttons: Sync from Chrome / Firefox / 1Password, plus
  Import 1Password export (.1pux), each importing into the encrypted vault.
- Tauri no-daemon fallback handles the same import RPCs in-process.
1Password's macOS app integration only grants `op` a session to a code-signed
caller, so puffer's daemon must be signed for the op-CLI import path; `cargo
build` produces ad-hoc binaries that 1Password rejects.

scripts/sign-dev-macos.sh signs the daemon + desktop host with a persistent
self-signed certificate FOR LOCAL TEST REPRODUCTION (re-run after each build).
Production macOS builds are signed with an Apple Developer ID certificate (and
notarized) in the release pipeline; this dev cert is never shipped.
@0oyun 0oyun marked this pull request as ready for review June 22, 2026 04:13
Copilot AI review requested due to automatic review settings June 22, 2026 04:13

Copilot AI 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.

Pull request overview

This PR adds multi-source secret/credential syncing into Puffer’s encrypted SecretVault so agents can use user-approved credentials (rather than stalling at login prompts), and wires the feature through the CLI/daemon RPC layer and the desktop Settings UI.

Changes:

  • Introduces puffer-secrets import framework for Chrome/Chromium, Firefox, and 1Password (via op CLI and .1pux export), producing unified ImportReports.
  • Adds daemon/desktop RPC endpoints for listing sources and triggering per-source imports (plus a hidden Windows Chrome v20 self-elevating import subcommand).
  • Adds desktop UI for “Settings → Secrets” with per-source sync buttons and a .1pux import flow.

Reviewed changes

Copilot reviewed 27 out of 29 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
scripts/sign-dev-macos.sh Dev-only signing helper for macOS to enable 1Password app-integration via op.
crates/puffer-secrets/src/win_chrome_import.rs Windows self-elevating Chrome v20 import helper flow (UAC → SYSTEM task → result file).
crates/puffer-secrets/src/onepassword.rs 1Password op CLI integration: authorization checks, batching, timeouts, and parsing.
crates/puffer-secrets/src/onepassword_1pux.rs .1pux ZIP export parser/importer with size cap to avoid zip-bomb OOM.
crates/puffer-secrets/src/lib.rs Secret import framework: BrowserSource, availability listing, unified ImportReport, 1Password sync entrypoints.
crates/puffer-secrets/src/keychain.rs Narrows macOS-only constants behind #[cfg(target_os = "macos")].
crates/puffer-secrets/src/firefox.rs Firefox NSS (key4.db + logins.json) decryptor and profile discovery across OSes.
crates/puffer-secrets/src/der.rs Minimal DER reader used by Firefox NSS decryptor.
crates/puffer-secrets/src/chromium.rs Cross-platform Chromium credential extraction (macOS Keychain, Windows DPAPI/ABE hooks, Linux Secret Service/peanuts).
crates/puffer-secrets/src/chrome.rs Removes old macOS-only Chrome extractor (replaced by chromium.rs).
crates/puffer-secrets/Cargo.toml Adds crypto + ZIP deps and Windows-specific windows crate features.
crates/puffer-core/runtime/claude_tools/workflow/request_secret.rs Resolves op://... references at request time for on-demand 1Password resolution.
crates/puffer-cli/src/main.rs Wires hidden __win-chrome-import subcommand dispatch.
crates/puffer-cli/src/desktop_api.rs Adds secret-source availability into SecretsSettingsDto.
crates/puffer-cli/src/desktop_api_types.rs Adds SecretSourceDto for secrets settings payloads.
crates/puffer-cli/src/daemon.rs Adds RPC handlers: import_browser_secrets, list_secret_sources, import_onepassword_export.
crates/puffer-cli/src/daemon_secrets.rs Implements per-source import + Windows helper invocation; adds 1Password export import.
crates/puffer-cli/src/cli_args.rs Defines hidden __win-chrome-import command for Windows v20 import stages.
Cargo.lock Updates lockfile for new deps (zip, des, chacha20poly1305, windows 0.58, etc.).
apps/puffer-desktop/tests/support/fakeDaemon.ts Adds fake responses for new secrets RPCs and source availability.
apps/puffer-desktop/tests/settings-ui.spec.ts Updates UI test to expect import_browser_secrets RPC.
apps/puffer-desktop/src/lib/types.ts Adds SecretSource and updates settings types for sources.
apps/puffer-desktop/src/lib/screens/settings/SecretsSettings.svelte Replaces Chrome-only import UI with per-source buttons + .1pux import file picker.
apps/puffer-desktop/src/lib/mockData.ts Extends mock settings snapshot with secret sources.
apps/puffer-desktop/src/lib/api/desktop.ts Adds desktop API helpers for import_browser_secrets and import_onepassword_export.
apps/puffer-desktop/src-tauri/src/settings_data.rs Includes secret sources in Settings snapshot DTOs (Tauri backend).
apps/puffer-desktop/src-tauri/src/dtos.rs Adds SecretSourceDto to Tauri DTO definitions.
apps/puffer-desktop/src-tauri/src/backend.rs Implements Tauri in-process backend methods for new secrets RPCs.
apps/puffer-desktop/src-tauri/Cargo.lock Updates Tauri lockfile for new deps pulled in via puffer-secrets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/puffer-core/runtime/claude_tools/workflow/request_secret.rs Outdated
Comment thread crates/puffer-secrets/src/win_chrome_import.rs
Comment thread crates/puffer-secrets/src/win_chrome_import.rs
Comment thread crates/puffer-secrets/src/onepassword.rs Outdated
Comment thread crates/puffer-cli/src/daemon_secrets.rs
Comment thread apps/puffer-desktop/src-tauri/src/backend.rs
…ows test build

Address code-review feedback on the credential-import PR (berabuddies#427):

- win_chrome_import: validate the encrypted-blob length in gcm_decrypt before
  slicing, so a short/malformed row returns an error (skipped) instead of
  panicking and aborting the whole SYSTEM-stage import.
- win_chrome_import: clear the helper result file before elevating, so a
  declined UAC prompt can't read a previous run's OK as a fresh success.
- daemon_secrets: the elevated v20 helper now returns (imported, skipped,
  errors) and requires the CHROME_IMPORT_OK marker; imported and skipped are
  replaced together so the Windows Chrome report stays consistent.
- onepassword tests: move the op-shim tests behind #[cfg(unix)] so the crate
  compiles and tests on Windows; the pure parser tests stay cross-platform.
- request_secret: correct a stale comment — imported 1Password values are
  stored as plaintext; only manually-added op:// references resolve on demand.
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