Start implementing cloud login
This commit is contained in:
parent
223b7148af
commit
e7527ddc40
7 changed files with 355 additions and 7 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -9,4 +9,12 @@ edition = "2021"
|
||||||
anyhow = { version = "1.0.75", features = ["backtrace"] }
|
anyhow = { version = "1.0.75", features = ["backtrace"] }
|
||||||
if-addrs = "0.10.1"
|
if-addrs = "0.10.1"
|
||||||
aes = "0.8.3"
|
aes = "0.8.3"
|
||||||
rand = "0.8.5"
|
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"] }
|
||||||
|
|
3
cloud.py
3
cloud.py
|
@ -73,7 +73,8 @@ class MideaCloud:
|
||||||
data.update({
|
data.update({
|
||||||
"stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
"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
|
url = self._api_url + endpoint
|
||||||
dump_data = json.dumps(data)
|
dump_data = json.dumps(data)
|
||||||
sign = self._security.sign(dump_data, random)
|
sign = self._security.sign(dump_data, random)
|
||||||
|
|
12
security.py
12
security.py
|
@ -26,8 +26,16 @@ class CloudSecurity:
|
||||||
msg = self._iot_key
|
msg = self._iot_key
|
||||||
msg += data
|
msg += data
|
||||||
msg += random
|
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):
|
def encrypt_password(self, login_id, data):
|
||||||
m = sha256()
|
m = sha256()
|
||||||
|
|
277
src/cloud.rs
277
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 {
|
pub struct Cloud {
|
||||||
//
|
device_id: u64,
|
||||||
|
uid: Option<String>,
|
||||||
|
api_url: String,
|
||||||
|
access_token: Option<String>,
|
||||||
|
auth_base: String,
|
||||||
|
login_id: Option<String>,
|
||||||
|
|
||||||
|
account: String,
|
||||||
|
password: String,
|
||||||
|
|
||||||
|
security: CloudSecurity,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cloud {
|
impl Cloud {
|
||||||
|
pub const APP_ID: &str = "1010";
|
||||||
pub const APP_KEY: &str = "ac21b9f9cbfe4ca5a88562ef25e2b768";
|
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<Self> {
|
||||||
|
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<String, String> {
|
||||||
|
[
|
||||||
|
("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<String, 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()
|
||||||
|
.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<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct Data<'a> {
|
||||||
|
appKey: &'a str,
|
||||||
|
deviceId: &'a str,
|
||||||
|
platform: &'a str,
|
||||||
}
|
}
|
||||||
|
|
55
src/cloud_security.rs
Normal file
55
src/cloud_security.rs
Normal file
|
@ -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<String>,
|
||||||
|
random_data: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Sha256>;
|
||||||
|
|
||||||
|
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::<Vec<String>>()
|
||||||
|
.join("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::cloud::Cloud;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DeviceInfo {
|
pub struct DeviceInfo {
|
||||||
id: u64,
|
pub id: u64,
|
||||||
model: String,
|
model: String,
|
||||||
sn: String,
|
sn: String,
|
||||||
protocol: u8,
|
protocol: u8,
|
||||||
|
@ -65,7 +65,7 @@ impl Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Startup;
|
pub struct Startup;
|
||||||
|
|
||||||
impl Startup {
|
impl Startup {
|
||||||
const BROADCAST_MSG: &'static [u8] = &[
|
const BROADCAST_MSG: &'static [u8] = &[
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
|
|
||||||
mod cloud;
|
mod cloud;
|
||||||
|
mod cloud_security;
|
||||||
mod discover;
|
mod discover;
|
||||||
mod security;
|
mod security;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue