From e132bf1c34e8ae9b6c87f26a29ab67311bbf8977 Mon Sep 17 00:00:00 2001 From: aShanki Date: Sun, 26 Jul 2026 13:49:40 +1000 Subject: [PATCH] Migrate launcher telemetry to API v2 --- Cargo.lock | 134 +++++++++++++++- apps/desktop/src-tauri/Cargo.toml | 3 + apps/desktop/src-tauri/src/telemetry/mod.rs | 161 ++++++++++++++++---- 3 files changed, 267 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d226c1e0..0a340521 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,6 +237,18 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -526,6 +538,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cookie" version = "0.18.1" @@ -658,6 +676,33 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling" version = "0.23.0" @@ -703,6 +748,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -877,6 +932,30 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "embed-resource" version = "3.0.9" @@ -987,6 +1066,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "field-offset" version = "0.3.6" @@ -1018,9 +1103,12 @@ name = "flarial-launcher" version = "0.0.0" dependencies = [ "anyhow", + "base64 0.23.0", + "ed25519-dalek", "notify", "parking_lot", "quick-xml 0.36.2", + "rand", "reqwest 0.12.28", "serde", "serde_json", @@ -2666,6 +2754,16 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2924,12 +3022,12 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -2939,7 +3037,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -3495,6 +3602,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -3577,6 +3693,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 3725f5a8..e110a52d 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -39,6 +39,9 @@ quick-xml.workspace = true notify.workspace = true parking_lot.workspace = true uuid.workspace = true +ed25519-dalek = "2.1.1" +base64 = "0.23.0" +rand = "0.9.5" [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/apps/desktop/src-tauri/src/telemetry/mod.rs b/apps/desktop/src-tauri/src/telemetry/mod.rs index 1f4ed615..894e46c3 100644 --- a/apps/desktop/src-tauri/src/telemetry/mod.rs +++ b/apps/desktop/src-tauri/src/telemetry/mod.rs @@ -1,34 +1,141 @@ -use crate::http; -use serde::Serialize; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use ed25519_dalek::{Signer, SigningKey}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::path::PathBuf; -const ENDPOINT: &str = "https://api.flarial.xyz/launcher/events/launch"; +const BASE_URL: &str = "https://api.flarial.xyz"; +const EVENTS_PATH: &str = "/api/v2/events"; -#[derive(Serialize)] -struct LaunchEvent { - timestamp: String, - #[serde(rename = "installId")] - install_id: String, +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct StoredIdentity { + installation_id: String, + secret_key: String, } -pub fn fire_launch_event(install_id: String) { +struct Identity { + installation_id: String, + signing_key: SigningKey, +} + +pub fn fire_launch_event(legacy_install_id: String) { tokio::spawn(async move { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis().to_string()) - .unwrap_or_default(); - let nonce = uuid::Uuid::new_v4().to_string(); - let body = LaunchEvent { - timestamp: now.clone(), - install_id: install_id.clone(), - }; - let _ = http::client() - .post(ENDPOINT) - .header("User-Agent", "Samsung Smart Fridge") - .header("X-Flarial.Timestamp", now) - .header("X-Flarial-InstallId", install_id) - .header("X-Flarial-Nonce", nonce) - .json(&body) - .send() - .await; + if let Err(error) = send_launch_event(legacy_install_id).await { + tracing::debug!(%error, "API v2 launch telemetry failed"); + } + }); +} + +async fn send_launch_event(legacy_install_id: String) -> anyhow::Result<()> { + let identity = load_or_register().await?; + let occurred_at_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_millis() as u64; + let body = serde_json::json!({ + "batchId": random_token(), + "events": [{ + "type": "launcher_launch", + "occurredAtMs": occurred_at_ms, + "properties": { + "legacyInstallId": legacy_install_id, + "version": env!("CARGO_PKG_VERSION") + } + }] + }) + .to_string(); + let timestamp = (occurred_at_ms / 1000).to_string(); + let nonce = random_token(); + let digest = Sha256::digest(body.as_bytes()); + let canonical = format!( + "POST\n{EVENTS_PATH}\n{timestamp}\n{nonce}\n{}", + URL_SAFE_NO_PAD.encode(digest) + ); + let signature = identity.signing_key.sign(canonical.as_bytes()); + let response = crate::http::client() + .post(format!("{BASE_URL}{EVENTS_PATH}")) + .header("User-Agent", "Flarial-Launcher/2.0") + .header("X-Flarial-Installation", identity.installation_id) + .header("X-Flarial-Timestamp", timestamp) + .header("X-Flarial-Nonce", nonce) + .header( + "X-Flarial-Signature", + URL_SAFE_NO_PAD.encode(signature.to_bytes()), + ) + .header("Content-Type", "application/json") + .body(body) + .send() + .await?; + anyhow::ensure!( + response.status() == reqwest::StatusCode::ACCEPTED, + "event endpoint returned {}", + response.status() + ); + Ok(()) +} + +async fn load_or_register() -> anyhow::Result { + let path = identity_path()?; + if let Ok(saved) = tokio::fs::read(&path).await { + if let Ok(stored) = serde_json::from_slice::(&saved) { + if let Ok(secret) = URL_SAFE_NO_PAD.decode(stored.secret_key) { + if let Ok(bytes) = <[u8; 32]>::try_from(secret) { + return Ok(Identity { + installation_id: stored.installation_id, + signing_key: SigningKey::from_bytes(&bytes), + }); + } + } + } + } + + let signing_key = SigningKey::from_bytes(&rand::random()); + let request = serde_json::json!({ + "publicKey": URL_SAFE_NO_PAD.encode(signing_key.verifying_key().to_bytes()), + "platform": "launcher", + "version": env!("CARGO_PKG_VERSION") }); + let response = crate::http::client() + .post(format!("{BASE_URL}/api/v2/installations")) + .header("User-Agent", "Flarial-Launcher/2.0") + .json(&request) + .send() + .await?; + anyhow::ensure!( + response.status() == reqwest::StatusCode::CREATED, + "installation endpoint returned {}", + response.status() + ); + let response_body: serde_json::Value = response.json().await?; + let installation_id = response_body["installationId"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("installation response omitted installationId"))? + .to_owned(); + let stored = StoredIdentity { + installation_id: installation_id.clone(), + secret_key: URL_SAFE_NO_PAD.encode(signing_key.to_bytes()), + }; + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + let temporary = path.with_extension("json.tmp"); + tokio::fs::write(&temporary, serde_json::to_vec(&stored)?).await?; + tokio::fs::rename(temporary, path).await?; + Ok(Identity { + installation_id, + signing_key, + }) +} + +fn identity_path() -> anyhow::Result { + let local = std::env::var_os("LOCALAPPDATA") + .ok_or_else(|| anyhow::anyhow!("LOCALAPPDATA is unavailable"))?; + Ok(PathBuf::from(local) + .join("Flarial") + .join("Launcher") + .join("api-v2.json")) +} + +fn random_token() -> String { + URL_SAFE_NO_PAD.encode(rand::random::<[u8; 18]>()) }