Skip to main content

azul_layout/managers/
keyring.rs

1//! Keyring manager — cross-platform state for the system-keyring surface
2//! (SUPER_PLAN_2 §4 P4.2).
3//!
4//! Request-driven, mirroring [`crate::managers::biometric`]:
5//!
6//! - A **callback** calls `CallbackInfo::keyring_store/get/delete(...)`,
7//!   which parks a [`KeyringRequest`] in the request channel.
8//! - The dll **layout pass** drains it and dispatches to the platform
9//!   backend (`dll::desktop::extra::keyring`) — Keychain / KeyStore /
10//!   libsecret / CredentialLocker. A biometry-bound `Get` shows the OS
11//!   prompt; the outcome is parked in the result channel.
12//! - The layout pass folds the latest result into the manager via
13//!   [`KeyringManager::set_last_result`]; callbacks read it with
14//!   `CallbackInfo::get_keyring_result()`.
15//!
16//! No platform deps (SUPER_PLAN_2 §0.5); the channels are the same
17//! poison-recovering `Mutex<Vec<_>>` pattern as the geolocation /
18//! biometric managers.
19
20use alloc::vec::Vec;
21
22// `KeyringRequest` / `KeyringResult` live in `azul-core` so they cross the
23// FFI without a cyclic dep on `azul-layout`. Re-exported for the existing
24// `azul_layout::managers::keyring::*` import paths.
25pub use azul_core::keyring::{KeyringRequest, KeyringResult};
26
27/// Cross-platform keyring state. One per `App` — the OS keyring is a
28/// per-process (per-app-identity) store, not per-window.
29#[derive(Debug, Clone, PartialEq, Default)]
30pub struct KeyringManager {
31    /// Outcome of the most recent keyring op, or `None` until the first
32    /// completes. Read by callbacks via `CallbackInfo::get_keyring_result()`.
33    pub last_result: Option<KeyringResult>,
34}
35
36impl KeyringManager {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Most recent keyring outcome, or `None` until the first op resolves.
42    pub fn last_result(&self) -> Option<&KeyringResult> {
43        self.last_result.as_ref()
44    }
45
46    /// Apply the outcome the backend delivered. Returns `true` if it
47    /// differs from the previous one (so the window can be marked dirty to
48    /// re-render the revealed / stored state).
49    pub fn set_last_result(&mut self, result: KeyringResult) -> bool {
50        let changed = self.last_result.as_ref() != Some(&result);
51        self.last_result = Some(result);
52        changed
53    }
54}
55
56// ────────── Request channel (callback → platform backend) ─────────────
57
58static PENDING_REQUESTS: std::sync::Mutex<Vec<KeyringRequest>> =
59    std::sync::Mutex::new(Vec::new());
60
61/// Queue a keyring op from a callback. Drained by the dll layout pass and
62/// dispatched to the native keyring. Thread-safe; poison-recovering.
63pub fn push_keyring_request(request: KeyringRequest) {
64    let mut q = PENDING_REQUESTS.lock().unwrap_or_else(|e| e.into_inner());
65    q.push(request);
66}
67
68/// Drain every queued keyring op, in arrival order. Called once per
69/// layout pass; the dll dispatches each to the platform backend.
70pub fn drain_keyring_requests() -> Vec<KeyringRequest> {
71    let mut q = PENDING_REQUESTS.lock().unwrap_or_else(|e| e.into_inner());
72    core::mem::take(&mut *q)
73}
74
75// ────────── Result channel (platform backend → manager) ───────────────
76
77static PENDING_RESULTS: std::sync::Mutex<Vec<KeyringResult>> =
78    std::sync::Mutex::new(Vec::new());
79
80/// Park a keyring result delivered by a platform backend (in the dll).
81/// Thread-safe; poison-recovering (a biometry-bound `Get` resolves from
82/// the OS prompt's reply on an arbitrary thread).
83pub fn push_keyring_result(result: KeyringResult) {
84    let mut q = PENDING_RESULTS.lock().unwrap_or_else(|e| e.into_inner());
85    q.push(result);
86}
87
88/// Drain every parked keyring result, in arrival order. Called once per
89/// layout pass; the caller applies them via [`KeyringManager::set_last_result`].
90pub fn drain_keyring_results() -> Vec<KeyringResult> {
91    let mut q = PENDING_RESULTS.lock().unwrap_or_else(|e| e.into_inner());
92    core::mem::take(&mut *q)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use azul_css::AzString;
99
100    #[test]
101    fn manager_defaults_to_no_result() {
102        let mgr = KeyringManager::new();
103        assert_eq!(mgr.last_result(), None);
104    }
105
106    #[test]
107    fn set_last_result_returns_change_flag() {
108        let mut mgr = KeyringManager::new();
109        assert!(mgr.set_last_result(KeyringResult::Stored));
110        assert_eq!(mgr.last_result(), Some(&KeyringResult::Stored));
111        // Re-applying the same outcome is not a change.
112        assert!(!mgr.set_last_result(KeyringResult::Stored));
113        // A new outcome is a change.
114        assert!(mgr.set_last_result(KeyringResult::Deleted));
115    }
116
117    #[test]
118    fn result_helpers() {
119        let secret = KeyringResult::Retrieved(AzString::from_const_str("hunter2"));
120        assert_eq!(secret.secret().map(|s| s.as_str()), Some("hunter2"));
121        assert!(secret.is_ok());
122        assert!(KeyringResult::Stored.is_ok());
123        assert!(KeyringResult::Deleted.is_ok());
124        for r in [
125            KeyringResult::NotFound,
126            KeyringResult::Denied,
127            KeyringResult::Unavailable,
128            KeyringResult::Error,
129        ] {
130            assert!(!r.is_ok(), "{:?} must not be ok", r);
131            assert_eq!(r.secret(), None);
132        }
133    }
134
135    #[test]
136    fn requests_round_trip_through_channel() {
137        let _ = drain_keyring_requests();
138
139        push_keyring_request(KeyringRequest::Store {
140            key: AzString::from_const_str("token"),
141            secret: AzString::from_const_str("abc"),
142            require_biometry: true,
143        });
144        push_keyring_request(KeyringRequest::Get {
145            key: AzString::from_const_str("token"),
146        });
147        let drained = drain_keyring_requests();
148        assert_eq!(drained.len(), 2, "both queued requests drain in order");
149        assert!(matches!(drained[0], KeyringRequest::Store { .. }));
150        assert!(matches!(drained[1], KeyringRequest::Get { .. }));
151        assert!(drain_keyring_requests().is_empty());
152    }
153
154    #[test]
155    fn results_round_trip_through_manager() {
156        let _ = drain_keyring_results();
157
158        push_keyring_result(KeyringResult::NotFound);
159        push_keyring_result(KeyringResult::Retrieved(AzString::from_const_str("s"))); // last wins
160        let drained = drain_keyring_results();
161        assert_eq!(drained.len(), 2);
162
163        let mut mgr = KeyringManager::new();
164        for r in drained {
165            mgr.set_last_result(r);
166        }
167        assert_eq!(
168            mgr.last_result().and_then(|r| r.secret()).map(|s| s.as_str()),
169            Some("s"),
170            "the last applied result wins"
171        );
172        assert!(drain_keyring_results().is_empty());
173    }
174}