use serde::de::DeserializeOwned; use std::collections::HashMap; use tauri::{plugin::PluginApi, AppHandle, Runtime}; use crate::models::*; pub fn init( app: &AppHandle, _api: PluginApi, ) -> crate::Result> { // keyring v4 split the library into `keyring-core` plus a // per-platform credential-store crate. The default store is a // process-wide global that must be installed before the first // `Entry::new` call. `set_default_store` is idempotent — calling // it again on plugin re-init just replaces the previous handle. // We log and swallow errors so a misconfigured keychain doesn't // block plugin init; downstream calls then fail with NoDefaultStore // and the TS layer falls back to the ephemeral store. install_default_keyring_store(); Ok(NativeBridge(app.clone())) } #[cfg(target_os = "macos")] fn install_default_keyring_store() { match apple_native_keyring_store::keychain::Store::new() { Ok(store) => keyring_core::set_default_store(store), Err(err) => eprintln!("[native-bridge] keychain store init failed: {err}"), } } #[cfg(target_os = "windows")] fn install_default_keyring_store() { match windows_native_keyring_store::Store::new() { Ok(store) => keyring_core::set_default_store(store), Err(err) => eprintln!("[native-bridge] credential manager init failed: {err}"), } } #[cfg(target_os = "linux")] fn install_default_keyring_store() { match dbus_secret_service_keyring_store::Store::new() { Ok(store) => keyring_core::set_default_store(store), Err(err) => eprintln!("[native-bridge] secret service init failed: {err}"), } } /// Access to the native-bridge APIs. pub struct NativeBridge(AppHandle); impl NativeBridge { pub fn auth_with_safari(&self, _payload: AuthRequest) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn auth_with_custom_tab(&self, _payload: AuthRequest) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn copy_uri_to_path(&self, _payload: CopyURIRequest) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn save_image_to_gallery( &self, _payload: SaveImageToGalleryRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn use_background_audio(&self, _payload: UseBackgroundAudioRequest) -> crate::Result<()> { Err(crate::Error::UnsupportedPlatformError) } pub fn install_package( &self, _payload: InstallPackageRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn set_system_ui_visibility( &self, _payload: SetSystemUIVisibilityRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn get_status_bar_height(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn get_sys_fonts_list(&self) -> crate::Result { let font_collection = font_enumeration::Collection::new().unwrap(); let mut fonts = HashMap::new(); for font in font_collection.all() { if cfg!(target_os = "windows") { // FIXME: temporarily disable font name with style for windows fonts.insert(font.family_name.clone(), font.family_name.clone()); } else { fonts.insert(font.font_name.clone(), font.family_name.clone()); } } Ok(GetSysFontsListResponse { fonts, error: None }) } pub fn intercept_keys(&self, _payload: InterceptKeysRequest) -> crate::Result<()> { Err(crate::Error::UnsupportedPlatformError) } pub fn lock_screen_orientation( &self, _payload: LockScreenOrientationRequest, ) -> crate::Result<()> { Err(crate::Error::UnsupportedPlatformError) } pub fn iap_is_available(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn iap_initialize( &self, _payload: IAPInitializeRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn iap_fetch_products( &self, _payload: IAPFetchProductsRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn iap_purchase_product( &self, _payload: IAPPurchaseProductRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn iap_restore_purchases(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn get_system_color_scheme(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn get_safe_area_insets(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn get_screen_brightness(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn set_screen_brightness( &self, _payload: SetScreenBrightnessRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn get_external_sdcard_path(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn open_external_url( &self, _payload: OpenExternalUrlRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } /// Desktop has no mobile-style "system dictionary intent" surface; /// macOS's HUD is invoked through a separate top-level Tauri /// command (`show_lookup_popover` in `src/macos/system_dictionary.rs`), /// and Linux/Windows have no native target. Return /// UnsupportedPlatformError here so the TS layer doesn't /// accidentally dispatch through the mobile plugin on desktop. pub fn show_lookup_popover( &self, _payload: ShowLookupPopoverRequest, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn select_directory(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn get_storefront_region_code(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn request_manage_storage_permission( &self, ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } // ── Sync passphrase keychain ──────────────────────────────────────── // // Uses `keyring-core` v1 with a platform-specific credential store // installed in `init()` above: // * macOS → Security framework Keychain (apple-native-keyring-store) // * Windows → Credential Manager (windows-native-keyring-store) // * Linux → Secret Service (dbus-secret-service-keyring-store) // // `service` and `user` form the keychain item identity. Service is // the bundle id; user is a stable string ("default") so multiple // Readest installs on the same machine could coexist with distinct // user values if ever needed. pub fn set_sync_passphrase( &self, payload: SetSyncPassphraseRequest, ) -> crate::Result { match keyring_entry().and_then(|e| e.set_password(&payload.passphrase)) { Ok(()) => Ok(SyncPassphraseResponse { success: true, error: None, }), Err(err) => Ok(SyncPassphraseResponse { success: false, error: Some(err.to_string()), }), } } pub fn get_sync_passphrase(&self) -> crate::Result { match keyring_entry().and_then(|e| e.get_password()) { Ok(passphrase) => Ok(GetSyncPassphraseResponse { passphrase: Some(passphrase), error: None, }), Err(keyring_core::Error::NoEntry) => Ok(GetSyncPassphraseResponse { passphrase: None, error: None, }), Err(err) => Ok(GetSyncPassphraseResponse { passphrase: None, error: Some(err.to_string()), }), } } pub fn clear_sync_passphrase(&self) -> crate::Result { match keyring_entry().and_then(|e| e.delete_credential()) { Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(SyncPassphraseResponse { success: true, error: None, }), Err(err) => Ok(SyncPassphraseResponse { success: false, error: Some(err.to_string()), }), } } pub fn is_sync_keychain_available(&self) -> crate::Result { // Best-effort probe: open an entry handle. Surface the error // string instead of throwing so the TS layer can fall back // to the ephemeral store gracefully. match keyring_entry() { Ok(_) => Ok(SyncKeychainAvailableResponse { available: true, error: None, }), Err(err) => Ok(SyncKeychainAvailableResponse { available: false, error: Some(err.to_string()), }), } } /// Desktop has its own URL-clip path (`src/clip_url.rs` spawns a /// hidden `WebviewWindow` and listens on `127.0.0.1`). The plugin /// branch is mobile-only — if anyone calls into it from desktop, /// surface that mistake instead of silently returning empty HTML. pub fn clip_url(&self, _payload: ClipUrlRequest) -> crate::Result { Err(crate::Error::NativeBridgeError( "clip_url plugin is mobile-only; desktop callers should invoke the top-level command" .to_string(), )) } // ── Keyed secure key-value store ──────────────────────────────────── // // Same keychain backends + fail-loud/fail-soft contract as the sync // passphrase above, but each item gets its own keychain entry keyed by // `key` (the item's `user`/account), so many independent secrets (the // Drive token set, future provider tokens) coexist under one service // without colliding with the passphrase entry (user "default"). pub fn set_secure_item( &self, payload: SetSecureItemRequest, ) -> crate::Result { match keyring_entry_for(&payload.key).and_then(|e| e.set_password(&payload.value)) { Ok(()) => Ok(SecureItemResponse { success: true, error: None, }), Err(err) => Ok(SecureItemResponse { success: false, error: Some(err.to_string()), }), } } pub fn get_secure_item( &self, payload: GetSecureItemRequest, ) -> crate::Result { match keyring_entry_for(&payload.key).and_then(|e| e.get_password()) { Ok(value) => Ok(GetSecureItemResponse { value: Some(value), error: None, }), Err(keyring_core::Error::NoEntry) => Ok(GetSecureItemResponse { value: None, error: None, }), Err(err) => Ok(GetSecureItemResponse { value: None, error: Some(err.to_string()), }), } } pub fn clear_secure_item( &self, payload: GetSecureItemRequest, ) -> crate::Result { match keyring_entry_for(&payload.key).and_then(|e| e.delete_credential()) { Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(SecureItemResponse { success: true, error: None, }), Err(err) => Ok(SecureItemResponse { success: false, error: Some(err.to_string()), }), } } /// E-ink panels exist only on the mobile (Android) side. Desktop has no /// e-ink controller, so this is unsupported here. pub fn refresh_eink_screen(&self) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } pub fn update_reading_widget( &self, _payload: UpdateReadingWidgetRequest, ) -> crate::Result<()> { // Home-screen widgets are mobile-only; desktop is a no-op. Ok(()) } } const KEYRING_SERVICE: &str = "Readest Safe Storage"; const KEYRING_USER: &str = "default"; fn keyring_entry() -> std::result::Result { keyring_core::Entry::new(KEYRING_SERVICE, KEYRING_USER) } /// Keychain entry for a keyed secure item — same service as the passphrase, /// with the caller's `key` as the per-item account so each secret is distinct. fn keyring_entry_for(key: &str) -> std::result::Result { keyring_core::Entry::new(KEYRING_SERVICE, key) }