diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4ab9123..886a73e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -873,6 +873,27 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "dbus" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "zeroize", +] + [[package]] name = "deranged" version = "0.4.0" @@ -2051,6 +2072,21 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.3.0", + "windows-sys 0.60.2", + "zeroize", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -2099,6 +2135,15 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libdbus-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2242,8 +2287,10 @@ dependencies = [ "cpal", "dasp", "futures-util", + "keyring", "serde", "serde_json", + "strum", "tauri", "tauri-build", "tauri-plugin-opener", @@ -2288,7 +2335,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -3495,6 +3542,19 @@ dependencies = [ "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags 2.9.3", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.14.0" @@ -3859,6 +3919,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "swift-rs" version = "1.0.7" @@ -5675,6 +5756,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "zerotrie" version = "0.2.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 54ec28d..ae7e95b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,6 +28,8 @@ tungstenite = "0.27.0" futures-util = "0.3.31" tokio = "1.47.1" dasp = { version = "0.11.0", features = ["all"] } +keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "sync-secret-service"] } +strum = {version = "0.27.2", features = ["derive"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-positioner = "2" diff --git a/src-tauri/src/cartesia/client.rs b/src-tauri/src/cartesia/client.rs index 890867f..29ce152 100644 --- a/src-tauri/src/cartesia/client.rs +++ b/src-tauri/src/cartesia/client.rs @@ -1,24 +1,23 @@ use std::sync::Arc; -use tauri::{async_runtime::RwLock, Url}; +use tauri::{http::HeaderValue, Url}; use tokio::net::TcpStream; use tokio_tungstenite::{ connect_async, tungstenite::client::IntoClientRequest, MaybeTlsStream, WebSocketStream, }; +use crate::secrets::{SecretName, SecretsManager}; + #[derive(Clone)] pub struct CartesiaClient { - api_key: Arc>, + secrets_manager: Arc, } impl CartesiaClient { - pub fn new(api_key: String) -> Self { - Self { - api_key: Arc::new(RwLock::new(api_key)), - } + pub fn new(secrets_manager: Arc) -> Self { + Self { secrets_manager } } pub async fn open_stt_connection(&self) -> WebSocketStream> { - let api_key = self.api_key.read().await.clone(); let mut request = Url::parse_with_params( "wss://api.cartesia.ai/stt/websocket", &[ @@ -33,8 +32,15 @@ impl CartesiaClient { .expect("failed to instantiate STT WebSocket request"); let headers = request.headers_mut(); + let api_key = self + .secrets_manager + .get_secret(SecretName::CartesiaApiKey) + .expect("failed to retrieve API key"); - headers.insert("X-API-Key", api_key.parse().unwrap()); + headers.insert( + "X-API-Key", + HeaderValue::from_str(api_key.as_str()).expect("could not convert key to header value"), + ); headers.insert("Cartesia-Version", "2025-04-16".parse().unwrap()); let (stream, _) = connect_async(request) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d529654..1ccbe87 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,11 +1,11 @@ use crate::state::AppState; -use tauri::async_runtime::Mutex; use tauri::{Manager, WebviewUrl, WebviewWindowBuilder}; use tauri_plugin_positioner::{Position, WindowExt}; use tauri_plugin_window_state::{StateFlags, WindowExt as StateWindowExt}; mod cartesia; mod devices; +mod secrets; mod state; // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ @@ -67,6 +67,9 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ cartesia::commands::start_stt, cartesia::commands::stop_stt, + secrets::commands::has_secret, + secrets::commands::set_secret, + secrets::commands::delete_secret, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/secrets/commands.rs b/src-tauri/src/secrets/commands.rs new file mode 100644 index 0000000..cc8e118 --- /dev/null +++ b/src-tauri/src/secrets/commands.rs @@ -0,0 +1,61 @@ +use crate::{secrets::SecretName, state::AppState}; +use std::str::FromStr; + +#[tauri::command] +pub async fn has_secret(state: tauri::State<'_, AppState>, name: String) -> Result { + match SecretName::from_str(&name) { + Ok(name) => { + let res = state + .secrets_manager + .has_secret(name) + .expect("failed to check credential existence"); + + Ok(res) + } + Err(err) => { + eprintln!("invalid credential name: {}", err); + + Ok(false) + } + } +} + +#[tauri::command] +pub fn set_secret( + state: tauri::State<'_, AppState>, + name: String, + value: String, +) -> Result<(), ()> { + match SecretName::from_str(&name) { + Ok(name) => { + state + .secrets_manager + .set_secret(name, value) + .expect("failed to set credential"); + + Ok(()) + } + Err(err) => { + eprintln!("failed to decode secret name: {}", err); + Err(()) + } + } +} + +#[tauri::command] +pub fn delete_secret(state: tauri::State<'_, AppState>, name: String) -> Result<(), ()> { + match SecretName::from_str(&name) { + Ok(name) => { + state + .secrets_manager + .delete_secret(name) + .expect("failed to delete credential"); + + Ok(()) + } + Err(err) => { + eprintln!("failed to decode secret name: {}", err); + Err(()) + } + } +} diff --git a/src-tauri/src/secrets/mod.rs b/src-tauri/src/secrets/mod.rs new file mode 100644 index 0000000..a9f380d --- /dev/null +++ b/src-tauri/src/secrets/mod.rs @@ -0,0 +1,46 @@ +use keyring::{Entry, Error}; +use strum::{Display, EnumString}; + +pub mod commands; + +#[derive(Debug, PartialEq, Display, EnumString)] +pub enum SecretName { + #[strum(to_string = "cartesia_api_key")] + CartesiaApiKey, +} + +const SERVICE_NAME: &'static str = "miwiwi"; + +pub struct SecretsManager {} +impl SecretsManager { + pub fn new() -> Self { + Self {} + } + + pub fn get_secret(&self, name: SecretName) -> Result { + let entry = Entry::new(SERVICE_NAME, &name.to_string())?; + let cred = entry.get_password()?; + + Ok(cred) + } + + pub fn set_secret(&self, name: SecretName, value: String) -> Result<(), Error> { + let entry = Entry::new(SERVICE_NAME, &name.to_string())?; + + entry.set_password(&value) + } + + pub fn delete_secret(&self, name: SecretName) -> Result<(), Error> { + let entry = Entry::new(SERVICE_NAME, &name.to_string())?; + + entry.delete_credential() + } + + pub fn has_secret(&self, name: SecretName) -> Result { + match self.get_secret(name) { + Ok(_) => Ok(true), + Err(Error::NoEntry) => Ok(false), + Err(err) => Err(err), + } + } +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 660e40a..84bb02d 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -3,11 +3,13 @@ use std::sync::Arc; use crate::{ cartesia::{client::CartesiaClient, stt::SttManager}, devices::{input::InputDeviceManager, output::OutputDeviceManager, types::AudioDeviceError}, + secrets::SecretsManager, }; pub struct AppState { pub cartesia_client: Arc, pub stt_manager: Arc, + pub secrets_manager: Arc, pub input_device_manager: Arc, pub output_device_manager: Arc, } @@ -16,8 +18,8 @@ impl AppState { pub fn new() -> Result { let input_device_manager = Arc::new(InputDeviceManager::new()?); let output_device_manager = Arc::new(OutputDeviceManager::new()?); - - let cartesia_client = Arc::new(CartesiaClient::new("TODO".into())); + let secrets_manager = Arc::new(SecretsManager::new()); + let cartesia_client = Arc::new(CartesiaClient::new(secrets_manager.clone())); let stt_manager = Arc::new(SttManager::new( cartesia_client.clone(), input_device_manager.clone(), @@ -26,6 +28,7 @@ impl AppState { Ok(AppState { input_device_manager, output_device_manager, + secrets_manager, cartesia_client, stt_manager, }) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7758e3d..9840704 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,8 +1,35 @@ +{#await keyInvocation} +

checking for cred...

+{:then hasKey} + {#if hasKey} +

key has been set

+ + {:else} +

no key yet

+ + + {/if} +{:catch err} +

{err}

+{/await} +
miwiwi
- - + +