Skip to content

Fill password fields: from the Keychain by reference, or agent-supplied text with approval #117

Closed

Description

@iamnbutler

Decision (user, 2026-10-17). Closes #75's gap 2, which unblocks gaps 3 and 4.

Primary: fill from the Keychain by reference.

  • The agent names a Keychain item and never sees the secret.
  • The host reads the item with SecItemCopyMatching, so macOS's own Keychain prompt gates each item.
  • The host fills the item only into an AXSecureTextField in the snapshot. For an internet password, the field's web area URL host must match the item's server.

Fallback: agent-supplied text, with explicit approval.

  • The menu bar app shows a confirmation for each fill, naming the caller, the app and the site.
  • A setting, "Always allow agent-supplied passwords", skips that confirmation. It is off by default.

For both routes:

  • The caller must be approved under The bridge serves any same-user process, including input #24.
  • The secret is never logged, echoed in results, or kept in Recent Activity. Activity records only that a fill happened, with the caller and the target.
  • A fill is completed only on a readback that doesn't reveal the secret, such as the character count. Otherwise it is unknown.

Live spike first (the screen is unlocked now):

  • Does typed input or an AXValue set reach a secure field while secure keyboard entry is on? Test in a native NSSecureTextField and in a Chrome password input.
  • Does SecurityAgent (the admin prompt) accept synthetic input? Type a dummy value, then cancel. Never submit.
  • Are Wi-Fi passwords reachable, given they're in the System keychain?
  • What readback does a secure field expose?

Touch ID stays unsupported.

Activity

  1. iamnbutler commented on Oct 17, 2026

    @iamnbutler
    ContributorAuthor

    Live spike findings (2026-10-17)

    Setup

    1. Delivery into secure fields

    Target Secure keyboard entry desktop_key (window-targeted key events, background) AX value set
    NSSecureTextField, app in background off (secure input is on only while the field is focused in the active app) delivered (field length 0 → 1) refused by Peekaboo policy ("Direct value setting is not allowed for secure text fields") before dispatch
    NSSecureTextField, app frontmost on delivered (0 → 1) refused by policy, as above
    Chrome password input, Chrome in background while another app held secure input on (held by the other app) delivered: length 1 and one input event on the page desktop_type took the AX set route and was refused by policy. It came back as unknown with "incomplete exact-window identity", although nothing was dispatched (a labeling bug).
    Chrome password input, Chrome frontmost on (Chrome's own) delivered: length 2 and two input events not retried
    • Secure keyboard entry doesn't block the host's window-targeted events, in either app, whether foreground or background. It blocks other processes from reading keystrokes, not this delivery.
    • AX set at the OS level is untested. Peekaboo refuses it by policy in several places (BackgroundInputDriver, ActionInputDriver, TypeService, DialogService). The key-event route works everywhere tested, so the fill tool doesn't need AX set.
    • Characters outside the keyboard layout: desktop_key maps letters through the layout. A fill must send each character as a Unicode key event, as web typing already does, so symbols and non-layout characters work. I tested letters only.

    2. SecurityAgent (admin prompt)

    • Trigger: security find-generic-password -D "AirPort network password" -w /Library/Keychains/System.keychain, with stdout sent to /dev/null. It opened "security wants to use the “System” keychain" with User Name, Password, Deny and Allow.
    • Readable: it's an ordinary exact window of SecurityAgent, combined_eligible, with all controls in Accessibility.
    • Input: clicking the password field focused it. One desktop_key "x" was delivered: its value went from "" to one mask character.
    • Cancelled: I clicked Deny. security exited with 128 (cancelled), and no secret was read. Enter and Allow were never sent.
    • Conclusion: SecurityAgent accepts synthetic input from the host. Filling an admin prompt is therefore technically possible, so the policy has to decide it. An admin password is the riskiest thing to fill, so it's worth a separate decision from website passwords.

    3. Wi-Fi passwords (System keychain)

    • /Library/Keychains/System.keychain holds 15 "AirPort network password" items. Their attributes (SSID and so on) can be listed without authentication.
    • Reading an item's data opens the admin prompt above, every time. That's the System keychain ACL, which only airportd may read.
    • SecItemCopyMatching against the data-protection keychain doesn't reach the System keychain; that needs the legacy file-keychain API. A fill by reference for Wi-Fi would therefore cost the user an admin password prompt per read.
    • Recommendation: don't support Wi-Fi by reference in v1. Known networks join on their own. For a new network, use a login-keychain item the user created, or agent-supplied text with approval.
    • The fill tool must never fill a SecurityAgent prompt its own Keychain read raised. That would answer macOS's gate for the user.

    4. Readback that doesn't reveal the secret

    Field AXValue seen by another process Readback
    NSSecureTextField (AppKit) one U+F79A (private-use mask) per character character count
    SecurityAgent one U+F79A per character character count
    Chrome <input type=password> one U+2022 (•) per character character count
    • AXNumberOfCharacters stayed 0 in the AppKit field, so don't use it.
    • Plan: a fill is completed when the field's mask count goes from the value before to that value plus the filled length, after replacing the selection or clearing the field first. Otherwise it's unknown.
    • Secure detection is incomplete today: the inspected role is AXTextField, and the captured attributes carry no subrole. Only the localized roleDescription says "secure text field". The client's secure checks (role == "AXSecureTextField" || subrole == … in read.swift, elements.swift and table.swift) therefore never match.
      • Inspect and read currently pass the mask through, which reveals only the length.
      • The fill tool must identify secure fields host-side by reading AXSubrole directly, and the read paths should do the same.

    Also seen (separate from #117)

    Proposed fill design (for review)

    • Field: only a field whose AXSubrole is AXSecureTextField, checked host-side, in a fresh snapshot.
    • Delivery: Unicode key events, the same window-targeted delivery as desktop_key, but with the text decoded and typed host-side.
    • The secret stays in the host:
      • Keychain route: the host reads the item itself.
      • Agent-supplied route: the text arrives through the client's stdin, the same way insert input does. It's never in argv, logs, results or activity.
    • Readback: the mask count, before and after.
    • SecurityAgent: refused by default. It needs its own decision.
  2. iamnbutler commented on Oct 20, 2026

    @iamnbutler
    ContributorAuthor

    Shipped in 0.14.0 by #124 (desktop_fill_password: Keychain item by reference, or agent-supplied text with a per-fill confirmation). #124 said "Implements" rather than "Closes", so this stayed open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions