Skip to content

Add session-scoped Touch ID unlock on macOS - #374

Open
m11y wants to merge 1 commit into
doy:mainfrom
m11y:feat/macos-secure-enclave-unlock
Open

m11y wants to merge 1 commit into
doy:mainfrom
m11y:feat/macos-secure-enclave-unlock

Conversation

@m11y

@m11y m11y commented Sep 3, 2026 •

Copy link
Copy Markdown

Summary

Add optional, session-scoped Touch ID unlock on macOS using an ephemeral
Secure Enclave P-256 key.

After the master password unlocks the vault once in an rbw-agent process,
the agent can wrap the 64-byte user/vault key for a non-exportable Secure
Enclave key. A later timeout or rbw lock drops the plaintext vault and
organization keys as before; the next unlock asks the Secure Enclave to unwrap
the cached key, which makes macOS require Touch ID.

Enable it with:

rbw config set touch_id_unlock true

The setting defaults to false and is rejected when enabled on non-macOS
platforms.

Security model

  • The master password is never cached.
  • The Secure Enclave private key is non-exportable and non-persistent.
  • The wrapped vault key is held only in memory, never written to disk or the
    Keychain.
  • Both disappear when rbw-agent exits, so the first unlock after reboot or
    rbw stop-agent still requires the master password.
  • BiometryCurrentSet invalidates the key if enrolled fingerprints change.
  • A server logout notification or a changed protected_key revokes the
    biometric session. The unlock completion checks that the same session is
    still current before restoring keys, covering concurrent revocation.
  • Entries with master-password reprompt still use the existing master-password
    path.

The Secure Enclave operation runs in a private helper child on its main thread.
The helper has no listening socket and communicates only with its parent over
anonymous pipes. This is necessary because macOS did not reliably present the
authentication UI when Security framework was called from the daemon's Tokio
blocking worker.

Before the parent sends the vault key, the helper must successfully disable
debugger attachment and core dumps and acknowledge that hardened state. Helper
initialization runs off the Tokio worker threads and both handshake phases have
a bounded timeout. The child receives the vault key once to wrap it, then
retains only the Secure Enclave key handle and ciphertext. Plaintext returned
by Security framework is moved immediately into locked memory, its unavoidable
pageable copy is zeroized, and helper pipe I/O bypasses stdio buffering.

Scope and related PRs

This is deliberately narrower than #308: there is no PIN, configurable backend,
persistent state, or new client-agent protocol. It directly addresses the
runtime-only mode suggested by the maintainer in
#308 (comment), while moving the
unlock operation itself into the Secure Enclave as discussed in the follow-up
comment.

It also differs from draft #355: it has no Bitwarden Desktop dependency and
does not use the Desktop browser-integration protocol. The config key is named
touch_id_unlock so the two approaches do not claim the same generic setting.

During cancellation testing I reproduced the pre-existing disconnected-client
panic fixed independently by #332; that unrelated change is not included here.

Verification

  • cargo build --all-targets --all-features
  • cargo check --all-targets --all-features
  • cargo test --all-features
  • cargo test --no-default-features
  • RUSTDOCFLAGS=-Dwarnings cargo doc --all-features --no-deps
  • cargo fmt --check
  • git diff --check
  • strict Clippy passes after allowing only the five pre-existing Rust 1.98 lint
    categories tracked by Fix clippy 1.98 lints #373
  • manual macOS test on an M1 Max using a fully synthetic rbw profile:
    master-password unlock -> rbw lock -> Touch ID unlock -> unlocked state
  • manual Cancel test returned Touch ID was canceled without invoking pinentry;
    unified logging confirmed com.apple.LocalAuthentication code -2
  • killing the helper caused a safe master-password fallback and replacement
    helper creation
  • exact-label Keychain query while the helper was alive found no persistent
    session key
  • verified rbw stop-agent destroys the helper and the next agent process
    requires the master password again
  • direct random 64-byte helper ECIES round trip through anonymous pipes

cargo deny was not available locally. This patch adds no new package to
Cargo.lock: it only makes the already resolved security-framework 3.5.1
package a direct macOS dependency.

@m11y
m11y force-pushed the feat/macos-secure-enclave-unlock branch 2 times, most recently from a7cde29 to 8762fc7 Compare September 3, 2026 13:37
@m11y m11y mentioned this pull request Sep 4, 2026
3 of 6 tasks
@m11y
m11y force-pushed the feat/macos-secure-enclave-unlock branch from 8762fc7 to 57915ee Compare September 4, 2026 06:06
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.

1 participant