Merge branch 'dev/live-sync' into main

This commit is contained in:
PandaDEV 2025-01-29 20:42:56 +01:00 committed by GitHub
commit 1905593784
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 349 additions and 26 deletions

94
src-tauri/Cargo.lock generated
View file

@ -31,6 +31,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -714,6 +749,16 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.8.1" version = "1.8.1"
@ -1080,6 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core 0.6.4",
"typenum", "typenum",
] ]
@ -1120,6 +1166,15 @@ dependencies = [
"syn 2.0.87", "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]] [[package]]
name = "darling" name = "darling"
version = "0.20.10" version = "0.20.10"
@ -2645,6 +2700,15 @@ dependencies = [
"configparser", "configparser",
] ]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "interpolate_name" name = "interpolate_name"
version = "0.2.4" version = "0.2.4"
@ -3569,6 +3633,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.66" version = "0.10.66"
@ -3959,6 +4029,18 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -4081,6 +4163,7 @@ name = "qopy"
version = "0.4.0" version = "0.4.0"
dependencies = [ dependencies = [
"active-win-pos-rs", "active-win-pos-rs",
"aes-gcm",
"applications", "applications",
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
@ -4113,6 +4196,7 @@ dependencies = [
"tauri-plugin-updater", "tauri-plugin-updater",
"time", "time",
"tokio", "tokio",
"typenum",
"url", "url",
"uuid", "uuid",
] ]
@ -6469,6 +6553,16 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 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]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

View file

@ -53,6 +53,8 @@ include_dir = "0.7.4"
applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "dev" } applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "dev" }
meta_fetcher = "0.1.1" meta_fetcher = "0.1.1"
parking_lot = "0.12.3" parking_lot = "0.12.3"
aes-gcm = "0.10.3"
typenum = "1.17.0"
[features] [features]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]

View file

@ -3,6 +3,7 @@
mod api; mod api;
mod db; mod db;
mod utils; mod utils;
mod sync;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
use std::fs; use std::fs;
@ -10,11 +11,13 @@ use tauri::Manager;
use tauri_plugin_aptabase::{ EventTracker, InitOptions }; use tauri_plugin_aptabase::{ EventTracker, InitOptions };
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_prevent_default::Flags; use tauri_plugin_prevent_default::Flags;
use sync::sync::ClipboardSync;
use sync::pairing::PairingManager;
use tokio::sync::Mutex;
use std::sync::Arc;
fn main() { #[tokio::main]
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); async fn main() {
let _guard = runtime.enter();
tauri::Builder tauri::Builder
::default() ::default()
.plugin(tauri_plugin_clipboard::init()) .plugin(tauri_plugin_clipboard::init())
@ -69,38 +72,52 @@ fn main() {
} }
let db_url = format!("sqlite:{}", db_path.to_str().unwrap()); let db_url = format!("sqlite:{}", db_path.to_str().unwrap());
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
let app_handle_clone = app_handle.clone(); // Create the pool in a separate tokio runtime
tauri::async_runtime::spawn(async move { let pool = tokio::runtime::Runtime
let pool = SqlitePoolOptions::new() ::new()
.unwrap()
.block_on(async {
SqlitePoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(&db_url).await .connect(&db_url).await
.expect("Failed to create pool"); .expect("Failed to create pool")
app_handle_clone.manage(pool);
}); });
app_handle.manage(pool);
let main_window = app.get_webview_window("main"); 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::hotkeys::setup(app_handle.clone());
api::tray::setup(app)?; api::tray::setup(app).expect("Failed to setup tray");
api::clipboard::setup(app.handle()); api::clipboard::setup(&app_handle);
let _ = api::clipboard::start_monitor(app_handle.clone()); 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()); utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
main_window let _ = main_window
.as_ref() .as_ref()
.map(|w| w.hide()) .map(|w| w.hide())
.unwrap_or(Ok(()))?; .expect("Failed to hide window");
let _ = app.track_event("app_started", None); app.track_event("app_started", None).expect("Failed to track event");
tauri::async_runtime::spawn(async move {
api::updater::check_for_updates(app_handle, false).await;
});
Ok(()) Ok(())
}) })
@ -124,7 +141,11 @@ fn main() {
db::history::read_image, db::history::read_image,
db::settings::get_setting, db::settings::get_setting,
db::settings::save_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!()) .run(tauri::generate_context!())

View file

@ -0,0 +1,2 @@
pub mod pairing;
pub mod sync;

View file

@ -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<String, String> {
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<String, String> {
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()
}

View file

@ -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<U12>,
}
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<Mutex<Self>>) {
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<Mutex<ClipboardSync>>>) -> 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<Mutex<ClipboardSync>>>) -> Result<(), String> {
let sync = sync.lock().await;
sync.receive_clipboard(app_handle).await
}