1use crate::SyncError;
11use keyring::Entry;
12use sioul_core::config::Account;
13
14const SERVICE: &str = "sioul";
15
16fn entry(account: &Account) -> Result<Entry, SyncError> {
17 let host = account.host.as_deref().ok_or(SyncError::NoServer)?;
18 let login = account.login().ok_or(SyncError::NoServer)?;
19 Entry::new(SERVICE, &format!("{login} on {host}")).map_err(|e| SyncError::Keyring(e.to_string()))
20}
21
22pub fn save(account: &Account, password: &str) -> Result<(), SyncError> {
24 if testing() {
25 return Ok(());
26 }
27 entry(account)?.set_password(password).map_err(|e| SyncError::Keyring(e.to_string()))
28}
29
30pub(crate) fn testing() -> bool {
37 #[cfg(all(test, windows))]
38 if REAL_KEYRING.with(std::cell::Cell::get) {
39 return false;
40 }
41 #[cfg(feature = "insecure-test-tls")]
42 if std::env::var_os("SIOUL_TEST_PASSWORD").is_some() {
43 return true;
44 }
45 no_keyring()
46}
47
48#[cfg(test)]
49thread_local! {
50 static REAL_KEYRING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
52}
53
54#[cfg(test)]
64pub(crate) fn with_real_keyring<T>(f: impl FnOnce() -> T) -> T {
65 struct Reset;
66 impl Drop for Reset {
67 fn drop(&mut self) {
68 REAL_KEYRING.with(|real| real.set(false));
69 }
70 }
71 REAL_KEYRING.with(|real| real.set(true));
72 let _reset = Reset;
73 f()
74}
75
76#[cfg(feature = "no-keyring")]
78fn no_keyring() -> bool {
79 !std::hint::black_box("SIOUL_TEST_NO_KEYRING: this build keeps nothing in the keyring").is_empty()
80}
81
82#[cfg(not(feature = "no-keyring"))]
83fn no_keyring() -> bool {
84 false
85}
86
87pub fn password(account: &Account) -> Result<String, SyncError> {
92 if let Some(oauth) = crate::sasl::OAuth::of(account) {
93 return oauth.mark(account.login().ok_or(SyncError::NoServer)?);
94 }
95 #[cfg(feature = "insecure-test-tls")]
97 if let Ok(password) = std::env::var("SIOUL_TEST_PASSWORD") {
98 return Ok(password);
99 }
100 if testing() {
101 return Err(SyncError::NoPassword);
102 }
103 match entry(account)?.get_password() {
104 Ok(password) => Ok(password),
105 Err(keyring::Error::NoEntry) => Err(SyncError::NoPassword),
106 Err(e) => Err(SyncError::Keyring(e.to_string())),
107 }
108}
109
110fn passphrase_entry(fingerprint: &str) -> Result<Entry, SyncError> {
111 Entry::new(SERVICE, &format!("OpenPGP key {fingerprint}")).map_err(|e| SyncError::Keyring(e.to_string()))
112}
113
114pub fn pgp_passphrase(fingerprint: &str) -> Option<String> {
116 #[cfg(feature = "insecure-test-tls")]
117 if let Ok(passphrase) = std::env::var("SIOUL_TEST_PASSWORD") {
118 return Some(passphrase);
119 }
120 if cfg!(test) || testing() {
123 return None;
124 }
125 passphrase_entry(fingerprint).ok()?.get_password().ok()
126}
127
128pub fn test_passphrase() -> Option<String> {
131 #[cfg(feature = "insecure-test-tls")]
132 if let Ok(passphrase) = std::env::var("SIOUL_TEST_PASSWORD") {
133 return Some(passphrase);
134 }
135 None
136}
137
138pub fn save_pgp_passphrase(fingerprint: &str, passphrase: &str) -> Result<(), SyncError> {
140 if testing() {
141 return Ok(());
142 }
143 passphrase_entry(fingerprint)?.set_password(passphrase).map_err(|e| SyncError::Keyring(e.to_string()))
144}
145
146pub fn named(name: &str) -> Option<String> {
148 if testing() {
149 return None;
150 }
151 Entry::new(SERVICE, name).ok()?.get_password().ok()
152}
153
154pub fn save_named(name: &str, secret: &str) -> Result<(), SyncError> {
156 if testing() {
157 return Ok(());
158 }
159 Entry::new(SERVICE, name).and_then(|e| e.set_password(secret)).map_err(|e| SyncError::Keyring(e.to_string()))
160}
161
162pub fn forget_named(name: &str) -> Result<(), SyncError> {
164 if testing() {
165 return Ok(());
166 }
167 match Entry::new(SERVICE, name).and_then(|e| e.delete_credential()) {
168 Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
169 Err(e) => Err(SyncError::Keyring(e.to_string())),
170 }
171}
172
173pub const ENTRY_BYTES: usize = 2400;
178
179pub fn named_bytes(name: &str) -> Result<Option<Vec<u8>>, SyncError> {
182 if testing() {
183 return Ok(None);
184 }
185 match Entry::new(SERVICE, name).and_then(|e| e.get_secret()) {
186 Ok(bytes) => Ok(Some(bytes)),
187 Err(keyring::Error::NoEntry) => Ok(None),
188 Err(e) => Err(SyncError::Keyring(e.to_string())),
189 }
190}
191
192pub fn save_named_bytes(name: &str, secret: &[u8]) -> Result<(), SyncError> {
196 if testing() {
197 return Ok(());
198 }
199 if secret.len() > ENTRY_BYTES {
200 return Err(SyncError::Keyring(format!("{name}: {} bytes, more than an entry holds everywhere ({ENTRY_BYTES})", secret.len())));
201 }
202 Entry::new(SERVICE, name).and_then(|e| e.set_secret(secret)).map_err(|e| SyncError::Keyring(e.to_string()))
203}
204
205pub fn forget(account: &Account) -> Result<(), SyncError> {
207 if testing() {
208 return Ok(());
209 }
210 match entry(account)?.delete_credential() {
211 Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
212 Err(e) => Err(SyncError::Keyring(e.to_string())),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
228 fn only_the_windows_test_of_the_keyring_reaches_it_and_only_there() {
229 let built = testing();
230 let inside = with_real_keyring(|| (testing(), std::thread::spawn(testing).join().unwrap()));
231 assert_eq!(inside, (built && !cfg!(windows), built), "on Windows its own thread alone reaches the keyring");
232 assert_eq!(testing(), built, "given back after");
233 }
234}