From dcb2daaa872fb4d2952a4ad82e791b163dd6a59a Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Wed, 29 Jan 2025 20:40:06 +0100 Subject: [PATCH] feat(sync): add modules for pairing and syncing clipboard data --- src-tauri/src/main.rs | 71 +++++++++++++-------- src-tauri/src/sync/mod.rs | 2 + src-tauri/src/sync/pairing.rs | 112 ++++++++++++++++++++++++++++++++++ src-tauri/src/sync/sync.rs | 92 ++++++++++++++++++++++++++++ 4 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 src-tauri/src/sync/mod.rs create mode 100644 src-tauri/src/sync/pairing.rs create mode 100644 src-tauri/src/sync/sync.rs diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 179500c..6c9f1c1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,6 +3,7 @@ mod api; mod db; mod utils; +mod sync; use sqlx::sqlite::SqlitePoolOptions; use std::fs; @@ -10,11 +11,13 @@ use tauri::Manager; use tauri_plugin_aptabase::{ EventTracker, InitOptions }; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_prevent_default::Flags; +use sync::sync::ClipboardSync; +use sync::pairing::PairingManager; +use tokio::sync::Mutex; +use std::sync::Arc; -fn main() { - let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); - let _guard = runtime.enter(); - +#[tokio::main] +async fn main() { tauri::Builder ::default() .plugin(tauri_plugin_clipboard::init()) @@ -69,38 +72,52 @@ fn main() { } let db_url = format!("sqlite:{}", db_path.to_str().unwrap()); - let app_handle = app.handle().clone(); - let app_handle_clone = app_handle.clone(); - tauri::async_runtime::spawn(async move { - let pool = SqlitePoolOptions::new() - .max_connections(5) - .connect(&db_url).await - .expect("Failed to create pool"); + // Create the pool in a separate tokio runtime + let pool = tokio::runtime::Runtime + ::new() + .unwrap() + .block_on(async { + SqlitePoolOptions::new() + .max_connections(5) + .connect(&db_url).await + .expect("Failed to create pool") + }); - app_handle_clone.manage(pool); - }); + app_handle.manage(pool); let main_window = app.get_webview_window("main"); - let _ = db::database::setup(app); + db::database::setup(app).expect("Failed to setup database"); api::hotkeys::setup(app_handle.clone()); - api::tray::setup(app)?; - api::clipboard::setup(app.handle()); - let _ = api::clipboard::start_monitor(app_handle.clone()); + api::tray::setup(app).expect("Failed to setup tray"); + api::clipboard::setup(&app_handle); + api::clipboard::start_monitor(app_handle.clone()).expect("Failed to start monitor"); + + let pairing_manager = PairingManager::new(); + let encryption_key = pairing_manager.get_encryption_key().clone(); + let nonce = pairing_manager.get_nonce().clone(); + app_handle.manage(pairing_manager); + + let clipboard_sync = ClipboardSync::new(&encryption_key, &nonce); + let clipboard_sync_arc = Arc::new(Mutex::new(clipboard_sync)); + app_handle.manage(clipboard_sync_arc.clone()); + + let clipboard_sync_clone = clipboard_sync_arc.clone(); + let app_handle_clone = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let sync = clipboard_sync_clone.lock().await; + sync.listen_webhook(app_handle_clone, clipboard_sync_clone).await; + }); utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap()); - main_window + let _ = main_window .as_ref() .map(|w| w.hide()) - .unwrap_or(Ok(()))?; + .expect("Failed to hide window"); - let _ = app.track_event("app_started", None); - - tauri::async_runtime::spawn(async move { - api::updater::check_for_updates(app_handle, false).await; - }); + app.track_event("app_started", None).expect("Failed to track event"); Ok(()) }) @@ -124,7 +141,11 @@ fn main() { db::history::read_image, db::settings::get_setting, db::settings::save_setting, - utils::commands::fetch_page_meta + utils::commands::fetch_page_meta, + sync::pairing::initiate_pairing, + sync::pairing::complete_pairing, + sync::sync::send_clipboard_data, + sync::sync::receive_clipboard_data ] ) .run(tauri::generate_context!()) diff --git a/src-tauri/src/sync/mod.rs b/src-tauri/src/sync/mod.rs new file mode 100644 index 0000000..7f92a9c --- /dev/null +++ b/src-tauri/src/sync/mod.rs @@ -0,0 +1,2 @@ +pub mod pairing; +pub mod sync; \ No newline at end of file diff --git a/src-tauri/src/sync/pairing.rs b/src-tauri/src/sync/pairing.rs new file mode 100644 index 0000000..2c16f43 --- /dev/null +++ b/src-tauri/src/sync/pairing.rs @@ -0,0 +1,112 @@ +use aes_gcm::{ Aes256Gcm, KeyInit }; +use aes_gcm::aead::Aead; +use base64::{ engine::general_purpose::STANDARD, Engine }; +use rand::{ Rng, thread_rng }; +use rand::seq::SliceRandom; +use serde::{ Deserialize, Serialize }; +use tauri::State; +use uuid::Uuid; + +const EMOJI_POOL: &[&str] = &["😀", "😁", "😂", "🤣", "😃", "😄", "😅", "😆", "😉", "😊"]; +const PAIRING_KEY_LENGTH: usize = 4; + +#[derive(Serialize, Deserialize)] +pub struct PairingRequest { + pub inviter_id: String, + pub invitation_code: String, +} + +pub struct PairingManager { + pairing_key: String, + encryption_key: [u8; 32], + nonce: [u8; 12], +} + +impl PairingManager { + pub fn new() -> Self { + let mut rng = thread_rng(); + let pairing_key = Self::generate_emoji_sequence(&mut rng); + let encryption_key: [u8; 32] = rng.gen(); + let nonce: [u8; 12] = rng.gen(); + PairingManager { + pairing_key, + encryption_key, + nonce, + } + } + + pub fn generate_emoji_sequence(rng: &mut impl Rng) -> String { + let key: Vec<&str> = EMOJI_POOL.choose_multiple(rng, PAIRING_KEY_LENGTH).cloned().collect(); + key.join(" ") + } + + pub fn validate_pairing(&self, input_key: &str) -> bool { + self.pairing_key == input_key + } + + pub fn get_encryption_key(&self) -> &[u8; 32] { + &self.encryption_key + } + + pub fn get_nonce(&self) -> &[u8; 12] { + &self.nonce + } + + pub fn generate_invitation_code(&self) -> String { + Uuid::new_v4().to_string() + } + + pub fn encrypt_key(&self, key: &[u8; 32]) -> Result { + let cipher = Aes256Gcm::new(&self.encryption_key.into()); + let ciphertext = cipher + .encrypt(&self.nonce.into(), key.as_ref()) + .map_err(|e| e.to_string())?; + Ok(STANDARD.encode(ciphertext)) + } + + pub fn decrypt_key(&self, encrypted_key: &str) -> Result<[u8; 32], String> { + let ciphertext = STANDARD.decode(encrypted_key).map_err(|e| e.to_string())?; + let cipher = Aes256Gcm::new(&self.encryption_key.into()); + let plaintext = cipher + .decrypt(&self.nonce.into(), ciphertext.as_ref()) + .map_err(|e| e.to_string())?; + let mut key = [0u8; 32]; + key.copy_from_slice(&plaintext); + Ok(key) + } + + pub fn create_pairing_request(&self, inviter_id: String) -> PairingRequest { + PairingRequest { + inviter_id, + invitation_code: self.generate_invitation_code(), + } + } + + pub fn handle_pairing_response(&self, response: PairingRequest) -> bool { + self.validate_pairing(&response.invitation_code) + } +} + +#[tauri::command] +pub fn initiate_pairing(_pairing_manager: State<'_, PairingManager>) -> String { + let mut rng = thread_rng(); + PairingManager::generate_emoji_sequence(&mut rng) +} + +#[tauri::command] +pub fn complete_pairing( + input_key: String, + pairing_manager: State<'_, PairingManager> +) -> Result { + if pairing_manager.validate_pairing(&input_key) { + let _shared_key = pairing_manager.encryption_key.to_vec(); + Ok("Pairing successful".to_string()) + } else { + Err("Invalid pairing key".to_string()) + } +} + +#[tauri::command] +pub fn generate_invitation(pairing_manager: State<'_, PairingManager>) -> String { + pairing_manager.generate_invitation_code() +} diff --git a/src-tauri/src/sync/sync.rs b/src-tauri/src/sync/sync.rs new file mode 100644 index 0000000..681c291 --- /dev/null +++ b/src-tauri/src/sync/sync.rs @@ -0,0 +1,92 @@ +use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; +use aes_gcm::aead::Aead; +use base64::{engine::general_purpose::STANDARD, Engine}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Emitter}; +use tokio::sync::Mutex; +use std::sync::Arc; +use typenum::U12; + +const KVS_URL: &str = "https://kvs.wireway.ch"; + +#[derive(Serialize, Deserialize, Clone)] +pub struct ClipData { + content: String, + content_type: String, + timestamp: u64, +} + +#[derive(Clone)] +pub struct ClipboardSync { + client: Client, + cipher: Aes256Gcm, + nonce: Nonce, +} + +impl ClipboardSync { + pub fn new(encryption_key: &[u8; 32], nonce_bytes: &[u8; 12]) -> Self { + let cipher = Aes256Gcm::new(encryption_key.into()); + let nonce = Nonce::from_slice(nonce_bytes).clone(); + ClipboardSync { + client: Client::new(), + cipher, + nonce, + } + } + + pub async fn send_clipboard(&self, clip: ClipData) -> Result<(), String> { + let plaintext = serde_json::to_string(&clip).map_err(|e| e.to_string())?; + let ciphertext = self.cipher.encrypt(&self.nonce, plaintext.as_bytes()).map_err(|e| e.to_string())?; + let encoded = STANDARD.encode(ciphertext); + self.client + .post(&format!("{}/clipboard", KVS_URL)) + .json(&serde_json::json!({ + "key": "clipboard", + "value": encoded, + "expires_in": 60 + })) + .send() + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + + pub async fn receive_clipboard(&self, app_handle: AppHandle) -> Result<(), String> { + let res = self.client.get(&format!("{}/clipboard", KVS_URL)).send().await.map_err(|e| e.to_string())?; + if res.status().is_success() { + let json: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + if let Some(encoded) = json["value"].as_str() { + let ciphertext = STANDARD.decode(encoded).map_err(|e| e.to_string())?; + let plaintext = self.cipher.decrypt(&self.nonce, ciphertext.as_ref()).map_err(|e| e.to_string())?; + let clip_str = String::from_utf8(plaintext).map_err(|e| e.to_string())?; + let clip: ClipData = serde_json::from_str(&clip_str).map_err(|e| e.to_string())?; + app_handle.emit("clipboard-update", clip).map_err(|e| e.to_string())?; + } + } + Ok(()) + } + + pub async fn listen_webhook(&self, app_handle: AppHandle, state: Arc>) { + tokio::spawn(async move { + loop { + if let Err(_) = state.lock().await.receive_clipboard(app_handle.clone()).await { + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + } + tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + } + }); + } +} + +#[tauri::command] +pub async fn send_clipboard_data(clip: ClipData, sync: tauri::State<'_, Arc>>) -> Result<(), String> { + let sync = sync.lock().await; + sync.send_clipboard(clip).await +} + +#[tauri::command] +pub async fn receive_clipboard_data(app_handle: AppHandle, sync: tauri::State<'_, Arc>>) -> Result<(), String> { + let sync = sync.lock().await; + sync.receive_clipboard(app_handle).await +}