use std::collections::HashSet;
#[cfg(target_os = "ios")]
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use keyring_core::Entry;
use sha2::{Digest, Sha256};
use crate::backend::{ensure_init, map_keyring_err};
#[cfg(target_os = "ios")]
use crate::backend::is_keychain_locked_error;
use crate::error::{Error, Result};
use crate::models::BytesDto;
fn digest16(data: &[u8]) -> String {
let mut h = Sha256::new();
h.update(data);
let out = h.finalize();
hex::encode(&out[..8])
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WriteAccessibility {
#[default]
AfterFirstUnlock,
WhenUnlocked,
AfterFirstUnlockThisDeviceOnly,
WhenUnlockedThisDeviceOnly,
WhenPasscodeSetThisDeviceOnly,
RequireUserPresence,
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
impl WriteAccessibility {
pub fn as_access_policy_modifier(self) -> &'static str {
match self {
Self::AfterFirstUnlock => "after-first-unlock",
Self::WhenUnlocked => "when-unlocked",
Self::AfterFirstUnlockThisDeviceOnly => "after-first-unlock-this-device-only",
Self::WhenUnlockedThisDeviceOnly => "when-unlocked-this-device-only",
Self::WhenPasscodeSetThisDeviceOnly => "when-passcode-set-this-device-only",
Self::RequireUserPresence => "require-user-presence",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyringAvailability {
Available,
Locked,
}
#[cfg(target_os = "ios")]
const AVAILABILITY_PROBE_ACCOUNT: &str = "__tauri_keyring_store_availability_probe__";
#[derive(Default, Clone)]
pub struct SessionRegistry(pub Arc<Mutex<HashSet<String>>>);
impl SessionRegistry {
pub fn insert(&self, path: String) {
self.0.lock().expect("session mutex poisoned").insert(path);
}
pub fn remove(&self, path: &str) -> bool {
self.0.lock().expect("session mutex poisoned").remove(path)
}
pub fn contains(&self, path: &str) -> bool {
self.0
.lock()
.expect("session mutex poisoned")
.contains(path)
}
}
#[derive(Debug, Clone)]
pub struct KeyringStore {
service: String,
#[cfg(target_os = "ios")]
write_accessibility: WriteAccessibility,
}
impl KeyringStore {
pub fn new(service: impl Into<String>) -> Self {
Self {
service: service.into(),
#[cfg(target_os = "ios")]
write_accessibility: WriteAccessibility::default(),
}
}
#[cfg(target_os = "ios")]
pub fn with_write_accessibility(mut self, policy: WriteAccessibility) -> Self {
self.write_accessibility = policy;
self
}
pub fn service(&self) -> &str {
&self.service
}
fn entry(&self, account: &str) -> Result<Entry> {
ensure_init().map_err(Error::Init)?;
Entry::new(&self.service, account).map_err(|e| Error::Keyring(e.to_string()))
}
fn entry_for_write(&self, account: &str) -> Result<Entry> {
#[cfg(target_os = "ios")]
{
ensure_init().map_err(Error::Init)?;
let policy = self.write_accessibility.as_access_policy_modifier();
let modifiers = HashMap::from([("access-policy", policy)]);
Entry::new_with_modifiers(&self.service, account, &modifiers)
.map_err(|e| Error::Keyring(e.to_string()))
}
#[cfg(not(target_os = "ios"))]
{
self.entry(account)
}
}
pub fn availability(&self) -> KeyringAvailability {
#[cfg(target_os = "ios")]
{
let entry = match self.entry(AVAILABILITY_PROBE_ACCOUNT) {
Ok(e) => e,
Err(e) => {
log::warn!("keyring availability probe: entry creation failed: {e}");
return KeyringAvailability::Available;
}
};
match entry.get_password() {
Ok(_) | Err(keyring_core::error::Error::NoEntry) => KeyringAvailability::Available,
Err(e) if is_keychain_locked_error(&e) => KeyringAvailability::Locked,
Err(e) => {
log::warn!("keyring availability probe: unexpected error: {e}");
KeyringAvailability::Available
}
}
}
#[cfg(not(target_os = "ios"))]
{
KeyringAvailability::Available
}
}
pub fn set_password(&self, account: &str, password: &str) -> Result<()> {
let entry = self.entry_for_write(account)?;
entry.set_password(password).map_err(map_keyring_err)
}
pub fn set_bytes(&self, account: &str, value: &[u8]) -> Result<()> {
let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, value);
self.set_password(account, &encoded)
}
pub fn get_password(&self, account: &str) -> Result<Option<String>> {
let entry = self.entry(account)?;
match entry.get_password() {
Ok(p) => Ok(Some(p)),
Err(e) => {
if matches!(&e, keyring_core::error::Error::NoEntry) {
Ok(None)
} else {
Err(map_keyring_err(e))
}
}
}
}
pub fn get_password_for_background(&self, account: &str) -> Result<Option<String>> {
if self.availability() == KeyringAvailability::Locked {
return Ok(None);
}
self.get_password(account)
}
pub fn get_bytes(&self, account: &str) -> Result<Option<Vec<u8>>> {
match self.get_password(account)? {
None => Ok(None),
Some(s) => {
let raw =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.trim())
.map_err(|e| Error::Encoding(e.to_string()))?;
Ok(Some(raw))
}
}
}
pub fn get_bytes_for_background(&self, account: &str) -> Result<Option<Vec<u8>>> {
match self.get_password_for_background(account)? {
None => Ok(None),
Some(s) => {
let raw =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.trim())
.map_err(|e| Error::Encoding(e.to_string()))?;
Ok(Some(raw))
}
}
}
pub fn delete(&self, account: &str) -> Result<()> {
let entry = self.entry(account)?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(e) => {
if matches!(&e, keyring_core::error::Error::NoEntry) {
Ok(())
} else {
Err(map_keyring_err(e))
}
}
}
}
pub fn exists_nonempty(&self, account: &str) -> Result<bool> {
Ok(self
.get_password(account)?
.map(|v| !v.trim().is_empty())
.unwrap_or(false))
}
pub fn exists_nonempty_for_background(&self, account: &str) -> Result<bool> {
if self.availability() == KeyringAvailability::Locked {
return Ok(false);
}
self.exists_nonempty(account)
}
pub fn account_raw(&self, snapshot_path: &str, client: &BytesDto, suffix: &str) -> String {
let sd = digest16(snapshot_path.as_bytes());
let cd = digest16(client.as_ref());
let xd = digest16(suffix.as_bytes());
format!("kp:v1:{sd}:{cd}:x:{xd}")
}
pub fn account_store_key(
&self,
snapshot_path: &str,
client: &BytesDto,
store_key: &str,
) -> String {
let sd = digest16(snapshot_path.as_bytes());
let cd = digest16(client.as_ref());
let kd = digest16(store_key.as_bytes());
format!("kp:v1:{sd}:{cd}:st:{kd}")
}
pub fn account_vault_record(
&self,
snapshot_path: &str,
client: &BytesDto,
vault: &BytesDto,
record_path: &BytesDto,
) -> String {
let sd = digest16(snapshot_path.as_bytes());
let cd = digest16(client.as_ref());
let vd = digest16(vault.as_ref());
let rd = digest16(record_path.as_ref());
format!("kp:v1:{sd}:{cd}:v:{vd}:{rd}")
}
}