Feat/secrets multi source sync#427
Open
0oyun wants to merge 16 commits into
Open
Conversation
…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>
|
@0oyun is attempting to deploy a commit to the fuzzland Team on Vercel. A member of the Team first needs to authorize it. |
…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.
There was a problem hiding this comment.
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-secretsimport framework for Chrome/Chromium, Firefox, and 1Password (viaopCLI and.1puxexport), producing unifiedImportReports. - 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
.1puximport 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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds credential import to puffer's encrypted
SecretVault, so an agent can sync theuser'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:
(AES-256-GCM) and v20 App-Bound Encryption (per-machine CNG key), Linux Secret
Service. Reads both
Login Dataand the Google-accountLogin Data For Accountstore.key4.db+logins.json(legacy 3DES and modern AES-256-CBC).Windows Hello, no token to hunt for) and a
.1puxexport 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
SecretVault+BrowserSourceframework; Chromium decryptorsfor 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.
import_browser_secrets,import_onepassword_export,list_secret_sources) and the hidden__win-chrome-importself-elevating subcommand.(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
opa delegated session whenthe process that invoked it is code-signed —
cargo buildproduces ad-hoc binariesthat 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.shsigns the daemon + desktop host with a persistentself-signed certificate (re-run after each
cargo build); that dev cert is never shipped.Test Plan
Automated:
cargo test -p puffer-secrets— decryptor,.1puxparser, and op-path unit tests.cargo check -p puffer-cliandcargo check -p corbina.cargo check -p puffer-secrets --target x86_64-pc-windows-gnu(Windows-only paths).npm run check(svelte-check — 0 errors) andvite build.Manually verified end-to-end on real machines:
import; 1Password via the op CLI (Touch ID) and via a
.1puxexport; theapp-installed/CLI-missing path auto-installs the op CLI.
App-Bound Encryption import through the self-elevating UAC helper, plus DPAPI v10/v11
and the Google-account store.