From 1fdd2a3f1e4abba00c2813102ff38135e5f15b93 Mon Sep 17 00:00:00 2001 From: hodasemi Date: Mon, 25 Sep 2023 11:30:54 +0200 Subject: [PATCH] More connection testing --- Cargo.toml | 3 +- device.py | 15 ++++++++++ midea.py | 10 +++++-- src/cloud.rs | 21 ++++++++++++-- src/device.rs | 68 +++++++++++++++++++++++++++++-------------- src/discover.rs | 4 +-- src/security.rs | 77 ++++++++++++++++++++++++++++++++----------------- 7 files changed, 141 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79cb768..293f172 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,10 @@ serde_json = "1.0.107" reqwest = "0.11.20" tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] } futures = "0.3.28" +serial_test = "2.0.0" #crypto -cbc = "0.1.2" +cbc = { version = "0.1.2", features = ["alloc"] } md5 = "0.7.0" base64 = "0.21.4" hmac = "0.12.1" diff --git a/device.py b/device.py index ea5c1c1..103f824 100644 --- a/device.py +++ b/device.py @@ -151,9 +151,24 @@ class MiedaDevice(threading.Thread): request = self._security.encode_8370( self._token, MSGTYPE_HANDSHAKE_REQUEST) _LOGGER.debug(f"[{self._device_id}] Handshaking") + print("REQUEST") print(request) + + req = bytearray([131, 112, 0, 64, 32, 0, 0, 0, 112, 43, 157, 252, 58, 198, 200, 41, 121, 152, 110, 227, 160, 83, 167, 111, 117, 249, 233, 199, 99, 206, 92, 37, 175, 92, 44, 201, 130, 247, 151, 169, 64, 154, 223, 243, 116, 94, 35, 254, 227, 164, 100, 215, 69, 224, 5, 200, 57, 239, 176, 184, 64, 130, 172, 201, 98, 229, 154, 184, 104, 62, 2, 153]) + + if (req == request): + print("both requests are the same") + + self._socket.send(req) + response = self._socket.recv(512) + + print(response) + self._socket.send(request) response = self._socket.recv(512) + + print(response) + if len(response) < 20: raise AuthException() response = response[8: 72] diff --git a/midea.py b/midea.py index 1c7290d..a1a93a4 100644 --- a/midea.py +++ b/midea.py @@ -17,8 +17,8 @@ async def test(): if len(devices) > 0: for device_id in devices: - token = "06df24fc4e8e950c6d9783051b8e38d971e5fbc617da259459d30d5e7d7fc05b4ccb708fe3a085f6f0af0f8cc961fa39dabfd0746f7bbcfbf7404d9cc5c2b077" - key = "2a5b5200c2c04d4c811d0550e1dc5b31435436b95b774d2a88d7e46d61fd9669" + token = "702b9dfc3ac6c82979986ee3a053a76f75f9e9c763ce5c25af5c2cc982f797a9409adff3745e23fee3a464d745e005c839efb0b84082acc962e59ab8683e0299" + key = "52b2feee353841588994e630dcb59819ec71ce1ffacb48628f4f436f5c54f11e" device_info = devices[device_id] @@ -35,7 +35,11 @@ async def test(): attributes={} ) - dev.connect(False) + if dev.connect(False): + print("success") + else: + print("fail") + # if await cl.login(): diff --git a/src/cloud.rs b/src/cloud.rs index 838c7a8..0df4150 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_string}; use crate::cloud_security::CloudSecurity; +use crate::hex; pub struct Cloud { device_id: String, @@ -238,7 +239,7 @@ impl Cloud { } pub async fn keys(&self, device_id: u64) -> Result<(String, String)> { - for method in [1, 2] { + for method in [2] { let udp_id = CloudSecurity::udp_id(device_id, method); let mut data = self.make_general_data(); @@ -252,13 +253,26 @@ impl Cloud { for token in response.token_list() { if token.udpId == udp_id { - return Ok((token.token.clone(), token.key.clone())); + return Ok(( + Self::hex_to_lower(&token.token)?, + Self::hex_to_lower(&token.key)?, + )); } } } bail!("no keys found") } + + fn hex_to_lower(h: &str) -> Result { + let lower = hex(h)? + .iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join(""); + + Ok(lower) + } } #[derive(Debug)] @@ -453,6 +467,7 @@ struct Data<'a> { mod test { use anyhow::Result; use futures::future::try_join; + use serial_test::serial; use crate::Startup; @@ -480,6 +495,7 @@ mod test { } #[tokio::test] + #[serial] async fn login() -> Result<()> { let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; @@ -489,6 +505,7 @@ mod test { } #[tokio::test] + #[serial] async fn keys() -> Result<()> { let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; diff --git a/src/device.rs b/src/device.rs index fef7b85..7a4f231 100644 --- a/src/device.rs +++ b/src/device.rs @@ -64,13 +64,21 @@ impl Device { .security .encode_8370(&self.token, MsgType::HANDSHAKE_REQUEST)?; + const PY_REQUEST :&[u8;72] = b"\x83p\x00@ \x00\x00\x00p+\x9d\xfc:\xc6\xc8)y\x98n\xe3\xa0S\xa7ou\xf9\xe9\xc7c\xce\\%\xaf\\,\xc9\x82\xf7\x97\xa9@\x9a\xdf\xf3t^#\xfe\xe3\xa4d\xd7E\xe0\x05\xc89\xef\xb0\xb8@\x82\xac\xc9b\xe5\x9a\xb8h>\x02\x99"; + + // assert_eq!(request, PY_REQUEST); + + println!("writing request to stream: {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!"); + bail!( + "Authentication failed! (answer too short) {:?}", + &buffer[..bytes_read] + ); } self.security.tcp_key(&buffer[8..72], &self.key)?; @@ -87,25 +95,24 @@ impl Device { #[cfg(test)] mod test { - use std::net::UdpSocket; - - use anyhow::Result; + use anyhow::{Context, Result}; use futures::future::try_join; + use serial_test::serial; use crate::{device::Device, Cloud, Startup}; #[tokio::test] - async fn connect() -> Result<()> { + async fn verify_hex() -> Result<()> { let devices = Startup::discover().await?; - const TOKEN:&str = "06df24fc4e8e950c6d9783051b8e38d971e5fbc617da259459d30d5e7d7fc05b4ccb708fe3a085f6f0af0f8cc961fa39dabfd0746f7bbcfbf7404d9cc5c2b077"; - const KEY: &str = "2a5b5200c2c04d4c811d0550e1dc5b31435436b95b774d2a88d7e46d61fd9669"; + const PY_TOKEN: &str = "06df24fc4e8e950c6d9783051b8e38d971e5fbc617da259459d30d5e7d7fc05b4ccb708fe3a085f6f0af0f8cc961fa39dabfd0746f7bbcfbf7404d9cc5c2b077"; + const PY_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)?; + let device = Device::connect(device_info, PY_TOKEN, PY_KEY)?; assert_eq!(&device.token, token_hex); assert_eq!(&device.key, key_hex); @@ -115,6 +122,35 @@ mod test { } #[tokio::test] + async fn connect_py_token() -> Result<()> { + let devices = Startup::discover().await?; + + const PY_TOKEN: &str = "18a821cb88293c6552dc576f0672d8b9445205f74b636764929de5e8badfa48a24caa9d741f632a18e1a9fee67c40b0b40edc21ac7c4c40b6352181cd4000203"; + const PY_KEY: &str = "0fc0c56ea8124414a362e6449ee45ba92558a54f159d4937af697e405f2326b9"; + + for device_info in devices { + Device::connect(device_info, PY_TOKEN, PY_KEY)?; + } + + Ok(()) + } + + #[tokio::test] + async fn connect_rust_token() -> Result<()> { + let devices = Startup::discover().await?; + + const TOKEN: &str = "702b9dfc3ac6c82979986ee3a053a76f75f9e9c763ce5c25af5c2cc982f797a9409adff3745e23fee3a464d745e005c839efb0b84082acc962e59ab8683e0299"; + const KEY: &str = "52b2feee353841588994e630dcb59819ec71ce1ffacb48628f4f436f5c54f11e"; + + for device_info in devices { + Device::connect(device_info, TOKEN, KEY)?; + } + + Ok(()) + } + + #[tokio::test] + #[serial] async fn full_flow() -> Result<()> { let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?; @@ -123,22 +159,10 @@ mod test { for device_info in devices { let (token, key) = cloud.keys(device_info.id).await?; - Device::connect(device_info, &token, &key)?; + Device::connect(device_info, &token, &key) + .context(format!("\ntoken: {token}\nkey: {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 3836ebb..8aaf6b2 100644 --- a/src/discover.rs +++ b/src/discover.rs @@ -176,9 +176,7 @@ mod test { #[tokio::test] async fn discover() -> Result<()> { - let devices = Startup::discover().await?; - - println!("{devices:#?}"); + Startup::discover().await?; Ok(()) } diff --git a/src/security.rs b/src/security.rs index b0689ff..8a3416b 100644 --- a/src/security.rs +++ b/src/security.rs @@ -1,6 +1,6 @@ use aes::{ cipher::{ - block_padding::Pkcs7, generic_array::GenericArray, BlockDecrypt, BlockDecryptMut, + block_padding::NoPadding, generic_array::GenericArray, BlockDecrypt, BlockDecryptMut, BlockEncryptMut, KeyInit, KeyIvInit, }, Aes128, @@ -16,7 +16,7 @@ use crate::hex; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub enum MsgType { HANDSHAKE_REQUEST = 0x0, - ANDSHAKE_RESPONSE = 0x1, + HANDSHAKE_RESPONSE = 0x1, ENCRYPTED_RESPONSE = 0x3, ENCRYPTED_REQUEST = 0x6, } @@ -46,47 +46,45 @@ impl Security { data } - pub fn aes_cbc_encrypt(&self, raw: &[u8], key: &[u8; 32]) -> Vec { + pub fn aes_cbc_encrypt(&self, raw: [u8; 32], key: &[u8; 32]) -> [u8; 32] { type Aes256CbcEnc = cbc::Encryptor; - 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 + .encrypt_padded_vec_mut::(&raw) + .try_into() + .unwrap() } - pub fn aes_cbc_decrypt(&self, raw: &[u8], key: &[u8; 32]) -> Vec { + pub fn aes_cbc_decrypt(&self, raw: [u8; 32], key: &[u8; 32]) -> [u8; 32] { 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 + .decrypt_padded_vec_mut::(&raw) + .unwrap() + .try_into() + .unwrap() } pub fn tcp_key(&mut self, response: &[u8], key: &[u8; 32]) -> Result<()> { if response == b"ERROR" { - bail!("authentication failed!"); + bail!("authentication failed! (code ERROR)"); } - let payload = &response[0..32]; + let payload: [u8; 32] = response[0..32] + .iter() + .map(|&b| b) + .collect::>() + .try_into() + .unwrap(); + let sign = &response[32..]; + let result = self.aes_cbc_decrypt(payload, &key); - let plain = self.aes_cbc_decrypt(payload, &key); - - if Sha256::digest(&plain).into_iter().collect::>() != sign { + if Sha256::digest(&result).into_iter().collect::>() != sign { bail!("sign does not match"); } - self.tcp_key = Some(Self::xorstr(&plain, key).try_into().unwrap()); + self.tcp_key = Some(Self::xorstr(&result, key).try_into().unwrap()); self.request_count = 0; self.response_count = 0; @@ -94,7 +92,7 @@ impl Security { } fn xorstr(lhs: &[u8], rhs: &[u8]) -> Vec { - assert_eq!(lhs.len(), rhs.len()); + debug_assert_eq!(lhs.len(), rhs.len()); lhs.iter().zip(rhs.iter()).map(|(&l, &r)| l ^ r).collect() } @@ -135,7 +133,9 @@ impl Security { .into_iter() .collect(); - data = self.aes_cbc_encrypt(&data, self.tcp_key.as_ref().unwrap()); + data = self + .aes_cbc_encrypt(data.try_into().unwrap(), self.tcp_key.as_ref().unwrap()) + .to_vec(); data.extend(sign); } @@ -148,3 +148,28 @@ impl Security { lhs } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn aes_cbc_decrypt() { + let payload: [u8; 32] = + b",\xcbq_T\x81L\x96\xfa\xe7\xe4\xa7\xc5\xabU \r\xf5x\xd6\x08\x94_\\\xce\x8br\x1b\xa5\xbe\xc6\x1a" + .iter() + .map(|&b| b) + .collect::>() + .try_into() + .unwrap(); + + let key = b"*[R\x00\xc2\xc0ML\x81\x1d\x05P\xe1\xdc[1CT6\xb9[wM*\x88\xd7\xe4ma\xfd\x96i"; + let plain = b"\x9b\xaa\xdf\xff\x07\x1a\xd2\xe4\xb7TY\xe2\xf9\x8c\xdf\xe7!+\xda\xe4\x86GY\xe6j\x94\xdb\xe7\xb9b\xda\xe6"; + + let security = Security::default(); + + let result = security.aes_cbc_decrypt(payload, key); + + assert_eq!(&result, plain); + } +}