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 1/2] 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 +} From 78b962417223fd4141f43be5ff92694e7a60b4d8 Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Wed, 29 Jan 2025 20:40:32 +0100 Subject: [PATCH 2/2] feat: add new dependencies for AES encryption and decryption --- .gitignore | 1 + src-tauri/Cargo.lock | 104 +++++++++++++++++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 2 + 3 files changed, 107 insertions(+) diff --git a/.gitignore b/.gitignore index c000981..987af15 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ logs bun.lockb .gitignore .vscode +.aider* diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f4215d8..c220407 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -31,6 +31,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -714,6 +749,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1080,6 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1120,6 +1166,15 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.10" @@ -1910,6 +1965,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gif" version = "0.13.1" @@ -2612,6 +2677,15 @@ dependencies = [ "configparser", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -3536,6 +3610,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.66" @@ -3926,6 +4006,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -4048,6 +4140,7 @@ name = "qopy" version = "0.4.0" dependencies = [ "active-win-pos-rs", + "aes-gcm", "applications", "base64 0.22.1", "chrono", @@ -4080,6 +4173,7 @@ dependencies = [ "tauri-plugin-updater", "time", "tokio", + "typenum", "url", "uuid", ] @@ -6394,6 +6488,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3d71bff..c9b27dc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -49,6 +49,8 @@ include_dir = "0.7.4" applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "dev" } meta_fetcher = "0.1.1" parking_lot = "0.12.3" +aes-gcm = "0.10.3" +typenum = "1.17.0" [features] custom-protocol = ["tauri/custom-protocol"]