From 99819f36d97d1553e6fcf9c2d124965cb5fc362f Mon Sep 17 00:00:00 2001 From: hodasemi Date: Sat, 23 Sep 2023 16:42:59 +0200 Subject: [PATCH] Implement cloud connect --- Cargo.toml | 1 + cloud.py | 4 + midea.py | 18 +-- security.py | 25 +++- src/cloud.rs | 303 ++++++++++++++++++++++++++++++------------ src/cloud_security.rs | 70 +++++++++- src/discover.rs | 2 +- src/lib.rs | 3 + 8 files changed, 323 insertions(+), 103 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 50989d7..fbd6672 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ sha2 = "0.10.7" reqwest = "0.11.20" base64 = "0.21.4" tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] } +md5 = "0.7.0" diff --git a/cloud.py b/cloud.py index 4916c7c..bfdc8d2 100644 --- a/cloud.py +++ b/cloud.py @@ -89,6 +89,10 @@ class MideaCloud: "accesstoken": self._access_token }) response:dict = {"code": -1} + + print(header) + print(dump_data) + for i in range(0, 3): try: with self._api_lock: diff --git a/midea.py b/midea.py index 63f5b95..0449c61 100644 --- a/midea.py +++ b/midea.py @@ -6,17 +6,17 @@ import discover; import device; async def test(): - devices = discover.discover() + cl = cloud.MSmartHomeCloud( + "MSmartHome", + aiohttp.ClientSession(), + "michaelh.95@t-online.de", + "Hoda.semi1" + ) - for device_id in devices: - cl = cloud.MSmartHomeCloud( - "MSmartHome", - aiohttp.ClientSession(), - "michaelh.95@t-online.de", - "Hoda.semi1" - ) + if await cl.login(): + devices = discover.discover() - if await cl.login(): + for device_id in devices: keys = await cl.get_keys(device_id) for k in keys: diff --git a/security.py b/security.py index 625b070..5c2172e 100644 --- a/security.py +++ b/security.py @@ -38,12 +38,20 @@ class CloudSecurity: return hex def encrypt_password(self, login_id, data): + login_id = "bd3937c1-49ba-418c-a6e0-4b600263" + data = "Hoda.semi1" + m = sha256() m.update(data.encode("ascii")) - login_hash = login_id + m.hexdigest() + self._login_key + m_hex = m.hexdigest() + + login_hash = login_id + m_hex + self._login_key + m = sha256() m.update(login_hash.encode("ascii")) - return m.hexdigest() + m2_hex = m.hexdigest() + + return m2_hex def encrypt_iam_password(self, login_id, data) -> str: raise NotImplementedError @@ -136,12 +144,19 @@ class MSmartCloudSecurity(CloudSecurity): def encrypt_iam_password(self, login_id, data) -> str: md = md5() md.update(data.encode("ascii")) + md_hex = md.hexdigest() + md_second = md5() - md_second.update(md.hexdigest().encode("ascii")) - login_hash = login_id + md_second.hexdigest() + self._login_key + md_second.update(md_hex.encode("ascii")) + md_second_hex = md_second.hexdigest() + + login_hash = login_id + md_second_hex + self._login_key + sha = sha256() sha.update(login_hash.encode("ascii")) - return sha.hexdigest() + sha_hex = sha.hexdigest() + + return sha_hex def set_aes_keys(self, encrypted_key, encrypted_iv): key_digest = sha256(self._login_key.encode("ascii")).hexdigest() diff --git a/src/cloud.rs b/src/cloud.rs index c49a1ae..bff08d4 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -1,10 +1,11 @@ use std::{ collections::HashMap, - time::{Duration, SystemTime, UNIX_EPOCH}, + time::{SystemTime, UNIX_EPOCH}, }; -use anyhow::{anyhow, Error, Result}; +use anyhow::{Error, Result}; use base64::{engine::general_purpose, Engine}; + use chrono::Local; use rand::RngCore; use reqwest::header::HeaderMap; @@ -14,7 +15,7 @@ use serde_json::{from_str, to_string}; use crate::cloud_security::CloudSecurity; pub struct Cloud { - device_id: u64, + device_id: String, uid: Option, api_url: String, access_token: Option, @@ -27,6 +28,15 @@ pub struct Cloud { 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"; @@ -35,12 +45,12 @@ impl Cloud { 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 { + pub fn new(account: impl ToString, password: impl ToString) -> Result { + let account = account.to_string(); + Ok(Self { - device_id, + 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, @@ -48,7 +58,7 @@ impl Cloud { .encode(format!("{}:{}", Self::APP_KEY, Self::IOT_KEY).as_bytes()), login_id: None, - account: account.to_string(), + account, password: password.to_string(), security: CloudSecurity::new(Self::APP_KEY, Self::IOT_KEY, Self::HMAC_KEY), @@ -94,14 +104,13 @@ impl Cloud { async fn api_request( &self, endpoint: &str, - data: HashMap, + dump_data: String, 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() @@ -125,11 +134,19 @@ impl Cloud { 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) - .timeout(Duration::from_secs(10)) .send() .await? .text() @@ -142,31 +159,35 @@ impl Cloud { 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?, - )?; + 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"))?; - if let Some(api_url) = response.data.get("masUrl") { + if let Some(api_url) = response.normal_data().get("masUrl") { self.api_url = api_url.clone(); } Ok(()) } - async fn reqest_login_id(&mut self) -> Result<()> { + 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", data, None) + let response = Self::parse_response( + self.api_request("/v1/user/login/id/get", to_string(&data)?, None) .await?, - )?; + ) + .ok_or(Error::msg("failed parsing response"))?; let login_id = response - .data + .normal_data() .get("loginId") .cloned() .ok_or(Error::msg("failed to request loginId"))?; @@ -176,70 +197,180 @@ impl Cloud { Ok(()) } - async fn login(&mut self) -> Result<()> { + pub async fn login(&mut self) -> Result<()> { self.reroute().await?; - self.reqest_login_id().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), + .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), + .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 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 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 dump_i = to_string(&i).unwrap(); - let response: Response = from_str(&self.api_request("/mj/user/login", data, None).await?)?; + let response = + Self::parse_response(self.api_request("/mj/user/login", dump_i, None).await?) + .ok_or(Error::msg("failed parsing response"))?; - 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.uid = Some(response.nested_data().uid.clone()); + self.access_token = Some(response.nested_data().mdata.accessToken.clone()); 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"))?, + &response.nested_data().accessToken, + &response.nested_data().randomData, ); Ok(()) } + + fn parse_response(s: String) -> Option { + if let Ok(r) = from_str::(&s) { + return Some(Response::from(r)); + } + + 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 + } +} + +#[derive(Debug)] +enum Response { + Normal(ResponseNumber), + NestedMap(ResponseMap), +} + +impl Response { + pub fn normal_data(&self) -> &HashMap { + match self { + Self::Normal(n) => &n.data, + Self::NestedMap(_) => panic!(), + } + } + + pub fn nested_data(&self) -> &ResponseMapData { + match self { + Self::Normal(_) => panic!(), + Self::NestedMap(n) => &n.data, + } + } +} + +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: ResponseMap) -> Self { + Self::NestedMap(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 ResponseMap { + msg: String, + code: i32, + pub data: ResponseMapData, +} + +#[allow(non_snake_case)] +#[allow(unused)] +#[derive(Deserialize, Debug)] +struct ResponseMapData { + 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)] @@ -247,36 +378,34 @@ mod test { use anyhow::Result; use super::Cloud; - use crate::discover::Startup; #[tokio::test] - async fn test_login() -> Result<()> { - let devices = Startup::discover()?; + async fn reroute() -> Result<()> { + let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; - println!("{devices:#?}"); + cloud.reroute().await?; - for device_info in devices { - let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1", device_info.id)?; + Ok(()) + } - let id = cloud.login_id().await?; + #[tokio::test] + async fn login_id() -> Result<()> { + let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; - println!("{id:?}") - } + cloud.reroute().await?; + cloud.request_login_id().await?; + + assert!(cloud.login_id.is_some()); + + Ok(()) + } + + #[tokio::test] + async fn login() -> Result<()> { + let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; + + cloud.login().await?; 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 index 8e72218..8160b27 100644 --- a/src/cloud_security.rs +++ b/src/cloud_security.rs @@ -1,5 +1,5 @@ use hmac::{Hmac, Mac}; -use sha2::Sha256; +use sha2::{Digest, Sha256}; pub struct CloudSecurity { login_key: &'static str, @@ -45,6 +45,50 @@ impl CloudSecurity { .collect::>() .join("") } + + pub fn device_id(account: &str) -> String { + let mut sha = Sha256::digest(format!("Hello, {account}!").as_bytes()) + .iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join(""); + + sha.truncate(16); + + sha + } + + pub fn encrypt_iam_password(&self, login_id: &str, data: &str) -> String { + let md = md5::compute(data.as_bytes()); + let md_hex = format!("{:x}", md); + + let md_second = md5::compute(md_hex.as_bytes()); + let md_second_hex = format!("{:x}", md_second); + + let login_hash = format!("{}{}{}", login_id, md_second_hex, self.login_key); + + Sha256::digest(login_hash.as_bytes()) + .iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join("") + } + + pub fn encrypt_password(&self, login_id: &str, data: &str) -> String { + let m_hex = Sha256::digest(data.as_bytes()) + .iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join(""); + + let login_hash = format!("{}{}{}", login_id, m_hex, self.login_key); + + Sha256::digest(login_hash.as_bytes()) + .iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join("") + } } #[cfg(test)] @@ -52,4 +96,28 @@ mod test { use crate::cloud::Cloud; use super::*; + + #[test] + fn iam_password() { + let sec = CloudSecurity::new(Cloud::APP_KEY, "", ""); + + let sha = sec.encrypt_iam_password("bd3937c1-49ba-418c-a6e0-4b600263", "Hoda.semi1"); + + debug_assert_eq!( + sha, + "6cae02a0e1bc0c05ef886b1e045fc0ae662d9bc3ac60b890fa1c4bd59699c944" + ); + } + + #[test] + fn password() { + let sec = CloudSecurity::new(Cloud::APP_KEY, "", ""); + + let enc = sec.encrypt_password("bd3937c1-49ba-418c-a6e0-4b600263", "Hoda.semi1"); + + debug_assert_eq!( + enc, + "ac57663c18c81ad0423edb235d5b1059d792c2a18d5fad70a83a4afa92affabb" + ); + } } diff --git a/src/discover.rs b/src/discover.rs index 2b477ff..2032cfc 100644 --- a/src/discover.rs +++ b/src/discover.rs @@ -196,7 +196,7 @@ mod test { #[test] fn connect() -> Result<()> { for device_info in Startup::discover()? { - let device = Device::connect(device_info)?; + Device::connect(device_info)?; } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index c8e4e84..88e203a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,3 +11,6 @@ fn hex(s: &str) -> Result, ParseIntError> { .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) .collect() } + +pub use cloud::Cloud; +pub use discover::{Device, DeviceInfo, Startup};