1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//! OS credential store for vault passwords (biometric / keyring unlock).
//!
//! On **macOS**, passwords are stored with
//! [`SecAccessControl`](https://developer.apple.com/documentation/security/secaccesscontrol)
//! **BiometryCurrentSet** (`kSecAccessControlBiometryCurrentSet`) constraints so **Touch ID** is
//! required when reading the secret and the credential is invalidated when the biometric enrollment
//! changes (new finger enrolled). This is strictly stronger than the former `USER_PRESENCE` flag
//! (Tranche 2 upgrade, E4.2, 2026-05-16 — see ADR-008 implementation note).
//!
//! If the OS returns a **missing entitlement** error (typical for unsigned `cargo install` / dev
//! binaries), [`store_password`](crate::keyring_store::store_password) falls back to the same [`keyring`](https://docs.rs/keyring)-based
//! login keychain storage as other platforms so setup commands still succeed.
//!
//! On other platforms, behavior is unchanged ([`keyring`](https://docs.rs/keyring)).
//!
//! **macOS:** [`quick_unlock_storage_note`](crate::keyring_store::quick_unlock_storage_note) classifies
//! whether the credential appears to live in the data-protection keychain vs the legacy login-keychain
//! path (for `doctor` / `biometric status` UX).
use generic as imp;
use macos as imp;
/// Store the vault password in the OS credential store.
/// Retrieve the vault password from the OS credential store.
/// Returns `None` if no entry exists.
/// Remove the vault password from the OS credential store.
/// Check if a keyring entry exists for this profile.
///
/// Cost varies by platform — callers should treat this as **rare-path only**, not hot-loop:
///
/// - **macOS** — cheap probe via `kSecUseAuthenticationUISkip`; will not prompt for Touch ID
/// or any biometric UI even when the entry has a user-presence ACL. Safe to call freely.
/// - **Windows / Linux** — performs a full `keyring::Entry::get_password()` round-trip and
/// discards the bytes. No prompt fires on Windows Credential Manager, and Linux
/// Secret Service typically prompts only once at session start to unlock the keyring
/// (not per-read). Treat as a real OS round-trip — appropriate for `tsafe doctor`,
/// `tsafe biometric status`, and onboarding guards, but **not** appropriate for
/// render loops or hot paths.
///
/// Pairing this with [`retrieve_password`] back-to-back is wasteful — `retrieve_password`
/// already returns `Ok(None)` for missing entries. Call only one of the two in a single
/// code path. See `docs/keyring-audit.md` for the call-site survey.
/// Extra context for CLI (`biometric status`, `doctor`) about **macOS** quick-unlock storage tier.
///
/// On non-macOS, always returns `None`. On macOS, returns `None` when quick unlock is not
/// configured.
/// Classify a keyring/Crypto error as a stale-credential signal.
///
/// A "stale credential" means the credential store entry exists but the password
/// it contains no longer opens the vault — typically because the vault password
/// was rotated or (on macOS) a new fingerprint was enrolled after `biometric enable`.
///
/// This helper checks the *error message text* returned from a failed vault open
/// that used the retrieved keyring password. It does **not** inspect the keyring
/// read itself (the read may succeed; the stale credential is only discovered when
/// the returned password fails decryption and the decrypt error text indicates a
/// credential mismatch rather than a corrupted vault).
///
/// Current detection heuristics (conservative — may miss platform-specific variants):
/// - `"wrong password"` — from `SafeError::DecryptionFailed` display
/// - `"decryption failed"` — same
/// - `"credential"` + `"changed"` or `"stale"` — OS-level messages that may
/// surface on some keychain/libsecret backends
///
/// Callers that use `retrieve_password` followed by `Vault::open` and see
/// `SafeError::DecryptionFailed` SHOULD call this to decide whether to emit a
/// `StaleBiometricCredential` error rather than a generic decryption failure.
/// Internal account used only to verify the OS credential store accepts writes and deletes.
const PROBE_ACCOUNT: &str = "tsafekeyringprobe";
/// Returns `Ok(())` if the OS credential store can store and remove a credential.
///
/// Use this before offering interactive “quick unlock” setup (e.g. after `tsafe init`). This does
/// not prompt for biometrics. Fails on headless/SSH sessions or when no credential backend exists.