mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-22 05:34: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 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!())
|
||||
|
|
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