From e7527ddc4077ca29ebc39186c2dee97e64abfd68 Mon Sep 17 00:00:00 2001 From: hodasemi Date: Sat, 23 Sep 2023 11:41:49 +0200 Subject: [PATCH] Start implementing cloud login --- Cargo.toml | 10 +- cloud.py | 3 +- security.py | 12 +- src/cloud.rs | 277 +++++++++++++++++++++++++++++++++++++++++- src/cloud_security.rs | 55 +++++++++ src/discover.rs | 4 +- src/lib.rs | 1 + 7 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 src/cloud_security.rs diff --git a/Cargo.toml b/Cargo.toml index 5e3b214..50989d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,12 @@ edition = "2021" anyhow = { version = "1.0.75", features = ["backtrace"] } if-addrs = "0.10.1" aes = "0.8.3" -rand = "0.8.5" \ No newline at end of file +rand = "0.8.5" +chrono = "0.4.31" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.107" +hmac = "0.12.1" +sha2 = "0.10.7" +reqwest = "0.11.20" +base64 = "0.21.4" +tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] } diff --git a/cloud.py b/cloud.py index 0ddb5a4..4916c7c 100644 --- a/cloud.py +++ b/cloud.py @@ -73,7 +73,8 @@ class MideaCloud: data.update({ "stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S") }) - random = str(int(time.time())) + random = str(1695459986) + # random = str(int(time.time())) url = self._api_url + endpoint dump_data = json.dumps(data) sign = self._security.sign(dump_data, random) diff --git a/security.py b/security.py index 6a5daa6..625b070 100644 --- a/security.py +++ b/security.py @@ -26,8 +26,16 @@ class CloudSecurity: msg = self._iot_key msg += data msg += random - sign = hmac.new(self._hmac_key.encode("ascii"), msg.encode("ascii"), sha256) - return sign.hexdigest() + + hmac_ascii = self._hmac_key.encode("ascii") + msg_ascii = msg.encode("ascii") + + sign = hmac.new(hmac_ascii, msg_ascii, sha256) + + dig = sign.digest() + hex = sign.hexdigest() + + return hex def encrypt_password(self, login_id, data): m = sha256() diff --git a/src/cloud.rs b/src/cloud.rs index ceefc8d..c49a1ae 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -1,7 +1,282 @@ +use std::{ + collections::HashMap, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use anyhow::{anyhow, 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; + pub struct Cloud { - // + device_id: u64, + uid: Option, + api_url: String, + access_token: Option, + auth_base: String, + login_id: Option, + + account: String, + password: String, + + security: CloudSecurity, } 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 const IOT_KEY: &str = "9795516279659324117647275084689641883661667"; + // pub const HMAC_KEY: &str = "117390035944627627450677220413733956185864939010425"; + + pub fn new(account: impl ToString, password: impl ToString, device_id: u64) -> Result { + Ok(Self { + device_id, + 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: account.to_string(), + 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, + data: HashMap, + header: Option, + ) -> Result { + let mut header = header.unwrap_or_default(); + + let url = format!("{}{}", self.api_url, endpoint); + + let dump_data = to_string(&data)?; + 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()); + } + + Ok(reqwest::Client::new() + .post(url) + .headers(header) + .body(dump_data) + .timeout(Duration::from_secs(10)) + .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", data, None) + .await?, + )?; + + if let Some(api_url) = response.data.get("masUrl") { + self.api_url = api_url.clone(); + } + + Ok(()) + } + + async fn reqest_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", data, None) + .await?, + )?; + + let login_id = response + .data + .get("loginId") + .cloned() + .ok_or(Error::msg("failed to request loginId"))?; + + self.login_id = Some(login_id); + + Ok(()) + } + + async fn login(&mut self) -> Result<()> { + self.reroute().await?; + self.reqest_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 iot_data_dump = to_string(&iot_data)?; + + let mut data = HashMap::new(); + data.insert("iotData".to_string(), iot_data_dump); + data.insert( + "data".to_string(), + to_string(&Data { + appKey: Self::APP_KEY, + deviceId: self.device_id.to_string().as_str(), + platform: "2", + })?, + ); + data.insert("stamp".to_string(), stamp); + + let response: Response = from_str(&self.api_request("/mj/user/login", data, None).await?)?; + + self.uid = Some( + response + .data + .get("uid") + .cloned() + .ok_or(Error::msg("failed to request uid"))?, + ); + self.access_token = Some( + response + .data + .get("accessToken") + .cloned() + .ok_or(Error::msg("failed to request uid"))?, + ); + self.security.set_aes_keys( + &response + .data + .get("accessToken") + .cloned() + .ok_or(Error::msg("failed to request accessToken"))?, + &response + .data + .get("randomData") + .cloned() + .ok_or(Error::msg("failed to request randomData"))?, + ); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use anyhow::Result; + + use super::Cloud; + use crate::discover::Startup; + + #[tokio::test] + async fn test_login() -> Result<()> { + let devices = Startup::discover()?; + + println!("{devices:#?}"); + + for device_info in devices { + let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1", device_info.id)?; + + let id = cloud.login_id().await?; + + println!("{id:?}") + } + + Ok(()) + } +} + +#[derive(Deserialize, Debug)] +struct Response { + msg: String, + code: i32, + pub data: HashMap, +} + +#[derive(Serialize, Debug)] +struct Data<'a> { + appKey: &'a str, + deviceId: &'a str, + platform: &'a str, } diff --git a/src/cloud_security.rs b/src/cloud_security.rs new file mode 100644 index 0000000..8e72218 --- /dev/null +++ b/src/cloud_security.rs @@ -0,0 +1,55 @@ +use hmac::{Hmac, Mac}; +use sha2::Sha256; + +pub struct CloudSecurity { + login_key: &'static str, + iot_key: &'static str, + hmac_key: &'static str, + + access_token: Option, + random_data: Option, +} + +impl CloudSecurity { + pub fn new(login_key: &'static str, iot_key: &'static str, hmac_key: &'static str) -> Self { + Self { + login_key, + iot_key, + hmac_key, + + access_token: None, + random_data: None, + } + } + + pub fn set_aes_keys(&mut self, access_token: &str, random_data: &str) { + self.access_token = Some(access_token.to_string()); + self.random_data = Some(random_data.to_string()); + } + + pub fn sign(&self, data: &str, random: &str) -> String { + let mut msg = self.iot_key.to_string(); + + msg += data; + msg += random; + + type HmacSha256 = Hmac; + + let mut mac = HmacSha256::new_from_slice(self.hmac_key.as_bytes()).unwrap(); + mac.update(msg.as_bytes()); + + mac.finalize() + .into_bytes() + .iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join("") + } +} + +#[cfg(test)] +mod test { + use crate::cloud::Cloud; + + use super::*; +} diff --git a/src/discover.rs b/src/discover.rs index 7e442ce..2b477ff 100644 --- a/src/discover.rs +++ b/src/discover.rs @@ -14,7 +14,7 @@ use crate::{ #[derive(Debug, Clone)] pub struct DeviceInfo { - id: u64, + pub id: u64, model: String, sn: String, protocol: u8, @@ -65,7 +65,7 @@ impl Device { } } -struct Startup; +pub struct Startup; impl Startup { const BROADCAST_MSG: &'static [u8] = &[ diff --git a/src/lib.rs b/src/lib.rs index 2ac49c1..c8e4e84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use std::num::ParseIntError; mod cloud; +mod cloud_security; mod discover; mod security;