diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6cb41..15ae302 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ Todas las versiones publicadas de `gradio-player` (paquete .deb). --- +## [0.4.18] — 2026-08-23 + +### Seguridad + +- **Protección contra secuestro de ID en el relay de internet**: `relay.rs` ahora envía al relay una huella SHA-256 del `client_token` configurado (nunca el token en sí) al registrarse con el ID de 8 dígitos. El servidor de relay (`gradio-relay`, desplegado en `relay.gradio.net`) rechaza un nuevo registro para un ID ya activo si la huella no coincide, evitando que un tercero que conozca/adivine el ID de una estación le robe el slot e intercepte el token de los clientes remotos que se conecten después. Compatible con estaciones que aún no actualizaron: si el slot activo nunca tuvo huella (versión anterior), el comportamiento es el mismo que antes de este fix — sin regresión, pero sin protección hasta actualizar. + +--- + ## [0.4.17] — 2026-08-22 ### Nuevo diff --git a/Cargo.lock b/Cargo.lock index 819eeb7..861a52d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1591,7 +1591,7 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radio-player" -version = "0.4.17" +version = "0.4.18" dependencies = [ "anyhow", "cairo-rs", @@ -1614,6 +1614,7 @@ dependencies = [ "rand", "serde", "serde_json", + "sha2", "symphonia", "tokio", "tokio-tungstenite", @@ -1834,6 +1835,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index c7a44bb..dfcd96d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "radio-player" -version = "0.4.17" +version = "0.4.18" edition = "2021" description = "G Radio Player — reproductor y herramientas de programación" license = "GPL-3.0-or-later" @@ -92,6 +92,7 @@ zip = "2" tokio-tungstenite = { version = "0.24", features = ["native-tls"] } futures-util = "0.3" native-tls = "0.2" +sha2 = "0.10" log = "0.4" env_logger = "0.11" diff --git a/src/main.rs b/src/main.rs index b5e8293..e7f3e0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6619,7 +6619,7 @@ fn main() { if gr_cfg_srv.relay_habilitado && gr_cfg_srv.relay_id.len() == 8 && gr_cfg_srv.relay_id.chars().all(|c| c.is_ascii_digit()) { - relay::iniciar(gr_cfg_srv.relay_id, gr_cfg_srv.client_port, cliente_conectado.clone()); + relay::iniciar(gr_cfg_srv.relay_id, gr_cfg_srv.client_port, cliente_conectado.clone(), gr_cfg_srv.client_token.clone()); } let app = Application::builder() diff --git a/src/relay.rs b/src/relay.rs index 5e0059d..58f29c0 100644 --- a/src/relay.rs +++ b/src/relay.rs @@ -45,9 +45,27 @@ fn relay_url() -> String { std::env::var("GRADIO_RELAY_URL").unwrap_or_else(|_| DEFAULT_RELAY_URL.to_string()) } +/// Calcula la huella (SHA-256 hex) del client_token para probar continuidad +/// de dueño ante el relay sin enviarle el token real. `None` si el token +/// está vacío (el relay trata eso como "sin protección", igual que antes +/// de tener este mecanismo). +fn secreto_de_token(client_token: &str) -> Option { + if client_token.trim().is_empty() { + return None; + } + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(client_token.trim().as_bytes()); + Some(format!("{:x}", hasher.finalize())) +} + /// Lanza el hilo de relay. No bloquea — retorna inmediatamente. -/// `id` — 8 dígitos numéricos, `puerto_local` — puerto del servidor TCP local. -pub fn iniciar(id: String, puerto_local: u16, cliente_conectado: ClienteConectado) { +/// `id` — 8 dígitos numéricos, `puerto_local` — puerto del servidor TCP local, +/// `client_token` — token de autenticación configurado (se envía al relay +/// como huella SHA-256, nunca en texto plano) para evitar que un tercero +/// que conozca el ID le robe el slot a esta estación. +pub fn iniciar(id: String, puerto_local: u16, cliente_conectado: ClienteConectado, client_token: String) { + let secreto = secreto_de_token(&client_token); std::thread::Builder::new() .name("gradio-relay".to_string()) .spawn(move || { @@ -55,17 +73,17 @@ pub fn iniciar(id: String, puerto_local: u16, cliente_conectado: ClienteConectad .enable_all() .build() .expect("relay: no se pudo crear runtime tokio"); - rt.block_on(bucle_relay(id, puerto_local, cliente_conectado)); + rt.block_on(bucle_relay(id, puerto_local, cliente_conectado, secreto)); }) .expect("relay: no se pudo crear hilo"); } // ─── Bucle de reconexión ────────────────────────────────────────────────────── -async fn bucle_relay(id: String, puerto_local: u16, cliente_conectado: ClienteConectado) { +async fn bucle_relay(id: String, puerto_local: u16, cliente_conectado: ClienteConectado, secreto: Option) { loop { log::info!("relay: conectando con ID={}", id); - match conectar_y_operar(&id, puerto_local, cliente_conectado.clone()).await { + match conectar_y_operar(&id, puerto_local, cliente_conectado.clone(), secreto.clone()).await { Ok(()) => { log::info!("relay: sesión cerrada, reconectando en 1s..."); tokio::time::sleep(Duration::from_secs(1)).await; @@ -80,7 +98,7 @@ async fn bucle_relay(id: String, puerto_local: u16, cliente_conectado: ClienteCo // ─── Conexión principal ─────────────────────────────────────────────────────── -async fn conectar_y_operar(id: &str, puerto_local: u16, cliente_conectado: ClienteConectado) -> Result<()> { +async fn conectar_y_operar(id: &str, puerto_local: u16, cliente_conectado: ClienteConectado, secreto: Option) -> Result<()> { // connect_async maneja TLS automáticamente para wss:// (feature native-tls) let url = relay_url(); log::info!("relay: usando URL {}", url); @@ -115,7 +133,10 @@ async fn conectar_y_operar(id: &str, puerto_local: u16, cliente_conectado: Clien }); // ── Handshake de registro ───────────────────────────────────────────────── - let msg = format!(r#"{{"tipo":"register","id":"{}"}}"#, id); + let msg = match &secreto { + Some(s) => format!(r#"{{"tipo":"register","id":"{}","secreto":"{}"}}"#, id, s), + None => format!(r#"{{"tipo":"register","id":"{}"}}"#, id), + }; out_tx.send(Message::Text(msg.into())) .map_err(|_| anyhow::anyhow!("relay: canal de escritura cerrado antes de registrar"))?;