Implement cloud connect
This commit is contained in:
parent
e7527ddc40
commit
99819f36d9
8 changed files with 323 additions and 103 deletions
|
@ -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"
|
||||
|
|
4
cloud.py
4
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:
|
||||
|
|
6
midea.py
6
midea.py
|
@ -6,9 +6,6 @@ import discover;
|
|||
import device;
|
||||
|
||||
async def test():
|
||||
devices = discover.discover()
|
||||
|
||||
for device_id in devices:
|
||||
cl = cloud.MSmartHomeCloud(
|
||||
"MSmartHome",
|
||||
aiohttp.ClientSession(),
|
||||
|
@ -17,6 +14,9 @@ async def test():
|
|||
)
|
||||
|
||||
if await cl.login():
|
||||
devices = discover.discover()
|
||||
|
||||
for device_id in devices:
|
||||
keys = await cl.get_keys(device_id)
|
||||
|
||||
for k in keys:
|
||||
|
|
25
security.py
25
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()
|
||||
|
|
299
src/cloud.rs
299
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<String>,
|
||||
api_url: String,
|
||||
access_token: Option<String>,
|
||||
|
@ -27,6 +28,15 @@ pub struct Cloud {
|
|||
security: CloudSecurity,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct Input {
|
||||
iotData: HashMap<String, String>,
|
||||
data: HashMap<String, String>,
|
||||
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<Self> {
|
||||
pub fn new(account: impl ToString, password: impl ToString) -> Result<Self> {
|
||||
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<String, String>,
|
||||
dump_data: String,
|
||||
header: Option<HeaderMap>,
|
||||
) -> Result<String> {
|
||||
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)
|
||||
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 mut data = HashMap::new();
|
||||
data.insert("iotData".to_string(), iot_data_dump);
|
||||
data.insert(
|
||||
"data".to_string(),
|
||||
to_string(&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",
|
||||
})?,
|
||||
);
|
||||
data.insert("stamp".to_string(), stamp);
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
stamp,
|
||||
reqId: "25f278357a1b1c08cf878b05ade7db26".to_string(),
|
||||
};
|
||||
|
||||
let response: Response = from_str(&self.api_request("/mj/user/login", data, None).await?)?;
|
||||
let dump_i = to_string(&i).unwrap();
|
||||
|
||||
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"))?,
|
||||
);
|
||||
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.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<Response> {
|
||||
if let Ok(r) = from_str::<ResponseNumber>(&s) {
|
||||
return Some(Response::from(r));
|
||||
}
|
||||
|
||||
if let Ok(r) = from_str::<ResponseString>(&s) {
|
||||
return Some(Response::from(r));
|
||||
}
|
||||
|
||||
if let Ok(r) = from_str::<ResponseMap>(&s) {
|
||||
return Some(Response::from(r));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Response {
|
||||
Normal(ResponseNumber),
|
||||
NestedMap(ResponseMap),
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn normal_data(&self) -> &HashMap<String, String> {
|
||||
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<ResponseNumber> for Response {
|
||||
fn from(value: ResponseNumber) -> Self {
|
||||
Self::Normal(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseString> for Response {
|
||||
fn from(value: ResponseString) -> Self {
|
||||
Self::Normal(ResponseNumber {
|
||||
msg: value.msg,
|
||||
code: value.code.parse().unwrap(),
|
||||
data: value.data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseMap> for Response {
|
||||
fn from(value: ResponseMap) -> Self {
|
||||
Self::NestedMap(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ResponseNumber {
|
||||
msg: String,
|
||||
code: i32,
|
||||
pub data: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ResponseString {
|
||||
msg: String,
|
||||
code: String,
|
||||
pub data: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[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<String, Option<String>>,
|
||||
doDeviceBind: Option<String>,
|
||||
accessToken: String,
|
||||
signUnlockEnabled: Option<String>,
|
||||
}
|
||||
|
||||
#[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)?;
|
||||
|
||||
let id = cloud.login_id().await?;
|
||||
|
||||
println!("{id:?}")
|
||||
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]
|
||||
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<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct Data<'a> {
|
||||
appKey: &'a str,
|
||||
deviceId: &'a str,
|
||||
platform: &'a str,
|
||||
}
|
||||
|
|
|
@ -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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -11,3 +11,6 @@ fn hex(s: &str) -> Result<Vec<u8>, ParseIntError> {
|
|||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub use cloud::Cloud;
|
||||
pub use discover::{Device, DeviceInfo, Startup};
|
||||
|
|
Loading…
Reference in a new issue