From 3122f03e9ec6a56bdc8ab87513b2257a3f985881 Mon Sep 17 00:00:00 2001 From: hodasemi Date: Sun, 24 Sep 2023 13:16:57 +0200 Subject: [PATCH] Test device authentication --- Cargo.toml | 15 ++++--- cloud.py | 4 +- device.py | 6 +++ discover.py | 7 --- midea.py | 63 ++++++++++++++++++--------- security.py | 3 -- src/device.rs | 101 ++++++++++++++++++++++++++++++++++++++------ src/discover.rs | 25 ++++++++++- src/security.rs | 110 ++++++++++++++++++++++++++++++++++++++++++------ 9 files changed, 270 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c486c3a..79cb768 100644 --- a/Cargo.toml +++ b/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" diff --git a/cloud.py b/cloud.py index 967a469..5e39b1d 100644 --- a/cloud.py +++ b/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: diff --git a/device.py b/device.py index b049bcb..ea5c1c1 100644 --- a/device.py +++ b/device.py @@ -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: diff --git a/discover.py b/discover.py index 2e41786..edd295e 100644 --- a/discover.py +++ b/discover.py @@ -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]) diff --git a/midea.py b/midea.py index e6a79c7..1c7290d 100644 --- a/midea.py +++ b/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()) diff --git a/security.py b/security.py index 5c2172e..09dd3d5 100644 --- a/security.py +++ b/security.py @@ -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() diff --git a/src/device.rs b/src/device.rs index 6e48a8b..fef7b85 100644 --- a/src/device.rs +++ b/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 { - let socket = UdpSocket::bind("0.0.0.0:0")?; + pub fn connect(info: DeviceInfo, token: &str, key: &str) -> Result { + 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(()) + } } diff --git a/src/discover.rs b/src/discover.rs index 42cdd90..3836ebb 100644 --- a/src/discover.rs +++ b/src/discover.rs @@ -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 { diff --git a/src/security.rs b/src/security.rs index a77cabd..b0689ff 100644 --- a/src/security.rs +++ b/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 { - let mut header = hex("83,70")?; - let mut data: Vec = Vec::new(); + pub fn aes_cbc_encrypt(&self, raw: &[u8], key: &[u8; 32]) -> Vec { + type Aes256CbcEnc = cbc::Encryptor; - 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::(&mut buf, raw.len()) + .unwrap(); + + buf + } + + pub fn aes_cbc_decrypt(&self, raw: &[u8], key: &[u8; 32]) -> Vec { + type Aes256CbcDec = cbc::Decryptor; + + let mut buf = vec![0; raw.len()]; + buf.copy_from_slice(raw); + + Aes256CbcDec::new(key.into(), &Self::IV.into()) + .decrypt_padded_mut::(&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::>() != 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 { + 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> { + let mut header = hex("8370")?; + let mut data: Vec = 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 = 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, rhs: Vec) -> Vec { + lhs.extend(rhs); + lhs } }