Skip to main content

Module credential

Module credential 

Source
Expand description

credential service — the paired CEF host’s browser-pane Basic-Auth credential broker calls this, host→srv, to resolve an identity for a pane and to look up / save / delete / fill a stored site credential.

Fill is the only method here that ever returns a plaintext password, and it’s called exactly once per approved use, at the moment the host is about to hand the credential to CEF’s AuthCallback::cont(). Lookup deliberately returns only a masked username — the credential-approval window (agentmux-cef, open_subwindow-based, see credential_broker there) uses Lookup to decide what to display, and must never receive the real password even transiently.

Storage itself lives in [agentmux_srv::identity::browser_credential_store] (OS keychain via the existing secret_store.rs wrapper) — this module is just the /agentmux/service RPC surface over it, following the same shape as super::host_ipc (the other host→srv service module).

§Auth: host-only, NOT the shared X-AuthKey

Every method on this service requires args[1] to be this srv instance’s AGENTMUX_HOST_REG_SECRET, the same host-only credential host_ipc.Register proves (see that module’s doc comment and Config::host_reg_secret).

An earlier cut of this module gated on the instance-wide X-AuthKey alone, reasoning that — unlike host_ipc.Register’s “session-hijack-shaped” port+token — every method here only touches data scoped to a caller-supplied identity_id. That reasoning was wrong, and it defeated this feature’s entire stated security goal (reagent P0 on PR #2824). X-AuthKey is deliberately re-injected into every spawned agent’s own environment as AGENTMUX_AUTH_KEY (server/agent_handlers/input.rs), and AGENTMUX_BLOCKID is injected alongside it. So any agent could, with a plain curl from its own Shell tool:

  1. credential.ResolveIdentity with its own known block_id → identity_id
  2. credential.Fill with that identity_id plus the origin/realm it already knows (it is, after all, the thing browsing that site) → the plaintext password

…with no human approval window anywhere in the path. Fill uniquely returns a raw password, which is exactly the class of secret that needed host_ipc.Register’s treatment, not a weaker one.

The gate is applied to the whole service rather than to Fill alone: ResolveIdentity leaks identity_ids, Lookup confirms which sites have saved credentials, and Save/Delete let an agent plant or destroy them. None of those are things an agent should reach either, and a uniform gate cannot be defeated by finding the one method someone forgot to cover.

Note this proves caller is the host, not a human approved this exact use. That is the boundary the threat model needs — the host is the process that runs the approval window, so a capability token it mints for itself would add no defence against the attacker in question (an agent), only an internal consistency check within the host.

Structs§

ProtectionSpaceArgs 🔒
ResolveIdentityArgs 🔒
SaveArgs 🔒

Functions§

handle_credential_service 🔒
handle_delete 🔒
handle_fill 🔒
The one method that returns a real, unmasked password. Called exactly once per approved credential use — see this module’s own doc comment.
handle_lookup 🔒
handle_resolve_identity 🔒
block_id → identity_id, via the same instance-lookup the identity resolver already uses for OAuth env-var injection at spawn time (identity::resolver::inject::…) — reusing Store::instance_get_active_for_block here keeps “which identity owns this pane” resolved in exactly one place in the codebase.
handle_save 🔒
host_caller_allowed 🔒
Some(error) when the caller failed to prove it’s the paired host — checked for EVERY method before dispatch, so a new method added later is gated by construction rather than by remembering to gate it.
mask_username 🔒
us***@example.com / ab*** style masking for the approval window’s display — enough for a human to recognize which saved login is being offered, not enough to be useful if somehow logged or screenshotted wrong. Keeps at most the first 2 characters before any masking.
reject_unless_host_caller 🔒