Test device authentication
This commit is contained in:
parent
cf88642612
commit
3122f03e9e
9 changed files with 270 additions and 64 deletions
15
Cargo.toml
15
Cargo.toml
|
@ -8,15 +8,18 @@ edition = "2021"
|
|||
[dependencies]
|
||||
anyhow = { version = "1.0.75", features = ["backtrace"] }
|
||||
if-addrs = "0.10.1"
|
||||
aes = "0.8.3"
|
||||
rand = "0.8.5"
|
||||
chrono = "0.4.31"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
reqwest = "0.11.20"
|
||||
tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] }
|
||||
futures = "0.3.28"
|
||||
|
||||
#crypto
|
||||
cbc = "0.1.2"
|
||||
md5 = "0.7.0"
|
||||
base64 = "0.21.4"
|
||||
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"] }
|
||||
md5 = "0.7.0"
|
||||
futures = "0.3.28"
|
||||
aes = "0.8.3"
|
||||
|
|
4
cloud.py
4
cloud.py
|
@ -73,8 +73,7 @@ class MideaCloud:
|
|||
data.update({
|
||||
"stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
})
|
||||
random = str(1695459986)
|
||||
# random = str(int(time.time()))
|
||||
random = str(int(time.time()))
|
||||
url = self._api_url + endpoint
|
||||
dump_data = json.dumps(data)
|
||||
sign = self._security.sign(dump_data, random)
|
||||
|
@ -102,6 +101,7 @@ class MideaCloud:
|
|||
pass
|
||||
if int(response["code"]) == 0 and "data" in response:
|
||||
return response["data"]
|
||||
print(response)
|
||||
return None
|
||||
|
||||
async def _get_login_id(self) -> str | None:
|
||||
|
|
|
@ -73,6 +73,11 @@ class MiedaDevice(threading.Thread):
|
|||
self._heartbeat_interval = 10
|
||||
self._default_refresh_interval = 30
|
||||
|
||||
print(token)
|
||||
print(self._token)
|
||||
|
||||
k = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._device_name
|
||||
|
@ -146,6 +151,7 @@ class MiedaDevice(threading.Thread):
|
|||
request = self._security.encode_8370(
|
||||
self._token, MSGTYPE_HANDSHAKE_REQUEST)
|
||||
_LOGGER.debug(f"[{self._device_id}] Handshaking")
|
||||
print(request)
|
||||
self._socket.send(request)
|
||||
response = self._socket.recv(512)
|
||||
if len(response) < 20:
|
||||
|
|
|
@ -71,13 +71,6 @@ def discover(discover_type=None, ip_address=None):
|
|||
encrypt_data = data[40:-16]
|
||||
reply = security.aes_decrypt(encrypt_data)
|
||||
_LOGGER.debug(f"Declassified reply: {reply.hex()}")
|
||||
|
||||
start = 41
|
||||
end = start + reply[40]
|
||||
|
||||
ssid_bytes = reply[start:end]
|
||||
print(ssid_bytes)
|
||||
|
||||
ssid = reply[41:41 + reply[40]].decode("utf-8")
|
||||
device_type = ssid.split("_")[1]
|
||||
port = bytes2port(reply[4:8])
|
||||
|
|
63
midea.py
63
midea.py
|
@ -15,31 +15,54 @@ async def test():
|
|||
|
||||
devices = discover.discover()
|
||||
|
||||
if await cl.login():
|
||||
if len(devices) > 0:
|
||||
for device_id in devices:
|
||||
keys = await cl.get_keys(device_id)
|
||||
token = "06df24fc4e8e950c6d9783051b8e38d971e5fbc617da259459d30d5e7d7fc05b4ccb708fe3a085f6f0af0f8cc961fa39dabfd0746f7bbcfbf7404d9cc5c2b077"
|
||||
key = "2a5b5200c2c04d4c811d0550e1dc5b31435436b95b774d2a88d7e46d61fd9669"
|
||||
|
||||
for k in keys:
|
||||
token = keys[k]['token']
|
||||
key = keys[k]['key']
|
||||
device_info = devices[device_id]
|
||||
|
||||
device_info = devices[device_id]
|
||||
dev = device.MiedaDevice(
|
||||
name="",
|
||||
device_id=device_id,
|
||||
device_type=225,
|
||||
ip_address=device_info['ip_address'],
|
||||
port=device_info['port'],
|
||||
token=token,
|
||||
key=key,
|
||||
protocol=3,
|
||||
model=device_info['model'],
|
||||
attributes={}
|
||||
)
|
||||
|
||||
dev = device.MiedaDevice(
|
||||
name="",
|
||||
device_id=device_id,
|
||||
device_type=225,
|
||||
ip_address=device_info['ip_address'],
|
||||
port=device_info['port'],
|
||||
token=token,
|
||||
key=key,
|
||||
protocol=3,
|
||||
model=device_info['model'],
|
||||
attributes={}
|
||||
)
|
||||
dev.connect(False)
|
||||
|
||||
if dev.connect(False):
|
||||
return dev
|
||||
|
||||
# if await cl.login():
|
||||
# for device_id in devices:
|
||||
# keys = await cl.get_keys(device_id)
|
||||
|
||||
# for k in keys:
|
||||
# token = keys[k]['token']
|
||||
# key = keys[k]['key']
|
||||
|
||||
# device_info = devices[device_id]
|
||||
|
||||
# dev = device.MiedaDevice(
|
||||
# name="",
|
||||
# device_id=device_id,
|
||||
# device_type=225,
|
||||
# ip_address=device_info['ip_address'],
|
||||
# port=device_info['port'],
|
||||
# token=token,
|
||||
# key=key,
|
||||
# protocol=3,
|
||||
# model=device_info['model'],
|
||||
# attributes={}
|
||||
# )
|
||||
|
||||
# if dev.connect(False):
|
||||
# return dev
|
||||
|
||||
dev = asyncio.run(test())
|
||||
|
||||
|
|
|
@ -38,9 +38,6 @@ 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"))
|
||||
m_hex = m.hexdigest()
|
||||
|
|
101
src/device.rs
101
src/device.rs
|
@ -1,8 +1,14 @@
|
|||
use std::{net::UdpSocket, time::Duration};
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::TcpStream,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Context, Error, Result};
|
||||
|
||||
use crate::{
|
||||
hex,
|
||||
security::{MsgType, Security},
|
||||
DeviceInfo,
|
||||
};
|
||||
|
@ -10,22 +16,38 @@ use crate::{
|
|||
pub struct Device {
|
||||
info: DeviceInfo,
|
||||
|
||||
socket: UdpSocket,
|
||||
socket: TcpStream,
|
||||
security: Security,
|
||||
|
||||
token: [u8; 64],
|
||||
key: [u8; 32],
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn connect(info: DeviceInfo) -> Result<Self> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
||||
pub fn connect(info: DeviceInfo, token: &str, key: &str) -> Result<Self> {
|
||||
let mut socket = Err(Error::msg(""));
|
||||
|
||||
for _ in 0..10 {
|
||||
socket = TcpStream::connect(info.addr).context(info.addr);
|
||||
|
||||
if socket.is_ok() {
|
||||
break;
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
|
||||
let socket = socket?;
|
||||
socket.set_write_timeout(Some(Duration::from_secs(10)))?;
|
||||
socket.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||
|
||||
socket.connect(info.addr)?;
|
||||
|
||||
let mut me = Self {
|
||||
info,
|
||||
socket,
|
||||
security: Security::default(),
|
||||
|
||||
token: hex(token)?.try_into().unwrap(),
|
||||
key: hex(key)?.try_into().unwrap(),
|
||||
};
|
||||
|
||||
if me.info.protocol == 3 {
|
||||
|
@ -38,7 +60,20 @@ impl Device {
|
|||
}
|
||||
|
||||
fn authenticate(&mut self) -> Result<()> {
|
||||
let request = self.security.encode_8370(MsgType::HANDSHAKE_REQUEST)?;
|
||||
let request = self
|
||||
.security
|
||||
.encode_8370(&self.token, MsgType::HANDSHAKE_REQUEST)?;
|
||||
|
||||
self.socket.write(&request)?;
|
||||
|
||||
let mut buffer = [0; 512];
|
||||
let bytes_read = self.socket.read(&mut buffer)?;
|
||||
|
||||
if bytes_read < 20 {
|
||||
bail!("Authentication failed!");
|
||||
}
|
||||
|
||||
self.security.tcp_key(&buffer[8..72], &self.key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -52,16 +87,58 @@ impl Device {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use anyhow::Result;
|
||||
use std::net::UdpSocket;
|
||||
|
||||
use crate::{device::Device, Startup};
|
||||
use anyhow::Result;
|
||||
use futures::future::try_join;
|
||||
|
||||
use crate::{device::Device, Cloud, Startup};
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect() -> Result<()> {
|
||||
for device_info in Startup::discover().await? {
|
||||
Device::connect(device_info)?;
|
||||
let devices = Startup::discover().await?;
|
||||
|
||||
const TOKEN:&str = "06df24fc4e8e950c6d9783051b8e38d971e5fbc617da259459d30d5e7d7fc05b4ccb708fe3a085f6f0af0f8cc961fa39dabfd0746f7bbcfbf7404d9cc5c2b077";
|
||||
const KEY: &str = "2a5b5200c2c04d4c811d0550e1dc5b31435436b95b774d2a88d7e46d61fd9669";
|
||||
|
||||
let token_hex = b"\x06\xdf$\xfcN\x8e\x95\x0cm\x97\x83\x05\x1b\x8e8\xd9q\xe5\xfb\xc6\x17\xda%\x94Y\xd3\r^}\x7f\xc0[L\xcbp\x8f\xe3\xa0\x85\xf6\xf0\xaf\x0f\x8c\xc9a\xfa9\xda\xbf\xd0to{\xbc\xfb\xf7@M\x9c\xc5\xc2\xb0w";
|
||||
let key_hex = b"*[R\x00\xc2\xc0ML\x81\x1d\x05P\xe1\xdc[1CT6\xb9[wM*\x88\xd7\xe4ma\xfd\x96i";
|
||||
|
||||
for device_info in devices {
|
||||
let device = Device::connect(device_info, TOKEN, KEY)?;
|
||||
|
||||
assert_eq!(&device.token, token_hex);
|
||||
assert_eq!(&device.key, key_hex);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_flow() -> Result<()> {
|
||||
let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?;
|
||||
|
||||
let (_, devices) = try_join(cloud.login(), Startup::discover()).await?;
|
||||
|
||||
for device_info in devices {
|
||||
let (token, key) = cloud.keys(device_info.id).await?;
|
||||
|
||||
Device::connect(device_info, &token, &key)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_socket() -> Result<()> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
||||
socket.connect("192.168.178.94:6445")?;
|
||||
|
||||
socket.send(b"test")?;
|
||||
|
||||
let mut buffer = [0; 512];
|
||||
let bytes_read = socket.recv(&mut buffer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ impl Startup {
|
|||
socket.send_to(Self::BROADCAST_MSG, (addr, 20086))?;
|
||||
|
||||
let mut buffer = [0; 1024];
|
||||
if let Ok((bytes_read, addr)) = socket.recv_from(&mut buffer) {
|
||||
if let Ok((bytes_read, mut addr)) = socket.recv_from(&mut buffer) {
|
||||
let mut bytes = buffer[0..bytes_read].to_vec();
|
||||
|
||||
let protocol = if bytes[..2] == hex("5a5a")? {
|
||||
|
@ -72,7 +72,7 @@ impl Startup {
|
|||
}
|
||||
|
||||
let len = bytes.len();
|
||||
let encrypt_data = Security::decrypt(&mut bytes[40..(len - 16)]);
|
||||
let encrypt_data = Security::aes_decrypt(&mut bytes[40..(len - 16)]);
|
||||
|
||||
let start = 41;
|
||||
let upper = start + encrypt_data[40] as usize;
|
||||
|
@ -81,6 +81,9 @@ impl Startup {
|
|||
let device_type = u32::from_str_radix(ssid.split('_').nth(1).unwrap(), 16)?;
|
||||
let model = from_utf8(&encrypt_data[17..25])?;
|
||||
let sn = from_utf8(&encrypt_data[8..40])?;
|
||||
let port = Self::bytes_to_port(&encrypt_data[4..8]);
|
||||
|
||||
addr.set_port(port);
|
||||
|
||||
devices.insert(
|
||||
device_id,
|
||||
|
@ -111,6 +114,24 @@ impl Startup {
|
|||
)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn bytes_to_port(bytes: &[u8]) -> u16 {
|
||||
let mut b = 0;
|
||||
let mut i: u32 = 0;
|
||||
|
||||
while b < 4 {
|
||||
let b1 = if b < bytes.len() {
|
||||
(bytes[b] & 0xFF) as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
i |= b1 << b * 8;
|
||||
b += 1;
|
||||
}
|
||||
|
||||
i as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl Startup {
|
||||
|
|
110
src/security.rs
110
src/security.rs
|
@ -1,15 +1,19 @@
|
|||
use aes::{
|
||||
cipher::{generic_array::GenericArray, BlockDecrypt, KeyInit},
|
||||
cipher::{
|
||||
block_padding::Pkcs7, generic_array::GenericArray, BlockDecrypt, BlockDecryptMut,
|
||||
BlockEncryptMut, KeyInit, KeyIvInit,
|
||||
},
|
||||
Aes128,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use rand::{self, RngCore};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::hex;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
||||
pub enum MsgType {
|
||||
HANDSHAKE_REQUEST = 0x0,
|
||||
ANDSHAKE_RESPONSE = 0x1,
|
||||
|
@ -19,15 +23,18 @@ pub enum MsgType {
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Security {
|
||||
request_count: u32,
|
||||
response_count: u32,
|
||||
request_count: u16,
|
||||
response_count: u16,
|
||||
|
||||
tcp_key: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl Security {
|
||||
const N: u128 = 141661095494369103254425781617665632877;
|
||||
const KEY: [u8; 16] = Self::N.to_be_bytes();
|
||||
const IV: [u8; 16] = [b'\0'; 16];
|
||||
|
||||
pub fn decrypt(data: &mut [u8]) -> &[u8] {
|
||||
pub fn aes_decrypt(data: &mut [u8]) -> &[u8] {
|
||||
let array = GenericArray::from(Self::KEY);
|
||||
let cipher = Aes128::new(&array);
|
||||
|
||||
|
@ -39,11 +46,64 @@ impl Security {
|
|||
data
|
||||
}
|
||||
|
||||
pub fn encode_8370(&mut self, msg_type: MsgType) -> Result<String> {
|
||||
let mut header = hex("83,70")?;
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
pub fn aes_cbc_encrypt(&self, raw: &[u8], key: &[u8; 32]) -> Vec<u8> {
|
||||
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
|
||||
|
||||
let mut size = data.len();
|
||||
let mut buf = vec![0; raw.len()];
|
||||
buf.copy_from_slice(raw);
|
||||
|
||||
Aes256CbcEnc::new(key.into(), &Self::IV.into())
|
||||
.encrypt_padded_mut::<Pkcs7>(&mut buf, raw.len())
|
||||
.unwrap();
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn aes_cbc_decrypt(&self, raw: &[u8], key: &[u8; 32]) -> Vec<u8> {
|
||||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||
|
||||
let mut buf = vec![0; raw.len()];
|
||||
buf.copy_from_slice(raw);
|
||||
|
||||
Aes256CbcDec::new(key.into(), &Self::IV.into())
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut buf)
|
||||
.unwrap();
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn tcp_key(&mut self, response: &[u8], key: &[u8; 32]) -> Result<()> {
|
||||
if response == b"ERROR" {
|
||||
bail!("authentication failed!");
|
||||
}
|
||||
|
||||
let payload = &response[0..32];
|
||||
let sign = &response[32..];
|
||||
|
||||
let plain = self.aes_cbc_decrypt(payload, &key);
|
||||
|
||||
if Sha256::digest(&plain).into_iter().collect::<Vec<u8>>() != sign {
|
||||
bail!("sign does not match");
|
||||
}
|
||||
|
||||
self.tcp_key = Some(Self::xorstr(&plain, key).try_into().unwrap());
|
||||
self.request_count = 0;
|
||||
self.response_count = 0;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn xorstr(lhs: &[u8], rhs: &[u8]) -> Vec<u8> {
|
||||
assert_eq!(lhs.len(), rhs.len());
|
||||
|
||||
lhs.iter().zip(rhs.iter()).map(|(&l, &r)| l ^ r).collect()
|
||||
}
|
||||
|
||||
pub fn encode_8370(&mut self, msg: &[u8], msg_type: MsgType) -> Result<Vec<u8>> {
|
||||
let mut header = hex("8370")?;
|
||||
let mut data: Vec<u8> = msg.to_vec();
|
||||
|
||||
let mut size = data.len() as u16;
|
||||
let mut padding = 0;
|
||||
|
||||
if msg_type == MsgType::ENCRYPTED_RESPONSE || msg_type == MsgType::ENCRYPTED_REQUEST {
|
||||
|
@ -51,7 +111,7 @@ impl Security {
|
|||
padding = 16 - ((size + 2) & 0xf);
|
||||
size += padding + 32;
|
||||
data.extend({
|
||||
let mut d = vec![0; padding];
|
||||
let mut d = vec![0; padding as usize];
|
||||
rand::thread_rng().fill_bytes(&mut d);
|
||||
|
||||
d
|
||||
|
@ -59,6 +119,32 @@ impl Security {
|
|||
}
|
||||
}
|
||||
|
||||
todo!()
|
||||
header.extend(size.to_be_bytes());
|
||||
header.extend([0x20, (padding << 4) as u8 | msg_type as u8]);
|
||||
|
||||
data = {
|
||||
let mut b = self.request_count.to_be_bytes().to_vec();
|
||||
b.extend(data);
|
||||
b
|
||||
};
|
||||
|
||||
(self.request_count, _) = self.request_count.overflowing_add(1);
|
||||
|
||||
if msg_type == MsgType::ENCRYPTED_RESPONSE || msg_type == MsgType::ENCRYPTED_REQUEST {
|
||||
let sign: Vec<u8> = Sha256::digest(Self::add_bytes(header.clone(), data.clone()))
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
data = self.aes_cbc_encrypt(&data, self.tcp_key.as_ref().unwrap());
|
||||
data.extend(sign);
|
||||
}
|
||||
|
||||
header.extend(data);
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
fn add_bytes(mut lhs: Vec<u8>, rhs: Vec<u8>) -> Vec<u8> {
|
||||
lhs.extend(rhs);
|
||||
lhs
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue