mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 13:14:04 +02:00
feat(sync): add modules for pairing and syncing clipboard data
This commit is contained in:
parent
12b8f9a49e
commit
dcb2daaa87
4 changed files with 252 additions and 25 deletions
|
@ -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()
|
||||||
.max_connections(5)
|
.unwrap()
|
||||||
.connect(&db_url).await
|
.block_on(async {
|
||||||
.expect("Failed to create pool");
|
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 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!())
|
||||||
|
|
2
src-tauri/src/sync/mod.rs
Normal file
2
src-tauri/src/sync/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod pairing;
|
||||||
|
pub mod sync;
|
112
src-tauri/src/sync/pairing.rs
Normal file
112
src-tauri/src/sync/pairing.rs
Normal 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()
|
||||||
|
}
|
92
src-tauri/src/sync/sync.rs
Normal file
92
src-tauri/src/sync/sync.rs
Normal 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
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue