Initial commit

This commit is contained in:
KubaWis 2025-02-23 18:19:22 +01:00
parent 021859b544
commit d53ee8ab5c
6 changed files with 30213 additions and 0 deletions

19
Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "miband8-hr"
version = "0.1.0"
edition = "2021"
[dependencies]
aes = "0.8.4"
btleplug = "0.11.6"
ccm = "0.5.0"
futures = "0.3.31"
hex = "0.4.3"
hmac = "0.12.1"
protobuf = "3.7.1"
rand = "0.8.5"
rand_core = "0.6.4"
rosc = "0.10.1"
sha2 = "0.10.8"
tokio = {version="1.41.0", features = ["full"]}
uuid = "1.11.0"

14
src/constants.rs Normal file
View File

@ -0,0 +1,14 @@
// https://github.com/VladKolerts/miband4/blob/master/constants.js
// https://github.com/vshymanskyy/miband-js/blob/master/src/miband.js
pub const PAYLOAD_ACK: [u8;4] = [0x0,0x0,0x3,0x0];
pub const MIBAND8: &str = "0000fe95-0000-1000-8000-00805f9b34fb";
pub const READ: &str = "00000051-0000-1000-8000-00805f9b34fb";
pub const WRITE: &str = "00000052-0000-1000-8000-00805f9b34fb";
pub const ACTIVITY_DATA: &str = "00000053-0000-1000-8000-00805f9b34fb";
pub const DATA_UPLOAD: &str = "00000055-0000-1000-8000-00805f9b34fb";
pub const AUTH_COMMAND: &[u8] = &[0x00, 0x00, 0x02, 0x02, 0x08, 0x01, 0x10, 0x1a, 0x1a, 0x15, 0xf2, 0x01, 0x12, 0x0a, 0x10];

54
src/main.rs Normal file
View File

@ -0,0 +1,54 @@
mod constants;
mod miband8;
mod xiaomi;
mod protobuf_decoder;
use std::{net::{SocketAddrV4, UdpSocket}, str::FromStr};
use rosc::{encoder, OscMessage, OscPacket, OscType};
use tokio::{sync::watch, task};
const SEND_TO_CHAT: bool = false;
const SEND_ADDR: &str = "10.42.0.2:9000";
#[tokio::main]
async fn main() {
let send_addr: SocketAddrV4 = SocketAddrV4::from_str(SEND_ADDR).unwrap();
let sock = UdpSocket::bind(SocketAddrV4::from_str("0.0.0.0:9000").unwrap()).unwrap();
let (tx, mut rx) = watch::channel::<u8>(0);
let mut band = miband8::Miband8::new("f60b07bd3739018d7ee61d866c1eb01d".to_owned());
let e = band.init().await;
println!("{:?}", e);
task::spawn(async move {
band.continuously_measure_hr(tx).await;
});
while rx.changed().await.is_ok() {
let hr = rx.borrow_and_update().clone();
println!("Got heartrate: {}", hr);
if SEND_TO_CHAT {
let msg_buf = encoder::encode(&OscPacket::Message(OscMessage {
addr: "/chatbox/input".to_string(),
args: vec![OscType::String(format!("<3 {hr} bpm").to_string()), rosc::OscType::Bool(true),rosc::OscType::Bool(false)],
}))
.unwrap();
sock.send_to(&msg_buf, send_addr).unwrap();
}
let param_names = vec!["/avatar/parameters/VF101_beat", "/avatar/parameters/VF142_beat", "/avatar/parameters/VF145_beat"];
for param in param_names {
let msg_buf = encoder::encode(&OscPacket::Message(OscMessage {
addr: param.to_string(),
args: vec![OscType::Float((hr as f32 - 127.0)/ 127.0)],
}))
.unwrap();
sock.send_to(&msg_buf, send_addr).unwrap();
}
}
}

333
src/miband8.rs Normal file
View File

@ -0,0 +1,333 @@
use std::collections::HashMap;
use std::{error::Error, time::Duration, vec};
use btleplug::{api::{Characteristic, Service,Central, Manager as _, Peripheral as _, ScanFilter, WriteType}, platform::{self, Adapter, Peripheral}};
use futures::StreamExt;
use hmac::{Hmac, Mac};
use rand::Rng;
use tokio::sync::watch::Sender;
use tokio::time;
use uuid::uuid;
use aes::Aes128;
use sha2::Sha256;
use ccm::{aead::{generic_array::GenericArray, Aead,KeyInit as ccm_key},consts::{U4, U12},Ccm};
use crate::constants::{self, AUTH_COMMAND};
use crate::protobuf_decoder::{self, Value};
type HmacSha256 = Hmac<Sha256>;
type Aes128Ccm = Ccm<Aes128, U4, U12>;
pub struct Miband8 {
dev: Option<Peripheral>,
central: Option<Adapter>,
auth_key: String,
services: HashMap<String, Service>,
chars: HashMap<String, Characteristic>,
nonce: Vec<u8>,
encryption_nonce: Vec<u8>,
encryption_key: Vec<u8>,
decryption_nonce: Vec<u8>,
decryption_key: Vec<u8>,
handle: u8
}
impl Miband8 {
pub fn new(key: String) -> Self {
Miband8 {
dev: None,
central: None,
auth_key: key,
services: HashMap::new(),
chars: HashMap::new(),
nonce: [0;16].to_vec(),
encryption_nonce: vec![],
encryption_key: vec![],
decryption_nonce: vec![],
decryption_key: vec![],
handle: 2
}
}
pub async fn init(&mut self) -> Result<(), Box<dyn Error>> {
let manager = platform::Manager::new().await?;
// get the first bluetooth adapter
let adapters = manager.adapters().await?;
let central = adapters.into_iter().nth(0);
if central.is_none() {
return Err("No bluetooth adapter found")?;
}
self.central = Some(central.unwrap());
// start scanning for devices
self.central.as_ref().unwrap()
.start_scan(ScanFilter {
services: vec![uuid!(constants::MIBAND8)],
})
.await?;
time::sleep(Duration::from_secs(5)).await;
let mut band_op: Option<Peripheral> = None;
for p in self.central.as_ref().unwrap().peripherals().await? {
if p.properties()
.await?
.unwrap()
.local_name
.iter()
.any(|name| name.contains("Xiaomi Smart Band 8"))
{
band_op = Some(p);
}
}
if band_op.is_none() {
return Err("No band found")?;
}
let band = band_op.unwrap();
println!("Connecting to GATT");
band.connect().await?;
println!("Connected to GATT");
// discover services and characteristics
band.discover_services().await?;
println!("Discovered services");
self.dev = Some(band.clone());
let chars = band.services();
self.services.insert(
"miband8".to_owned(),
chars
.iter()
.find(|c| c.uuid == uuid!(constants::MIBAND8))
.unwrap().to_owned(),
);
println!("Services initialized");
let miband8_chars = &self.services
.get("miband8")
.unwrap()
.characteristics;
self.chars.insert(
"activity_data".to_owned(),
miband8_chars.iter()
.find(|c| c.uuid == uuid!(constants::ACTIVITY_DATA))
.unwrap().to_owned(),
);
self.chars.insert(
"read".to_owned(),
miband8_chars.iter()
.find(|c| c.uuid == uuid!(constants::READ))
.unwrap().to_owned(),
);
self.chars.insert(
"write".to_owned(),
miband8_chars.iter()
.find(|c| c.uuid == uuid!(constants::WRITE))
.unwrap().to_owned(),
);
self.chars.insert(
"data_upload".to_owned(),
miband8_chars.iter()
.find(|c| c.uuid == uuid!(constants::DATA_UPLOAD))
.unwrap().to_owned(),
);
println!("Characteristics initialized");
band.subscribe(&self.chars["write"]).await?;
band.subscribe(&self.chars["read"]).await?;
band.subscribe(&self.chars["activity_data"]).await?;
band.subscribe(&self.chars["data_upload"]).await?;
// Send AUTH step 1: auth_command + nonce
rand::thread_rng().fill(&mut self.nonce[..]);
band.write(&self.chars.get("write").unwrap(), &[AUTH_COMMAND, &self.nonce.clone()].concat(), WriteType::WithoutResponse).await?;
let mut events = band.notifications().await?;
while let Some(event) = events.next().await {
if event.uuid == uuid!(constants::WRITE) && event.value == constants::PAYLOAD_ACK {
println!("Got ack");
// TODO: wait for ack before sending another command
}
if event.uuid == uuid!(constants::READ) && event.value[5] == 0x01 {
self.ack().await?;
match event.value[7] {
// NONCE
0x1A => {
println!("Got watch nonce");
let next_packet: Vec<u8> = self.handle_watch_nonce(&event.value).await?;
band.write(&self.chars.get("write").unwrap(), &next_packet, WriteType::WithoutResponse).await?;
}
// Auth success
0x1B => {
println!("Authenticated!!");
return Ok(());
}
unk => {
println!("Unknown command subtype: {:2X?}", unk)
}
}
}
}
Ok(())
}
async fn ack(&mut self) -> Result<(), btleplug::Error> {
self.dev.as_ref().unwrap().write(&self.chars.get("read").unwrap(), &constants::PAYLOAD_ACK, WriteType::WithoutResponse).await
}
fn get_handle(&mut self) -> u8{
self.handle = self.handle.wrapping_add(1);
self.handle
}
pub async fn continuously_measure_hr(&mut self, channel_tx: Sender<u8>) -> u8 {
let command: Vec<u8> = vec![0x08, 0x08, 0x10, 0x2d];
let handle = self.get_handle();
let mut command_enc = vec![0x00, 0x00, 0x02, 0x01, handle, 0x00];
command_enc.append(&mut self.encrypt(self.encryption_key.clone(), &mut self.encryption_nonce.clone(), command, handle));
self.dev.as_ref().unwrap().write(&self.chars.get("write").unwrap(), &command_enc, WriteType::WithoutResponse).await.unwrap();
let mut events2 = self.dev.as_ref().unwrap().notifications().await.unwrap();
while let Some(event) = events2.next().await {
self.ack().await.unwrap();
let decrypted = self.decrypt(event.value[4..event.value.len()].to_vec());
if decrypted.len() > 8 {
let parsed = protobuf_decoder::Decoder::decode(&decrypted[..decrypted.len()-8].to_vec());
if let Some(Value::PROTOBUF(activity_update)) = parsed.values.get(&10) { // 10 ID for RealTimeStats
if let Some(Value::INT(heartrate)) = activity_update.values.get(&4) { // 4 ID heartRate
channel_tx.send(*heartrate as u8).unwrap();
}
}
}
}
0
}
async fn handle_watch_nonce(&mut self, notification_value: &Vec<u8>) -> Result<Vec<u8>, Box<dyn Error>> {
let watch_nonce: Vec<u8> = notification_value[15..31].to_vec();
let watch_hmac: Vec<u8> = notification_value[33..65].to_vec();
let step2hmac = self.compute_auth_step3_hmac(watch_nonce.clone()).await.unwrap();
self.decryption_key = step2hmac[0..16].to_vec();
self.encryption_key = step2hmac[16..32].to_vec();
self.decryption_nonce = step2hmac[32..36].to_vec();
self.decryption_nonce.append(&mut vec![0x00;8]);
let encryption_nonce: &[u8] = &step2hmac[36..40];
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&self.decryption_key)
.expect("HMAC can take key of any size");
let mut secret_key: Vec<u8> = vec![];
secret_key.append(&mut watch_nonce.clone());
secret_key.append(&mut self.nonce.clone());
mac.update(&secret_key.as_slice());
let tmp = mac.finalize().into_bytes();
let result = tmp.as_slice();
if result != watch_hmac {
return Err("Watch hmac mismatch")?;
}
let mut next_packet: Vec<u8> = vec![0x00, 0x00, 0x02, 0x02, 0x08, 0x01, 0x10, 0x1b, 0x1a, 0x42, 0x82, 0x02, 0x3f, 0x0a, 0x20, 0xf2, 0x10, 0xa9, 0xc9, 0xb4, 0x2f, 0x35, 0x28, 0x7d, 0xa0, 0x29, 0x28, 0x41, 0xad, 0x79, 0x50, 0x06, 0x4e, 0x2b, 0x2d, 0x11, 0x6f, 0x0f, 0x1b, 0x59, 0x11, 0xa7, 0x93, 0x21, 0xed, 0x0a, 0xf9, 0x12, 0x1b, 0x8d, 0xfd, 0xb9, 0x42, 0x24, 0xf2, 0xc2, 0xff, 0x09, 0xdd, 0x12, 0x71, 0xb0, 0x9d, 0xc7, 0x36, 0x3c, 0xa6, 0x43, 0xd1, 0xaf, 0x20, 0xd6, 0x47, 0x04, 0x54, 0x9c];
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(self.encryption_key.as_slice())
.expect("HMAC can take key of any size");
let mut secret_key: Vec<u8> = vec![];
secret_key.append(&mut self.nonce.clone());
secret_key.append(&mut watch_nonce.clone());
mac.update(secret_key.as_slice());
let hmac_key = mac.finalize().into_bytes();
let hmac_key_bytes = hmac_key.as_slice();
for (i,byte) in hmac_key_bytes.iter().enumerate() {
next_packet[i+15] = *byte;
}
let device_info: Vec<u8> = vec![0x08, 0x00, 0x15, 0x00, 0x00, 0x04, 0x42, 0x1a, 0x07, 0x52, 0x4d, 0x58, 0x33, 0x33, 0x37, 0x30, 0x20, 0xe0, 0x01, 0x2a, 0x02, 0x45, 0x4e];
self.encryption_nonce = encryption_nonce.to_vec();
let encrypted_device_info = self.encrypt(self.encryption_key.clone(), &mut self.encryption_nonce.clone(), device_info, 0);
for (i,byte) in encrypted_device_info.iter().enumerate() {
next_packet[i+49] = *byte;
}
Ok(next_packet)
}
pub async fn compute_auth_step3_hmac(&mut self, watch_nonce: Vec<u8>) -> Result<[u8;64], Box<dyn Error>> {
let mut secret_key: Vec<u8> = vec![];
secret_key.append(&mut self.nonce.clone());
secret_key.append(&mut watch_nonce.clone());
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(secret_key.as_slice())
.expect("HMAC can take key of any size");
mac.update(hex::decode(self.auth_key.clone()).unwrap().as_slice());
let hmac_key = mac.finalize().into_bytes();
let hmac_key_bytes = hmac_key.as_slice();
let mut output: [u8;64] = [0;64];
let mut tmp: &[u8] = &[0;0];
let mut b: u8 = 1;
let mut i = 0;
let mut tmp2;
while i < output.len(){
mac = <HmacSha256 as hmac::Mac>::new_from_slice(hmac_key_bytes)
.expect("HMAC can take key of any size");
let mut a: Vec<u8> = vec![];
a.append(&mut tmp.to_vec());
a.append(&mut "miwear-auth".as_bytes().to_vec());
a.append(&mut vec![b]);
mac.update(a.as_slice());
tmp2 = mac.finalize().into_bytes();
tmp = tmp2.as_slice();
for j in tmp {
output[i] = *j;
i += 1;
}
b += 1;
}
Ok(output)
}
pub fn encrypt(&mut self, key: Vec<u8>, nonce: &mut Vec<u8>, payload: Vec<u8>, i: u8) -> Vec<u8> {
nonce.append(&mut vec![0x00,0x00,0x00,0x00,i,0x00,0x00,0x00]);
let cipher = Aes128Ccm::new(GenericArray::from_slice(&key.as_slice()));
let nonce = GenericArray::from_slice(&nonce); // 12-bytes; unique per message
let ciphertext = cipher.encrypt(nonce, payload.as_slice()).unwrap();
ciphertext
}
fn decrypt(&mut self, payload: Vec<u8>) -> Vec<u8> {
let cipher = Aes128Ccm::new(GenericArray::from_slice(self.decryption_key.as_slice()));
let nonce = GenericArray::from_slice(&self.decryption_nonce); // 12-bytes; unique per message
let ciphertext = cipher.encrypt(nonce, payload.as_slice()).unwrap();
ciphertext
}
}

112
src/protobuf_decoder.rs Normal file
View File

@ -0,0 +1,112 @@
use core::str;
use std::collections::HashMap;
#[derive(Debug)]
enum WireType {
VARINT = 1,
I64 = 2,
LEN = 3,
SGROUP = 4,
EGROUP = 5,
I32 = 6,
UNKNOWN = 7
}
impl From<u8> for WireType {
fn from(orig: u8) -> Self {
match orig {
1 => WireType::VARINT,
2 => WireType::I64,
3 => WireType::LEN,
4 => WireType::SGROUP,
5 => WireType::EGROUP,
6 => WireType::I32,
_ => WireType::UNKNOWN
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum Value {
INT(u16),
STRING(String),
PROTOBUF(Protobuf)
}
#[derive(Debug, Clone)]
pub struct Protobuf {
pub values: HashMap<u8, Value>
}
pub struct Decoder {}
impl Decoder {
pub fn decode(input: &Vec<u8>) -> Protobuf {
let mut hashmap: HashMap<u8, Value> = HashMap::new();
// println!("Huja: {:02x?}", input);
if input.len() < 1 {
// Empty message
return Protobuf { values: hashmap };
}
let variant_key = input[0] & 0b01111111;
let field_number = variant_key >> 3;
let mut iter = input[2..].iter();
// Citing the documentation: "You now know that the first number in the stream is always a varint key"
if (input[1] >> 7) == 1 {
let a: u16 = input[1] as u16;
let b: u16 = *iter.next().unwrap() as u16;
hashmap.insert(field_number, Value::INT(((b & 0b01111111) << 7) | (a & 0b01111111)));
}else {
hashmap.insert(field_number, Value::INT(input[1] as u16));
}
while let Some(mut value) = iter.next() {
let variant_key = value & 0b01111111;
let wire_type: WireType = WireType::from((variant_key & 0b111) + 1);
let field_number = variant_key >> 3;
// print!("{}:{:?}", field_number, wire_type);
value = iter.next().unwrap();
match wire_type {
WireType::VARINT => {
if (value >> 7) == 1 {
// print!( " e");
let a: &u16 = &(*value as u16);
let b: &u16 = &(*iter.next().unwrap() as u16);
// println!(" value: {}", ((b & 0b01111111) << 7) | (a & 0b01111111));
hashmap.insert(field_number, Value::INT(((b & 0b01111111) << 7) | (a & 0b01111111)));
}else {
// println!(" value: {}", value);
hashmap.insert(field_number, Value::INT(*value as u16));
}
},
WireType::LEN => {
let tmp = iter.clone().take(*value as usize).map(|x| *x).collect::<Vec<u8>>();
if *value == 0 {
break;
}
iter.nth(*value as usize - 1 );
let huj = str::from_utf8(tmp.as_slice());
if huj.is_err() {
hashmap.insert(field_number,Value::PROTOBUF(Self::decode(&tmp[1..].to_vec())));
}else {
hashmap.insert(field_number,Value::STRING(huj.unwrap().to_string()));
}
},
WireType::UNKNOWN => {
// println!(" unknown skipping");
break;
}
_ => {
// println!(" unimplemented: {:?}", wire_type);
}
}
}
Protobuf { values: hashmap }
}
}

29681
src/xiaomi.rs Normal file

File diff suppressed because it is too large Load Diff