Implement cloud connect

This commit is contained in:
hodasemi 2023-09-23 16:42:59 +02:00
parent e7527ddc40
commit 99819f36d9
8 changed files with 323 additions and 103 deletions

View file

@ -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"

View file

@ -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:

View file

@ -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:

View file

@ -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()

View file

@ -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)
.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<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)?;
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<String, String>,
}
#[derive(Serialize, Debug)]
struct Data<'a> {
appKey: &'a str,
deviceId: &'a str,
platform: &'a str,
}

View file

@ -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"
);
}
}

View file

@ -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(())

View file

@ -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};