1use std::{
4 sync::atomic::{AtomicU8, Ordering},
5 time::Duration,
6};
7
8const EXECUTOR_READY: u8 = 0;
9const EXECUTOR_IN_FLIGHT: u8 = 1;
10const EXECUTOR_CIRCUIT_OPEN: u8 = 2;
11static SECRET_STORE_EXECUTOR: SecretStoreExecutor = SecretStoreExecutor::new();
12
13pub const KEYRING_OPERATION_TIMEOUT: Duration = Duration::from_secs(3);
21
22#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
23pub enum SecretStoreError {
24 #[error("OS credential-store {operation} timed out")]
25 Timeout { operation: &'static str },
26 #[error("OS credential-store {operation} worker stopped before returning")]
27 WorkerDisconnected { operation: &'static str },
28 #[error("another OS credential-store operation is already in flight")]
29 OperationInFlight,
30 #[error("OS credential-store circuit is open after an earlier timeout or worker failure")]
31 CircuitOpen,
32 #[error("OS credential-store {operation} failed: {message}")]
33 Backend {
34 operation: &'static str,
35 message: String,
36 },
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum PasswordLookup {
41 Found(String),
42 Empty,
43 NoEntry,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum DeleteOutcome {
48 Deleted,
49 NoEntry,
50}
51
52struct SecretStoreExecutor {
53 state: AtomicU8,
54}
55
56impl SecretStoreExecutor {
57 const fn new() -> Self {
58 Self {
59 state: AtomicU8::new(EXECUTOR_READY),
60 }
61 }
62
63 fn run_with_deadline<T: Send + 'static>(
64 &self,
65 operation_name: &'static str,
66 timeout: Duration,
67 operation: impl FnOnce() -> T + Send + 'static,
68 ) -> Result<T, SecretStoreError> {
69 match self.state.compare_exchange(
70 EXECUTOR_READY,
71 EXECUTOR_IN_FLIGHT,
72 Ordering::AcqRel,
73 Ordering::Acquire,
74 ) {
75 Ok(_) => {}
76 Err(EXECUTOR_IN_FLIGHT) => return Err(SecretStoreError::OperationInFlight),
77 Err(EXECUTOR_CIRCUIT_OPEN) => return Err(SecretStoreError::CircuitOpen),
78 Err(_) => return Err(SecretStoreError::CircuitOpen),
79 }
80
81 let (sender, receiver) = std::sync::mpsc::sync_channel(1);
82 if std::thread::Builder::new()
83 .name(format!("credential-store-{operation_name}"))
84 .spawn(move || {
85 let _ = sender.send(operation());
86 })
87 .is_err()
88 {
89 self.state.store(EXECUTOR_CIRCUIT_OPEN, Ordering::Release);
90 return Err(SecretStoreError::WorkerDisconnected {
91 operation: operation_name,
92 });
93 }
94
95 match receiver.recv_timeout(timeout) {
96 Ok(value) => {
97 self.state.store(EXECUTOR_READY, Ordering::Release);
98 Ok(value)
99 }
100 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
101 self.state.store(EXECUTOR_CIRCUIT_OPEN, Ordering::Release);
102 Err(SecretStoreError::Timeout {
103 operation: operation_name,
104 })
105 }
106 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
107 self.state.store(EXECUTOR_CIRCUIT_OPEN, Ordering::Release);
108 Err(SecretStoreError::WorkerDisconnected {
109 operation: operation_name,
110 })
111 }
112 }
113 }
114}
115
116fn run_with_deadline<T: Send + 'static>(
117 operation_name: &'static str,
118 timeout: Duration,
119 operation: impl FnOnce() -> T + Send + 'static,
120) -> Result<T, SecretStoreError> {
121 SECRET_STORE_EXECUTOR.run_with_deadline(operation_name, timeout, operation)
122}
123
124pub fn read_password(username: &str) -> Result<PasswordLookup, SecretStoreError> {
125 let username = username.to_owned();
126 run_with_deadline("read", KEYRING_OPERATION_TIMEOUT, move || {
127 let entry = keyring::Entry::new(crate::KEYRING_SERVICE, &username).map_err(|error| {
128 SecretStoreError::Backend {
129 operation: "read",
130 message: error.to_string(),
131 }
132 })?;
133 match entry.get_password() {
134 Ok(password) if password.is_empty() => Ok(PasswordLookup::Empty),
135 Ok(password) => Ok(PasswordLookup::Found(password)),
136 Err(keyring::Error::NoEntry) => Ok(PasswordLookup::NoEntry),
137 Err(error) => Err(SecretStoreError::Backend {
138 operation: "read",
139 message: error.to_string(),
140 }),
141 }
142 })?
143}
144
145pub fn set_password(username: &str, password: &str) -> Result<(), SecretStoreError> {
146 let username = username.to_owned();
147 let password = password.to_owned();
148 run_with_deadline("write", KEYRING_OPERATION_TIMEOUT, move || {
149 let entry = keyring::Entry::new(crate::KEYRING_SERVICE, &username).map_err(|error| {
150 SecretStoreError::Backend {
151 operation: "write",
152 message: error.to_string(),
153 }
154 })?;
155 entry
156 .set_password(&password)
157 .map_err(|error| SecretStoreError::Backend {
158 operation: "write",
159 message: error.to_string(),
160 })
161 })?
162}
163
164pub fn delete_credential(username: &str) -> Result<DeleteOutcome, SecretStoreError> {
165 let username = username.to_owned();
166 run_with_deadline("delete", KEYRING_OPERATION_TIMEOUT, move || {
167 let entry = keyring::Entry::new(crate::KEYRING_SERVICE, &username).map_err(|error| {
168 SecretStoreError::Backend {
169 operation: "delete",
170 message: error.to_string(),
171 }
172 })?;
173 match entry.delete_credential() {
174 Ok(()) => Ok(DeleteOutcome::Deleted),
175 Err(keyring::Error::NoEntry) => Ok(DeleteOutcome::NoEntry),
176 Err(error) => Err(SecretStoreError::Backend {
177 operation: "delete",
178 message: error.to_string(),
179 }),
180 }
181 })?
182}
183
184#[cfg(test)]
185mod tests;