tauri_plugin_keyring_store/
store.rs1use std::collections::HashSet;
21#[cfg(target_os = "ios")]
22use std::collections::HashMap;
23use std::sync::{Arc, Mutex};
24
25use keyring_core::Entry;
26use sha2::{Digest, Sha256};
27
28use crate::backend::{ensure_init, map_keyring_err};
29#[cfg(target_os = "ios")]
30use crate::backend::is_keychain_locked_error;
31use crate::error::{Error, Result};
32use crate::models::BytesDto;
33
34fn digest16(data: &[u8]) -> String {
35 let mut h = Sha256::new();
36 h.update(data);
37 let out = h.finalize();
38 hex::encode(&out[..8])
39}
40
41#[cfg(any(target_os = "ios", target_os = "macos"))]
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum WriteAccessibility {
47 #[default]
49 AfterFirstUnlock,
50 WhenUnlocked,
51 AfterFirstUnlockThisDeviceOnly,
52 WhenUnlockedThisDeviceOnly,
53 WhenPasscodeSetThisDeviceOnly,
54 RequireUserPresence,
55}
56
57#[cfg(any(target_os = "ios", target_os = "macos"))]
58impl WriteAccessibility {
59 pub fn as_access_policy_modifier(self) -> &'static str {
61 match self {
62 Self::AfterFirstUnlock => "after-first-unlock",
63 Self::WhenUnlocked => "when-unlocked",
64 Self::AfterFirstUnlockThisDeviceOnly => "after-first-unlock-this-device-only",
65 Self::WhenUnlockedThisDeviceOnly => "when-unlocked-this-device-only",
66 Self::WhenPasscodeSetThisDeviceOnly => "when-passcode-set-this-device-only",
67 Self::RequireUserPresence => "require-user-presence",
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum KeyringAvailability {
75 Available,
77 Locked,
79}
80
81#[cfg(target_os = "ios")]
82const AVAILABILITY_PROBE_ACCOUNT: &str = "__tauri_keyring_store_availability_probe__";
83
84#[derive(Default, Clone)]
86pub struct SessionRegistry(pub Arc<Mutex<HashSet<String>>>);
87
88impl SessionRegistry {
89 pub fn insert(&self, path: String) {
91 self.0.lock().expect("session mutex poisoned").insert(path);
92 }
93
94 pub fn remove(&self, path: &str) -> bool {
96 self.0.lock().expect("session mutex poisoned").remove(path)
97 }
98
99 pub fn contains(&self, path: &str) -> bool {
101 self.0
102 .lock()
103 .expect("session mutex poisoned")
104 .contains(path)
105 }
106}
107
108#[derive(Debug, Clone)]
110pub struct KeyringStore {
111 service: String,
112 #[cfg(target_os = "ios")]
113 write_accessibility: WriteAccessibility,
114}
115
116impl KeyringStore {
117 pub fn new(service: impl Into<String>) -> Self {
119 Self {
120 service: service.into(),
121 #[cfg(target_os = "ios")]
122 write_accessibility: WriteAccessibility::default(),
123 }
124 }
125
126 #[cfg(target_os = "ios")]
128 pub fn with_write_accessibility(mut self, policy: WriteAccessibility) -> Self {
129 self.write_accessibility = policy;
130 self
131 }
132
133 pub fn service(&self) -> &str {
135 &self.service
136 }
137
138 fn entry(&self, account: &str) -> Result<Entry> {
139 ensure_init().map_err(Error::Init)?;
140 Entry::new(&self.service, account).map_err(|e| Error::Keyring(e.to_string()))
141 }
142
143 fn entry_for_write(&self, account: &str) -> Result<Entry> {
144 #[cfg(target_os = "ios")]
145 {
146 ensure_init().map_err(Error::Init)?;
147 let policy = self.write_accessibility.as_access_policy_modifier();
148 let modifiers = HashMap::from([("access-policy", policy)]);
149 Entry::new_with_modifiers(&self.service, account, &modifiers)
150 .map_err(|e| Error::Keyring(e.to_string()))
151 }
152 #[cfg(not(target_os = "ios"))]
153 {
154 self.entry(account)
155 }
156 }
157
158 pub fn availability(&self) -> KeyringAvailability {
164 #[cfg(target_os = "ios")]
165 {
166 let entry = match self.entry(AVAILABILITY_PROBE_ACCOUNT) {
167 Ok(e) => e,
168 Err(e) => {
169 log::warn!("keyring availability probe: entry creation failed: {e}");
170 return KeyringAvailability::Available;
171 }
172 };
173 match entry.get_password() {
174 Ok(_) | Err(keyring_core::error::Error::NoEntry) => KeyringAvailability::Available,
175 Err(e) if is_keychain_locked_error(&e) => KeyringAvailability::Locked,
176 Err(e) => {
177 log::warn!("keyring availability probe: unexpected error: {e}");
178 KeyringAvailability::Available
179 }
180 }
181 }
182 #[cfg(not(target_os = "ios"))]
183 {
184 KeyringAvailability::Available
185 }
186 }
187
188 pub fn set_password(&self, account: &str, password: &str) -> Result<()> {
190 let entry = self.entry_for_write(account)?;
191 entry.set_password(password).map_err(map_keyring_err)
192 }
193
194 pub fn set_bytes(&self, account: &str, value: &[u8]) -> Result<()> {
196 let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, value);
197 self.set_password(account, &encoded)
198 }
199
200 pub fn get_password(&self, account: &str) -> Result<Option<String>> {
202 let entry = self.entry(account)?;
203 match entry.get_password() {
204 Ok(p) => Ok(Some(p)),
205 Err(e) => {
206 if matches!(&e, keyring_core::error::Error::NoEntry) {
207 Ok(None)
208 } else {
209 Err(map_keyring_err(e))
210 }
211 }
212 }
213 }
214
215 pub fn get_password_for_background(&self, account: &str) -> Result<Option<String>> {
218 if self.availability() == KeyringAvailability::Locked {
219 return Ok(None);
220 }
221 self.get_password(account)
222 }
223
224 pub fn get_bytes(&self, account: &str) -> Result<Option<Vec<u8>>> {
226 match self.get_password(account)? {
227 None => Ok(None),
228 Some(s) => {
229 let raw =
230 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.trim())
231 .map_err(|e| Error::Encoding(e.to_string()))?;
232 Ok(Some(raw))
233 }
234 }
235 }
236
237 pub fn get_bytes_for_background(&self, account: &str) -> Result<Option<Vec<u8>>> {
239 match self.get_password_for_background(account)? {
240 None => Ok(None),
241 Some(s) => {
242 let raw =
243 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.trim())
244 .map_err(|e| Error::Encoding(e.to_string()))?;
245 Ok(Some(raw))
246 }
247 }
248 }
249
250 pub fn delete(&self, account: &str) -> Result<()> {
252 let entry = self.entry(account)?;
253 match entry.delete_credential() {
254 Ok(()) => Ok(()),
255 Err(e) => {
256 if matches!(&e, keyring_core::error::Error::NoEntry) {
257 Ok(())
258 } else {
259 Err(map_keyring_err(e))
260 }
261 }
262 }
263 }
264
265 pub fn exists_nonempty(&self, account: &str) -> Result<bool> {
267 Ok(self
268 .get_password(account)?
269 .map(|v| !v.trim().is_empty())
270 .unwrap_or(false))
271 }
272
273 pub fn exists_nonempty_for_background(&self, account: &str) -> Result<bool> {
275 if self.availability() == KeyringAvailability::Locked {
276 return Ok(false);
277 }
278 self.exists_nonempty(account)
279 }
280
281 pub fn account_raw(&self, snapshot_path: &str, client: &BytesDto, suffix: &str) -> String {
294 let sd = digest16(snapshot_path.as_bytes());
295 let cd = digest16(client.as_ref());
296 let xd = digest16(suffix.as_bytes());
297 format!("kp:v1:{sd}:{cd}:x:{xd}")
298 }
299
300 pub fn account_store_key(
302 &self,
303 snapshot_path: &str,
304 client: &BytesDto,
305 store_key: &str,
306 ) -> String {
307 let sd = digest16(snapshot_path.as_bytes());
308 let cd = digest16(client.as_ref());
309 let kd = digest16(store_key.as_bytes());
310 format!("kp:v1:{sd}:{cd}:st:{kd}")
311 }
312
313 pub fn account_vault_record(
315 &self,
316 snapshot_path: &str,
317 client: &BytesDto,
318 vault: &BytesDto,
319 record_path: &BytesDto,
320 ) -> String {
321 let sd = digest16(snapshot_path.as_bytes());
322 let cd = digest16(client.as_ref());
323 let vd = digest16(vault.as_ref());
324 let rd = digest16(record_path.as_ref());
325 format!("kp:v1:{sd}:{cd}:v:{vd}:{rd}")
326 }
327}