Test device authentication

This commit is contained in:
hodasemi 2023-09-24 13:16:57 +02:00
parent cf88642612
commit 3122f03e9e
9 changed files with 270 additions and 64 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -15,13 +15,10 @@ 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)
for k in keys:
token = keys[k]['token']
key = keys[k]['key']
token = "06df24fc4e8e950c6d9783051b8e38d971e5fbc617da259459d30d5e7d7fc05b4ccb708fe3a085f6f0af0f8cc961fa39dabfd0746f7bbcfbf7404d9cc5c2b077"
key = "2a5b5200c2c04d4c811d0550e1dc5b31435436b95b774d2a88d7e46d61fd9669"
device_info = devices[device_id]
@ -38,8 +35,34 @@ async def test():
attributes={}
)
if dev.connect(False):
return dev
dev.connect(False)
# 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())

View file

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

View file

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

View file

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

View file

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