diff --git a/Cargo.toml b/Cargo.toml index fbd6672..c486c3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ reqwest = "0.11.20" base64 = "0.21.4" tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] } md5 = "0.7.0" +futures = "0.3.28" diff --git a/src/cloud.rs b/src/cloud.rs index bff08d4..1ccb46c 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -1,9 +1,10 @@ use std::{ collections::HashMap, + str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; -use anyhow::{Error, Result}; +use anyhow::{anyhow, bail, Error, Result}; use base64::{engine::general_purpose, Engine}; use chrono::Local; @@ -101,13 +102,8 @@ impl Cloud { .collect() } - async fn api_request( - &self, - endpoint: &str, - dump_data: String, - header: Option, - ) -> Result { - let mut header = header.unwrap_or_default(); + async fn api_request(&self, endpoint: &str, dump_data: String) -> Result { + let mut header = HeaderMap::default(); let url = format!("{}{}", self.api_url, endpoint); @@ -159,15 +155,11 @@ impl Cloud { data.insert("userType".to_string(), "0".to_string()); data.insert("userName".to_string(), format!("{}", self.account)); - let response = Self::parse_response( - self.api_request( - "/v1/multicloud/platform/user/route", - to_string(&data)?, - None, - ) - .await?, - ) - .ok_or(Error::msg("failed parsing response"))?; + let response = Response::from_str( + &self + .api_request("/v1/multicloud/platform/user/route", to_string(&data)?) + .await?, + )?; if let Some(api_url) = response.normal_data().get("masUrl") { self.api_url = api_url.clone(); @@ -180,11 +172,11 @@ impl Cloud { let mut data = self.make_general_data(); data.insert("loginAccount".to_string(), format!("{}", self.account)); - let response = Self::parse_response( - self.api_request("/v1/user/login/id/get", to_string(&data)?, None) + let response = Response::from_str( + &self + .api_request("/v1/user/login/id/get", to_string(&data)?) .await?, - ) - .ok_or(Error::msg("failed parsing response"))?; + )?; let login_id = response .normal_data() @@ -233,57 +225,112 @@ impl Cloud { let dump_i = to_string(&i).unwrap(); - let response = - Self::parse_response(self.api_request("/mj/user/login", dump_i, None).await?) - .ok_or(Error::msg("failed parsing response"))?; + let response = Response::from_str(&self.api_request("/mj/user/login", dump_i).await?)?; - self.uid = Some(response.nested_data().uid.clone()); - self.access_token = Some(response.nested_data().mdata.accessToken.clone()); + self.uid = Some(response.login_data().uid.clone()); + self.access_token = Some(response.login_data().mdata.accessToken.clone()); self.security.set_aes_keys( - &response.nested_data().accessToken, - &response.nested_data().randomData, + &response.login_data().accessToken, + &response.login_data().randomData, ); Ok(()) } - fn parse_response(s: String) -> Option { - if let Ok(r) = from_str::(&s) { - return Some(Response::from(r)); + pub async fn keys(&self, device_id: u64) -> Result<(String, String)> { + for method in [1, 2] { + let udp_id = CloudSecurity::udp_id(device_id, method); + + let mut data = self.make_general_data(); + data.insert("udpid".to_string(), udp_id.clone()); + + let response = Response::from_str( + &self + .api_request("/v1/iot/secure/getToken", to_string(&data)?) + .await?, + )?; + + for token in response.token_list() { + if token.udpId == udp_id { + return Ok((token.token.clone(), token.key.clone())); + } + } } - if let Ok(r) = from_str::(&s) { - return Some(Response::from(r)); - } - - if let Ok(r) = from_str::(&s) { - return Some(Response::from(r)); - } - - None + bail!("no keys found") } } #[derive(Debug)] enum Response { Normal(ResponseNumber), - NestedMap(ResponseMap), + NestedMap(ResponseLogin), + TokenList(ResponseTokenList), } impl Response { - pub fn normal_data(&self) -> &HashMap { + pub fn code(&self) -> i32 { match self { - Self::Normal(n) => &n.data, - Self::NestedMap(_) => panic!(), + Self::Normal(n) => n.code, + Self::NestedMap(n) => n.code, + Self::TokenList(n) => n.code.parse().unwrap(), } } - pub fn nested_data(&self) -> &ResponseMapData { + pub fn normal_data(&self) -> &HashMap { match self { - Self::Normal(_) => panic!(), - Self::NestedMap(n) => &n.data, + Self::Normal(n) => &n.data, + _ => panic!(), } } + + pub fn login_data(&self) -> &ResponseLoginData { + match self { + Self::NestedMap(n) => &n.data, + _ => panic!(), + } + } + + pub fn token_list(&self) -> &[Token] { + match self { + Self::TokenList(n) => &n.data.tokenlist, + _ => panic!(), + } + } +} + +impl FromStr for Response { + type Err = Error; + + fn from_str(s: &str) -> Result { + let resp_fn = |s| { + if let Ok(r) = from_str::(s) { + return Ok(Response::from(r)); + } + + if let Ok(r) = from_str::(s) { + return Ok(Response::from(r)); + } + + if let Ok(r) = from_str::(s) { + return Ok(Response::from(r)); + } + + if let Ok(r) = from_str::(s) { + return Ok(Response::from(r)); + } + + Err(Error::msg("failed parsing response")) + }; + + let response = resp_fn(s)?; + + if response.code() != 0 { + bail!("Error return code: {}", response.code()); + } + + Ok(response) + } } impl From for Response { @@ -302,12 +349,18 @@ impl From for Response { } } -impl From for Response { - fn from(value: ResponseMap) -> Self { +impl From for Response { + fn from(value: ResponseLogin) -> Self { Self::NestedMap(value) } } +impl From for Response { + fn from(value: ResponseTokenList) -> Self { + Self::TokenList(value) + } +} + #[allow(unused)] #[derive(Deserialize, Debug)] struct ResponseNumber { @@ -325,16 +378,39 @@ struct ResponseString { #[allow(unused)] #[derive(Deserialize, Debug)] -struct ResponseMap { +struct ResponseLogin { msg: String, code: i32, - pub data: ResponseMapData, + pub data: ResponseLoginData, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +struct ResponseTokenList { + msg: String, + code: String, + pub data: TokenList, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +struct TokenList { + tokenlist: Vec, +} + +#[allow(unused)] +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +struct Token { + udpId: String, + key: String, + token: String, } #[allow(non_snake_case)] #[allow(unused)] #[derive(Deserialize, Debug)] -struct ResponseMapData { +struct ResponseLoginData { randomData: String, uid: String, accountId: String, @@ -376,6 +452,9 @@ struct Data<'a> { #[cfg(test)] mod test { use anyhow::Result; + use futures::future::try_join; + + use crate::Startup; use super::Cloud; @@ -408,4 +487,17 @@ mod test { Ok(()) } + + #[tokio::test] + async fn keys() -> Result<()> { + let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; + + let (_, devices) = try_join(cloud.login(), Startup::discover()).await?; + + for device_info in devices { + let (_token, _key) = cloud.keys(device_info.id).await?; + } + + Ok(()) + } } diff --git a/src/cloud_security.rs b/src/cloud_security.rs index 8160b27..508c283 100644 --- a/src/cloud_security.rs +++ b/src/cloud_security.rs @@ -89,6 +89,40 @@ impl CloudSecurity { .collect::>() .join("") } + + pub fn udp_id(device_id: u64, method: u8) -> String { + let bytes_id: Vec = match method { + 0 => device_id.to_be_bytes().into_iter().rev().collect(), + 1 => { + let mut v = device_id.to_be_bytes().to_vec(); + v.truncate(6); + + v + } + 1 => { + let mut v = device_id.to_be_bytes().to_vec(); + v.truncate(6); + + v + } + + _ => return String::new(), + }; + + let mut data = Sha256::digest(&bytes_id); + + for i in 0..16 { + data[i] ^= data[i + 16]; + } + + let mut v: Vec = data.into_iter().collect(); + v.truncate(16); + + v.iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join("") + } } #[cfg(test)] diff --git a/src/discover.rs b/src/discover.rs index 2032cfc..1648131 100644 --- a/src/discover.rs +++ b/src/discover.rs @@ -78,7 +78,7 @@ impl Startup { const NUM_RETRIES: u8 = 5; - pub fn discover() -> Result> { + pub async fn discover() -> Result> { let socket = UdpSocket::bind("0.0.0.0:0")?; socket.set_broadcast(true)?; socket.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -184,18 +184,18 @@ mod test { use super::{Device, Startup}; - #[test] - fn discover() -> Result<()> { - let devices = Startup::discover()?; + #[tokio::test] + async fn discover() -> Result<()> { + let devices = Startup::discover().await?; println!("{devices:#?}"); Ok(()) } - #[test] - fn connect() -> Result<()> { - for device_info in Startup::discover()? { + #[tokio::test] + async fn connect() -> Result<()> { + for device_info in Startup::discover().await? { Device::connect(device_info)?; } diff --git a/tst.json b/tst.json new file mode 100644 index 0000000..36bc01c --- /dev/null +++ b/tst.json @@ -0,0 +1,13 @@ +{ + "code": "0", + "msg": "ok", + "data": { + "tokenlist": [ + { + "udpId": "a79479839b272432f3bbf971f9faed30", + "key": "d9d856b06f5245349e96df80e063af989532e6ef03c34039b74bfa0754a0ddf0", + "token": "4D14C31771A353EB584566243552C5CC2CE55C06162E550E71A805949569F7E8FA659B5B4B6CC097475A1270A4CC3DB75D52782DE3D1E65FEEA0945C6FFC7B3C" + } + ] + } +} \ No newline at end of file