core_bluetooth 0.1.0

Safe API wrapper for Core Bluetooth framework
Documentation
//! Reads and decodes packets received in advertisements.
//! Based on https://github.com/custom-components/sensor.mitemp_bt
//! Some devices encrypt the data they advertise. To decrypt a key must be extracted and specified
//! in `--key` argument. See https://github.com/custom-components/sensor.mitemp_bt/blob/master/faq.md#my-sensors-ble-advertisements-are-encrypted-how-can-i-get-the-key
//! on how to get the key.
use anyhow::*;
use enumflags2::BitFlags;
use log::*;
use macaddr::MacAddr6;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::process::exit;

use core_bluetooth::central::*;
use core_bluetooth::*;

const SERVICE: &str = "0000fe95-0000-1000-8000-00805f9b34fb";

struct App {
    central: CentralManager,
    receiver: Receiver<CentralEvent>,
    encryption_keys: HashMap<MacAddr6, Vec<u8>>,
    seen: HashSet<MacAddr6>,
}

impl App {
    fn new(encryption_keys: HashMap<MacAddr6, Vec<u8>>) -> Self {
        let (central, receiver) = CentralManager::new();
        Self {
            central,
            receiver,
            encryption_keys,
            seen: HashSet::new(),
        }
    }

    fn handle_event(&mut self, event: CentralEvent) {
        debug!("New event: {:#?}", event);
        match event {
            CentralEvent::ManagerStateChanged { new_state } => {
                match new_state {
                    ManagerState::Unsupported => {
                        error!("Bluetooth is not supported on this system");
                        exit(1);
                    },
                    ManagerState::Unauthorized => {
                        error!("The app is not authorized to use Bluetooth on this system");
                        exit(1);
                    },
                    ManagerState::PoweredOff => {
                        error!("Bluetooth is disabled, please enable it");
                    },
                    ManagerState::PoweredOn => {
                        info!("Discovering Xiaomi sensors...");
                        self.central.scan();
                    },
                    _ => {},
                }
            }
            CentralEvent::PeripheralDiscovered {
                advertisement_data,
                ..
            } => {
                if let Some(packet) = advertisement_data.service_data().get(SERVICE.parse().unwrap()) {
                    match Packet::parse(packet, |mac| self.encryption_keys.get(&mac).map(|v| &v[..])) {
                        Ok(packet) => {
                            if !packet.sensor_values.is_empty() {
                                info!("{} ({}): {:?}", packet.mac_addr, packet.device_kind, packet.sensor_values);
                            } else if self.seen.insert(packet.mac_addr) {
                                info!("New device: {} ({})", packet.mac_addr, packet.device_kind);
                            }
                        }
                        Err(e) => {
                            error!("Error parsing packet: {}", e);
                        }
                    }
                }
            }
            _ => {}
        }
    }

    #[cfg(not(feature = "async_std_unstable"))]
    fn run(mut self) {
        debug!("Running in std");
        while let Ok(event) = self.receiver.recv() {
            self.handle_event(event);
        }
    }

    #[cfg(feature = "async_std_unstable")]
    fn run(mut self) {
        debug!("Running in async_std");
        async_std::task::block_on(async move {
            while let Some(event) = self.receiver.recv().await {
                self.handle_event(event);
            }
        })
    }
}

#[derive(BitFlags, Copy, Clone, Debug)]
#[repr(u8)]
enum Flag {
    Encrypted   = 0x08,
    HasCap      = 0x20,
    HasData     = 0x40,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DeviceKind {
    Hhccjcy01,
    Lywsdcgq,
    Lywsd02,
    Cgg1,
    Hhccpot002,
    Gcls002,
    Lywsd03mmc,
    Cgd1,
    Jqjcy01ym,
    Wx08zm,
}

impl DeviceKind {
    fn from_u16(v: u16) -> Option<Self> {
        use DeviceKind::*;
        Some(match v {
            0x9800 => Hhccjcy01,
            0xaa01 => Lywsdcgq,
            0x5b04 => Lywsd02,
            0x4703 => Cgg1,
            0x5d01 => Hhccpot002,
            0xbc03 => Gcls002,
            0x5b05 => Lywsd03mmc,
            0x7605 => Cgd1,
            0xdf02 => Jqjcy01ym,
            0x0a04 => Wx08zm,
            _ => return None,
        })
    }
}

impl std::fmt::Display for DeviceKind {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", format!("{:?}", self).to_ascii_uppercase())
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
enum SensorValue {
    Battery(u8),
    Conductivity(u32),
    Consumable(u8),
    Formaldehyde(f32),
    Humidity(f32),
    Illuminance(u32),
    Moisture(u8),
    Switch(u8),
    Temperature(f32),
}


#[derive(Clone, Debug, PartialEq)]
struct Packet {
    mac_addr: MacAddr6,
    device_kind: DeviceKind,
    sensor_values: Vec<SensorValue>,
}

impl Packet {
    fn parse<'a, F>(packet: &[u8], get_encryption_key: F) -> Result<Self>
        where F: Fn(MacAddr6) -> Option<&'a [u8]>
    {
        debug!("parsing {} B packet: {}", packet.len(), hex::encode(packet));
        if packet.len() < 12 {
            return Err(anyhow!("packet is too small: {}", packet.len()));
        }

        let flags = BitFlags::from_bits_truncate(packet[0]);
        debug!("flags: {:?}", flags);

        let mut mac_addr = [0; 6];
        mac_addr.copy_from_slice(&packet[5..11]);
        mac_addr.reverse();
        let mac_addr = MacAddr6::from(mac_addr);
        debug!("mac: {}", mac_addr);

        let device_kind = u16::from_be_bytes([packet[2], packet[3]]);
        let device_kind = DeviceKind::from_u16(device_kind)
            .ok_or_else(|| anyhow!("unrecognized device type ({}): 0x{:x}", mac_addr, device_kind))?;
        debug!("device_kind: {}", device_kind);

        if !flags.contains(Flag::HasData) {
            debug!("no HasData flag");
            return Ok(Self {
                mac_addr,
                device_kind,
                sensor_values: vec![],
            });
        }

        let payload_start = if flags.contains(Flag::HasCap) { 12 } else { 11 };

        let payload = if flags.contains(Flag::Encrypted) {
            let key = get_encryption_key(mac_addr)
                .ok_or_else(|| anyhow!("no encryption key found for {}", mac_addr))?;

            let mut nonce = [0; 12];
            nonce[..6].copy_from_slice(&packet[5..11]);
            nonce[6..9].copy_from_slice(&packet[2..5]);
            nonce[9..12].copy_from_slice(&packet[packet.len() - 7..packet.len() - 4]);

            let tag = &packet[packet.len() - 4..];
            let aad = &[0x11];

            let payload = decrypt_aes_128_ccm(&packet[payload_start..packet.len() - 7], &key, &nonce, tag, aad)?;
            Cow::Owned(payload)
        } else {
            Cow::Borrowed(&packet[payload_start..])
        };

        let mut payload = &payload[..];
        debug!("payload: {}", hex::encode(payload));
        let mut r = Vec::new();
        while !payload.is_empty() {
            if payload.len() < 3 {
                warn!("truncated value");
                break;
            }
            let kind = u16::from_be_bytes([payload[0], payload[1]]);
            let len = payload[2] as usize;
            payload = &payload[3..];
            if len > payload.len() {
                warn!("truncated value");
                break;
            }
            let v = &payload[..len];
            payload = &payload[len..];

            let mut decoded = true;
            match len {
                1 => match kind {
                    0xa10 => r.push(SensorValue::Battery(v[0])),
                    0x810 => r.push(SensorValue::Moisture(v[0])),
                    0x1210 => r.push(SensorValue::Switch(v[0])),
                    0x1310 => r.push(SensorValue::Consumable(v[0])),
                    _ => decoded = false,
                }
                3 => match kind {
                    0x710 => r.push(SensorValue::Illuminance(u32::from_le_bytes([v[0], v[1], v[2], 0]))),
                    _ => decoded = false,
                }
                2 => match kind {
                    0x610 => r.push(SensorValue::Humidity(u16::from_le_bytes([v[0], v[1]]) as f32 / 10.0)),
                    0x410 => r.push(SensorValue::Temperature(i16::from_le_bytes([v[0], v[1]]) as f32 / 10.0)),
                    0x910 => r.push(SensorValue::Conductivity(u16::from_le_bytes([v[0], v[1]]) as u32)),
                    0x1010 => r.push(SensorValue::Formaldehyde(u16::from_le_bytes([v[0], v[1]]) as f32 / 100.0)),
                    _ => decoded = false,
                }
                4 => match kind {
                    0xd10 => {
                        r.push(SensorValue::Temperature(i16::from_le_bytes([v[0], v[1]]) as f32 / 10.0));
                        r.push(SensorValue::Humidity(u16::from_le_bytes([v[2], v[3]]) as f32 / 10.0));
                    }
                    _ => decoded = false,
                }
                _ => decoded = false,
            }
            if !decoded {
                warn!("couldn't decode sensor value: kind={:x} value={}", kind, hex::encode(v));
            }
        }
        Ok(Self {
            mac_addr,
            device_kind,
            sensor_values: r,
        })
    }
}

fn decrypt_aes_128_ccm(ciphertext: &[u8], key: &[u8], nonce: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
    // Unfortunately Rust OpenSSL wrapper doesn't work with non-standard AES CCM tags and there's no
    // safe alternative.
    // See https://github.com/sfackler/rust-openssl/issues/1237

    use openssl_sys::*;
    use std::ptr::{null, null_mut};
    use std::convert::TryInto;

    unsafe {
        let cipher = EVP_aes_128_ccm();

        let mut out_len = 0;
        let mut out = vec![0; ciphertext.len() + EVP_CIPHER_block_size(cipher) as usize];

        let ctx = EVP_CIPHER_CTX_new();

        // Select cipher
        EVP_DecryptInit_ex(ctx, cipher, null_mut(), null(), null());

        // Set nonce length
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, nonce.len().try_into().unwrap(), null_mut());

        // Set expected tag value
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG,
                            tag.len().try_into().unwrap(), tag.as_ptr() as *mut _);

        // Specify key and noce
        EVP_DecryptInit_ex(ctx, null(), null_mut(), key.as_ptr(), nonce.as_ptr());

        // Set ciphertext length
        let ciphertext_len = ciphertext.len().try_into().unwrap();
        EVP_DecryptUpdate(ctx, null_mut(), &mut out_len, null(), ciphertext_len);

        // Set AAD
        EVP_DecryptUpdate(ctx, null_mut(), &mut out_len, aad.as_ptr(), aad.len().try_into().unwrap());

        // Decrypt plaintext, verify tag
        let r = EVP_DecryptUpdate(ctx, out.as_mut_ptr(), &mut out_len, ciphertext.as_ptr(), ciphertext_len);

        if r > 0 {
            out.truncate(out_len as usize);
            Ok(out)
        } else {
            Err(anyhow!("error decrypting"))
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use hex_literal::hex;

    #[test]
    fn decrypt_aes_128_ccm_() {
        let ciphertext = hex!("7f4258f2a8");
        let key = hex!("0f8fbcfc7d41c89c9b486b44e67be743");
        let nonce = hex!("9e03c038c1a45b0532000000");
        let tag = hex!("b3f39389");
        let aad = &[0x11];

        assert_eq!(decrypt_aes_128_ccm(&ciphertext, &key, &nonce, &tag, aad).unwrap(),
            b"\x06\x10\x02\xae\x01"[..].to_vec())
    }

    #[test]
    fn parse_() {
        let mut keys = HashMap::new();
        keys.insert(MacAddr6(hex!("a4c138c0039e")), hex!("0f8fbcfc7d41c89c9b486b44e67be743"));
        let parse = |packet| Packet::parse(packet, |mac| keys.get(&mac).map(|v| &v[..])).unwrap();

        let exp_packet = |sensor_values| Packet {
            mac_addr: MacAddr6(hex!("a4c138c0039e")),
            device_kind: DeviceKind::Lywsd03mmc,
            sensor_values,
        };

        assert_eq!(parse(&hex!("58585b05329e03c038c1a47f4258f2a8000000b3f39389")),
            exp_packet(vec![SensorValue::Humidity(43.0)]));
        assert_eq!(parse(&hex!("0201060f1695fe30585b056e9e03c038c1a408")),
            exp_packet(vec![]));
    }
}

pub fn main() -> Result<()> {
    env_logger::from_env(env_logger::Env::default()
        .default_filter_or("info")).init();

    use clap::Arg;
    let clapp = clap::App::new("Xiaomi Passive Sensor Reader")
        .about("Reads advertised sensor values from variety of Xiaomi BTLE devices")
        .arg(Arg::with_name("keys")
            .short('k')
            .long("key")
            .about("Sets encryption key for device in format --key=a4:c1:38:c0:03:9e=0f8fbcfc7d41c89c9b486b44e67be743")
            .takes_value(true)
            .multiple(true));
    let matches = clapp.get_matches();

    let mut keys = HashMap::new();
    for key in matches.values_of("keys").unwrap_or_default() {
        let (mac_addr, key) = Some(key.split("=")
            .collect::<Vec<_>>())
            .filter(|v| v.len() == 2 && v[1].len() == 32)
            .and_then(|v| {
                Some((v[0].parse::<MacAddr6>().ok()?, hex::decode(v[1]).ok()?))
            })
            .ok_or_else(|| anyhow!("invalid 'key' argument: {}", key))?;
        keys.insert(mac_addr, key);
    }

    App::new(keys).run();

    Ok(())
}