use std::{ collections::HashMap, str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; use anyhow::{bail, Error, Result}; use base64::{engine::general_purpose, Engine}; use chrono::Local; use rand::RngCore; use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_string}; use crate::cloud_security::CloudSecurity; use crate::hex; pub struct Cloud { device_id: String, uid: Option, api_url: String, access_token: Option, auth_base: String, login_id: Option, account: String, password: String, security: CloudSecurity, } #[allow(non_snake_case)] #[derive(Deserialize, Serialize, Debug)] struct Input { iotData: HashMap, data: HashMap, stamp: String, reqId: String, } impl Cloud { pub const APP_ID: &str = "1010"; pub const APP_KEY: &str = "ac21b9f9cbfe4ca5a88562ef25e2b768"; pub const API_URL: &str = "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias="; pub const APP_VERSION: &str = "3.0.2"; pub const SRC: &str = "10"; pub const IOT_KEY: &str = "meicloud"; pub const HMAC_KEY: &str = "PROD_VnoClJI9aikS8dyy"; pub fn new(account: impl ToString, password: impl ToString) -> Result { let account = account.to_string(); Ok(Self { device_id: CloudSecurity::device_id(&account), uid: None, api_url: "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=".to_string(), access_token: None, auth_base: general_purpose::STANDARD_NO_PAD .encode(format!("{}:{}", Self::APP_KEY, Self::IOT_KEY).as_bytes()), login_id: None, account, password: password.to_string(), security: CloudSecurity::new(Self::APP_KEY, Self::IOT_KEY, Self::HMAC_KEY), }) } fn make_general_data(&self) -> HashMap { [ ("appVersion", Self::APP_VERSION), ("src", Self::SRC), ("format", "2"), ( "stamp", Local::now().format("%Y%m%d%H%M%S").to_string().as_str(), ), ("platformId", "1"), ("deviceId", self.device_id.to_string().as_str()), ( "reqId", { let mut d = [0; 16]; rand::thread_rng().fill_bytes(&mut d); let mut s = String::new(); for b in d { s += format!("{b:x}").as_str(); } s } .as_str(), ), ("uid", self.uid.clone().unwrap_or_default().as_str()), ("clientType", "1"), ("appId", Self::APP_ID), ] .into_iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect() } async fn api_request(&self, endpoint: &str, dump_data: String) -> Result { let mut header = HeaderMap::default(); let url = format!("{}{}", self.api_url, endpoint); let random = SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs() .to_string(); let sign = self.security.sign(&dump_data, &random); header.insert("x-recipe-app", Self::APP_ID.parse().unwrap()); header.insert( "authorization", format!("Basic {}", self.auth_base).parse().unwrap(), ); header.insert( "content-type", "application/json; charset=utf-8".parse().unwrap(), ); header.insert("secretVersion", "1".parse().unwrap()); header.insert("sign", sign.parse().unwrap()); header.insert("random", random.parse().unwrap()); if let Some(access_token) = &self.access_token { header.insert("accesstoken", access_token.parse().unwrap()); } if let Some(_uid) = &self.uid { match header.get_mut("uid") { Some(uid) => *uid = _uid.parse().unwrap(), None => { header.insert("uid", _uid.parse().unwrap()); } } } Ok(reqwest::Client::new() .post(url) .headers(header) .body(dump_data) .send() .await? .text() .await?) } async fn reroute(&mut self) -> Result<()> { let mut data = self.make_general_data(); data.insert("userType".to_string(), "0".to_string()); data.insert("userName".to_string(), format!("{}", self.account)); 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(); } Ok(()) } async fn request_login_id(&mut self) -> Result<()> { let mut data = self.make_general_data(); data.insert("loginAccount".to_string(), format!("{}", self.account)); let response = Response::from_str( &self .api_request("/v1/user/login/id/get", to_string(&data)?) .await?, )?; let login_id = response .normal_data() .get("loginId") .cloned() .ok_or(Error::msg("failed to request loginId"))?; self.login_id = Some(login_id); Ok(()) } pub async fn login(&mut self) -> Result<()> { self.reroute().await?; self.request_login_id().await?; let mut iot_data = self.make_general_data(); iot_data.remove("uid"); iot_data.insert( "iampwd".to_string(), self.security .encrypt_iam_password(&self.login_id.clone().unwrap(), &self.password), ); iot_data.insert("loginAccount".to_string(), self.account.clone()); iot_data.insert( "password".to_string(), self.security .encrypt_password(&self.login_id.clone().unwrap(), &self.password), ); let stamp = iot_data.get("stamp").unwrap().clone(); let i = Input { iotData: iot_data, data: from_str( &to_string(&Data { appKey: Self::APP_KEY, deviceId: self.device_id.to_string().as_str(), platform: "2", }) .unwrap(), ) .unwrap(), stamp, reqId: "25f278357a1b1c08cf878b05ade7db26".to_string(), }; let dump_i = to_string(&i).unwrap(); let response = Response::from_str(&self.api_request("/mj/user/login", dump_i).await?)?; self.uid = Some(response.login_data().uid.clone()); self.access_token = Some(response.login_data().mdata.accessToken.clone()); self.security.set_aes_keys( &response.login_data().accessToken, &response.login_data().randomData, ); Ok(()) } 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); // 1: "a79479839b272432f3bbf971f9faed30" // 2: "a153e4273b29c04db85763c739d45203" let mut data = self.make_general_data(); data.insert("udpid".to_string(), udp_id.clone()); // { // 'appVersion': '3.0.2', // 'src': '10', // 'format': '2', // 'stamp': '20230925115743', // 'platformId': '1', // 'deviceId': 'c1bdb9d159aa18fe', // 'reqId': '3e593c7a186f25ccca0d010d4316af0d', // 'uid': '4c48146bdedaca956c465d53cf7dd9a3', // 'clientType': '1', // 'appId': '1010', // 'udpid': '53c18d6f4682867654bee60c9ea047f4' // } let response = Response::from_str( &self .api_request("/v1/iot/secure/getToken", to_string(&data)?) .await?, )?; // '76697C9A0685778C6B4F487A70497F4DB3CDD2C0949138B5BA798DADC5AE0712ECF6C3559C7EE68B96B2BB2DC140CF6172D9F1587065D4A536A75D492031E22E' // '025f9ff7bd3c4aceb1e559ab13d5e73f6fb2358e2bcf4bb883ab62225d6b9d2d' for token in response.token_list() { if token.udpId == udp_id { return Ok(( Self::hex_to_lower(&token.token)?, Self::hex_to_lower(&token.key)?, )); } } } bail!("no keys found") } fn hex_to_lower(h: &str) -> Result { let lower = hex(h)? .iter() .map(|b| format!("{b:02x}")) .collect::>() .join(""); Ok(lower) } } #[derive(Debug)] enum Response { Normal(ResponseNumber), NestedMap(ResponseLogin), TokenList(ResponseTokenList), } impl Response { pub fn code(&self) -> i32 { match self { Self::Normal(n) => n.code, Self::NestedMap(n) => n.code, Self::TokenList(n) => n.code.parse().unwrap(), } } pub fn normal_data(&self) -> &HashMap { match self { 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 { fn from(value: ResponseNumber) -> Self { Self::Normal(value) } } impl From for Response { fn from(value: ResponseString) -> Self { Self::Normal(ResponseNumber { msg: value.msg, code: value.code.parse().unwrap(), data: value.data, }) } } 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 { msg: String, code: i32, pub data: HashMap, } #[derive(Deserialize, Debug)] struct ResponseString { msg: String, code: String, pub data: HashMap, } #[allow(unused)] #[derive(Deserialize, Debug)] struct ResponseLogin { msg: String, code: i32, 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 ResponseLoginData { randomData: String, uid: String, accountId: String, nickname: String, mdata: MData, accessToken: String, userId: String, email: String, } #[allow(unused)] #[allow(non_snake_case)] #[derive(Deserialize, Debug)] struct MData { tokenPwdInfo: TokenPwdInfo, userInfo: HashMap>, doDeviceBind: Option, accessToken: String, signUnlockEnabled: Option, } #[allow(unused)] #[allow(non_snake_case)] #[derive(Deserialize, Debug)] struct TokenPwdInfo { tokenPwd: String, expiredDate: u64, createDate: u64, } #[allow(non_snake_case)] #[derive(Serialize, Debug)] struct Data<'a> { appKey: &'a str, deviceId: &'a str, platform: &'a str, } #[cfg(test)] mod test { use anyhow::Result; use futures::future::try_join; use serial_test::serial; use crate::Startup; use super::Cloud; #[tokio::test] async fn reroute() -> Result<()> { let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; cloud.reroute().await?; Ok(()) } #[tokio::test] async fn login_id() -> Result<()> { let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; cloud.reroute().await?; cloud.request_login_id().await?; assert!(cloud.login_id.is_some()); Ok(()) } #[tokio::test] #[serial] async fn login() -> Result<()> { let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; cloud.login().await?; Ok(()) } #[tokio::test] #[serial] 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(()) } }