Skip to main content

tauri_plugin_keyring_store/
store.rs

1//! High-level keyring access for a fixed service name.
2//!
3//! Account strings are deterministic hashes so snapshot paths and binary ids stay short and safe for OS limits.
4//!
5//! ## Example
6//!
7//! ```rust,no_run
8//! use tauri_plugin_keyring_store::KeyringStore;
9//! use tauri_plugin_keyring_store::BytesDto;
10//!
11//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
12//! let store = KeyringStore::new("com.example.app");
13//! let client = BytesDto::Text("my-client".into());
14//! let account = store.account_store_key("/data/main", &client, "settings.json");
15//! store.set_bytes(&account, b"{\"theme\":\"dark\"}")?;
16//! # Ok(())
17//! # }
18//! ```
19
20use std::collections::HashSet;
21use std::sync::{Arc, Mutex};
22
23use keyring_core::Entry;
24use sha2::{Digest, Sha256};
25
26use crate::backend::{ensure_init, map_keyring_err};
27use crate::error::{Error, Result};
28use crate::models::BytesDto;
29
30fn digest16(data: &[u8]) -> String {
31    let mut h = Sha256::new();
32    h.update(data);
33    let out = h.finalize();
34    hex::encode(&out[..8])
35}
36
37/// Managed snapshot sessions (Stronghold-compatible “initialized paths”).
38#[derive(Default, Clone)]
39pub struct SessionRegistry(pub Arc<Mutex<HashSet<String>>>);
40
41impl SessionRegistry {
42    /// Marks `path` as initialized for this process.
43    pub fn insert(&self, path: String) {
44        self.0.lock().expect("session mutex poisoned").insert(path);
45    }
46
47    /// Removes a path; returns whether it was present.
48    pub fn remove(&self, path: &str) -> bool {
49        self.0.lock().expect("session mutex poisoned").remove(path)
50    }
51
52    /// Returns whether `path` is currently tracked.
53    pub fn contains(&self, path: &str) -> bool {
54        self.0
55            .lock()
56            .expect("session mutex poisoned")
57            .contains(path)
58    }
59}
60
61/// OS-backed credential storage scoped to one service identifier (bundle id / custom).
62#[derive(Debug, Clone)]
63pub struct KeyringStore {
64    service: String,
65}
66
67impl KeyringStore {
68    /// Creates a store handle; no I/O until the first read/write (backend registers lazily).
69    pub fn new(service: impl Into<String>) -> Self {
70        Self {
71            service: service.into(),
72        }
73    }
74
75    /// Keyring **service** / namespace string passed to the native backend.
76    pub fn service(&self) -> &str {
77        &self.service
78    }
79
80    fn entry(&self, account: &str) -> Result<Entry> {
81        ensure_init().map_err(Error::Init)?;
82        Entry::new(&self.service, account).map_err(|e| Error::Keyring(e.to_string()))
83    }
84
85    /// Persists a UTF-8 secret (use [`Self::set_bytes`] for arbitrary bytes).
86    pub fn set_password(&self, account: &str, password: &str) -> Result<()> {
87        let entry = self.entry(account)?;
88        entry.set_password(password).map_err(map_keyring_err)
89    }
90
91    /// Encodes `value` as Base64 and stores it via [`Self::set_password`].
92    pub fn set_bytes(&self, account: &str, value: &[u8]) -> Result<()> {
93        let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, value);
94        self.set_password(account, &encoded)
95    }
96
97    /// Returns stored UTF-8, or [`None`] if the entry is missing.
98    pub fn get_password(&self, account: &str) -> Result<Option<String>> {
99        let entry = self.entry(account)?;
100        match entry.get_password() {
101            Ok(p) => Ok(Some(p)),
102            Err(e) => {
103                if matches!(&e, keyring_core::error::Error::NoEntry) {
104                    Ok(None)
105                } else {
106                    Err(map_keyring_err(e))
107                }
108            }
109        }
110    }
111
112    /// Decodes Base64 from [`Self::get_password`]; returns [`None`] if missing.
113    pub fn get_bytes(&self, account: &str) -> Result<Option<Vec<u8>>> {
114        match self.get_password(account)? {
115            None => Ok(None),
116            Some(s) => {
117                let raw =
118                    base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.trim())
119                        .map_err(|e| Error::Encoding(e.to_string()))?;
120                Ok(Some(raw))
121            }
122        }
123    }
124
125    /// Deletes the credential if present; missing entries are treated as success.
126    pub fn delete(&self, account: &str) -> Result<()> {
127        let entry = self.entry(account)?;
128        match entry.delete_credential() {
129            Ok(()) => Ok(()),
130            Err(e) => {
131                if matches!(&e, keyring_core::error::Error::NoEntry) {
132                    Ok(())
133                } else {
134                    Err(map_keyring_err(e))
135                }
136            }
137        }
138    }
139
140    /// `true` if a non-empty password exists for `account`.
141    pub fn exists_nonempty(&self, account: &str) -> Result<bool> {
142        Ok(self
143            .get_password(account)?
144            .map(|v| !v.trim().is_empty())
145            .unwrap_or(false))
146    }
147
148    /// Stable account id for an unstructured secret key under a snapshot + client namespace.
149    ///
150    /// # Example
151    ///
152    /// ```
153    /// use tauri_plugin_keyring_store::{BytesDto, KeyringStore};
154    ///
155    /// let store = KeyringStore::new("com.example.app");
156    /// let client = BytesDto::Text("cli".into());
157    /// let account = store.account_raw("/data/main", &client, "token");
158    /// assert!(account.starts_with("kp:v1:"));
159    /// ```
160    pub fn account_raw(&self, snapshot_path: &str, client: &BytesDto, suffix: &str) -> String {
161        let sd = digest16(snapshot_path.as_bytes());
162        let cd = digest16(client.as_ref());
163        let xd = digest16(suffix.as_bytes());
164        format!("kp:v1:{sd}:{cd}:x:{xd}")
165    }
166
167    /// Account key for a JSON **store record** (`store_key` is a logical filename).
168    pub fn account_store_key(
169        &self,
170        snapshot_path: &str,
171        client: &BytesDto,
172        store_key: &str,
173    ) -> String {
174        let sd = digest16(snapshot_path.as_bytes());
175        let cd = digest16(client.as_ref());
176        let kd = digest16(store_key.as_bytes());
177        format!("kp:v1:{sd}:{cd}:st:{kd}")
178    }
179
180    /// Account key for binary **vault** payload at `vault` / `record_path`.
181    pub fn account_vault_record(
182        &self,
183        snapshot_path: &str,
184        client: &BytesDto,
185        vault: &BytesDto,
186        record_path: &BytesDto,
187    ) -> String {
188        let sd = digest16(snapshot_path.as_bytes());
189        let cd = digest16(client.as_ref());
190        let vd = digest16(vault.as_ref());
191        let rd = digest16(record_path.as_ref());
192        format!("kp:v1:{sd}:{cd}:v:{vd}:{rd}")
193    }
194}