diff --git a/matter/Cargo.toml b/matter/Cargo.toml index 32974f8..5d33cf5 100644 --- a/matter/Cargo.toml +++ b/matter/Cargo.toml @@ -47,6 +47,16 @@ safemem = "0.3.3" chrono = { version = "0.4.19", default-features = false, features = ["clock", "std"] } async-channel = "1.6" +# to compute the check digit +verhoeff = "1" + +# needed to compute base38 packed binary data Structure +base-encode = "0.3" +packed_struct = "0.10" + +# print QR code +qrcode = { version = "0.12", default-features = false } + [target.'cfg(target_os = "macos")'.dependencies] astro-dnssd = "0.3" diff --git a/matter/src/codec/base38.rs b/matter/src/codec/base38.rs new file mode 100644 index 0000000..237e9b5 --- /dev/null +++ b/matter/src/codec/base38.rs @@ -0,0 +1,46 @@ +const BASE38_CHARS: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-."; + +fn encode_base38(mut value: u32, char_count: u8) -> String { + let mut result = String::new(); + for _ in 0..char_count { + let mut chars = BASE38_CHARS.chars(); + let remainder = value % 38; + result.push(chars.nth(remainder as usize).unwrap()); + value = (value - remainder) / 38; + } + result +} + +pub fn encode(bytes: &[u8]) -> String { + let length = bytes.len(); + let mut offset = 0; + let mut result = String::new(); + + while offset < length { + let remaining = length - offset; + match remaining.cmp(&2) { + std::cmp::Ordering::Greater => { + result.push_str(&encode_base38( + ((bytes[offset + 2] as u32) << 16) + | ((bytes[offset + 1] as u32) << 8) + | (bytes[offset] as u32), + 5, + )); + offset += 3; + } + std::cmp::Ordering::Equal => { + result.push_str(&encode_base38( + ((bytes[offset + 1] as u32) << 8) | (bytes[offset] as u32), + 4, + )); + break; + } + std::cmp::Ordering::Less => { + result.push_str(&encode_base38(bytes[offset] as u32, 2)); + break; + } + } + } + + result +} diff --git a/matter/src/codec/mod.rs b/matter/src/codec/mod.rs new file mode 100644 index 0000000..fdada5c --- /dev/null +++ b/matter/src/codec/mod.rs @@ -0,0 +1 @@ +pub mod base38; diff --git a/matter/src/core.rs b/matter/src/core.rs index 30dcf9d..1ba31ac 100644 --- a/matter/src/core.rs +++ b/matter/src/core.rs @@ -25,6 +25,7 @@ use crate::{ fabric::FabricMgr, interaction_model::InteractionModel, mdns::Mdns, + pairing::compute_and_print_pairing_code, secure_channel::core::SecureChannel, transport, }; @@ -69,6 +70,8 @@ impl Matter { &dev_det.device_name, ); + compute_and_print_pairing_code(dev_det, dev_comm); + let fabric_mgr = Arc::new(FabricMgr::new()?); let acl_mgr = Arc::new(AclMgr::new()?); let open_comm_window = fabric_mgr.is_empty(); diff --git a/matter/src/data_model/cluster_basic_information.rs b/matter/src/data_model/cluster_basic_information.rs index 10a2b34..149096a 100644 --- a/matter/src/data_model/cluster_basic_information.rs +++ b/matter/src/data_model/cluster_basic_information.rs @@ -26,6 +26,7 @@ enum Attributes { SwVer = 9, } +#[derive(Default)] pub struct BasicInfoConfig { pub vid: u16, pub pid: u16, diff --git a/matter/src/lib.rs b/matter/src/lib.rs index 2d2bee2..d96d9d7 100644 --- a/matter/src/lib.rs +++ b/matter/src/lib.rs @@ -70,6 +70,7 @@ pub mod acl; pub mod cert; +pub mod codec; pub mod core; pub mod crypto; pub mod data_model; @@ -78,6 +79,7 @@ pub mod fabric; pub mod group_keys; pub mod interaction_model; pub mod mdns; +pub mod pairing; pub mod secure_channel; pub mod sys; pub mod tlv; diff --git a/matter/src/pairing.rs b/matter/src/pairing.rs new file mode 100644 index 0000000..fd7d906 --- /dev/null +++ b/matter/src/pairing.rs @@ -0,0 +1,225 @@ +use log::info; +use packed_struct::prelude::*; +use qrcode::{render::unicode, QrCode, Version}; +use verhoeff::Verhoeff; + +use crate::{ + codec::base38, data_model::cluster_basic_information::BasicInfoConfig, CommissioningData, +}; + +#[repr(u8)] +#[derive(Clone, Copy)] +pub enum CommissionningFlowType { + Standard = 0, + UserIntent = 1, + Custom = 2, +} + +pub struct DiscoveryCapabilitiesSchema { + on_ip_network: bool, + ble: bool, + soft_access_point: bool, +} + +impl DiscoveryCapabilitiesSchema { + fn as_bits(&self) -> u8 { + let mut bits = 0; + if self.soft_access_point { + bits |= 1 << 0; + } + if self.ble { + bits |= 1 << 1; + } + if self.on_ip_network { + bits |= 1 << 2; + } + bits + } +} + +pub struct QrCodeData<'data> { + flow_type: CommissionningFlowType, + discovery_capabilities: DiscoveryCapabilitiesSchema, + dev_det: &'data BasicInfoConfig, + comm_data: &'data CommissioningData, +} + +impl<'data> QrCodeData<'data> { + pub fn new( + dev_det: &'data BasicInfoConfig, + comm_data: &'data CommissioningData, + discovery_capabilities: DiscoveryCapabilitiesSchema, + ) -> Self { + QrCodeData { + flow_type: CommissionningFlowType::Standard, + discovery_capabilities, + dev_det, + comm_data, + } + } +} + +#[derive(PackedStruct, Debug)] +#[packed_struct(bit_numbering = "msb0", size_bytes = "11", endian = "msb")] +pub struct PackedQrData { + #[packed_field(bits = "0..3")] + version: Integer>, + #[packed_field(bits = "3..19")] + vid: Integer>, + #[packed_field(bits = "19..35")] + pid: Integer>, + #[packed_field(bits = "35..37")] + commissionning_flow_type: Integer>, + #[packed_field(bits = "37")] + soft_access_point: bool, + #[packed_field(bits = "38")] + ble: bool, + #[packed_field(bits = "39")] + on_ip_network: bool, + #[packed_field(bits = "40..45")] + _reserved: Integer>, + #[packed_field(bits = "45..57")] + discriminator: Integer>, + #[packed_field(bits = "57..84")] + passcode: Integer>, + #[packed_field(bits = "84..88")] + _padding: Integer>, +} + +pub fn compute_and_print_pairing_code(dev_det: &BasicInfoConfig, comm_data: &CommissioningData) { + let pairing_code = compute_pairing_code(comm_data); + pretty_print_pairing_code(&pairing_code); + print_qr_code(&pairing_code, dev_det, comm_data); +} + +fn compute_pairing_code(comm_data: &CommissioningData) -> String { + // 0: no Vendor ID and Product ID present in Manual Pairing Code + const VID_PID_PRESENT: u8 = 0; + + let CommissioningData { + discriminator, + passwd, + .. + } = comm_data; + + let mut digits = String::new(); + digits.push_str(&((VID_PID_PRESENT << 2) | (discriminator >> 10) as u8).to_string()); + digits.push_str(&format!( + "{:0>5}", + ((discriminator & 0x300) << 6) | (passwd & 0x3FFF) as u16 + )); + digits.push_str(&format!("{:0>4}", passwd >> 14)); + + let check_digit = digits.calculate_verhoeff_check_digit(); + digits.push_str(&check_digit.to_string()); + + digits +} + +pub fn pretty_print_pairing_code(pairing_code: &str) { + assert!(pairing_code.len() == 11); + let mut pretty = String::new(); + pretty.push_str(&pairing_code[..4]); + pretty.push('-'); + pretty.push_str(&pairing_code[4..8]); + pretty.push('-'); + pretty.push_str(&pairing_code[8..]); + info!("Pairing Code: {}", pretty); +} + +fn print_qr_code(pairing_code: &str, dev_det: &BasicInfoConfig, comm_data: &CommissioningData) { + let code = QrCode::with_version(pairing_code, Version::Normal(2), qrcode::EcLevel::M).unwrap(); + let image = code + .render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + println!("{}", image); +} + +fn base38_encode_qr(qr_data: &QrCodeData) -> String { + let QrCodeData { + flow_type, + discovery_capabilities, + dev_det, + comm_data, + } = &qr_data; + + let BasicInfoConfig { vid, pid, .. } = dev_det; + const VERSION: u8 = 0; // 3-bit value specifying the QR code payload version. SHALL be 000. + + let packed_qr_data = PackedQrData { + version: VERSION.into(), + vid: (*vid).reverse_bits().into(), + pid: (*pid).reverse_bits().into(), + commissionning_flow_type: ((*flow_type) as u8).into(), + soft_access_point: discovery_capabilities.soft_access_point, + ble: discovery_capabilities.ble, + on_ip_network: discovery_capabilities.on_ip_network, + _reserved: 0u8.into(), + discriminator: comm_data.discriminator.reverse_bits().into(), + passcode: comm_data.passwd.reverse_bits().into(), + _padding: 0u8.into(), + }; + + println!("{:?}", packed_qr_data); + println!("{}", packed_qr_data); + + let data = packed_qr_data.pack().unwrap(); + let data = data + .into_iter() + .map(|b| b.reverse_bits()) + .collect::>(); + + let base38 = base38::encode(&data); + format!("MT:{}", base38) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_compute_pairing_code() { + let comm_data = CommissioningData { + passwd: 123456, + discriminator: 250, + ..Default::default() + }; + let pairing_code = compute_pairing_code(&comm_data); + assert_eq!(pairing_code, "00876800071"); + + let comm_data = CommissioningData { + passwd: 34567890, + discriminator: 2976, + ..Default::default() + }; + let pairing_code = compute_pairing_code(&comm_data); + assert_eq!(pairing_code, "26318621095"); + } + + #[test] + fn can_base38_encode() { + const QR_CODE: &str = "MT:YNJV7VSC00CMVH7SR00"; + + let comm_data = CommissioningData { + passwd: 34567890, + discriminator: 2976, + ..Default::default() + }; + let dev_det = BasicInfoConfig { + vid: 9050, + pid: 65279, + ..Default::default() + }; + let disc_cap = DiscoveryCapabilitiesSchema { + on_ip_network: false, + ble: true, + soft_access_point: false, + }; + + let qr_code_data = QrCodeData::new(&dev_det, &comm_data, disc_cap); + let data_str = base38_encode_qr(&qr_code_data); + assert_eq!(data_str, QR_CODE) + } +}