diff --git a/examples/onoff_light/src/main.rs b/examples/onoff_light/src/main.rs index bbf1d8b..ff86b68 100644 --- a/examples/onoff_light/src/main.rs +++ b/examples/onoff_light/src/main.rs @@ -36,6 +36,7 @@ fn main() { hw_ver: 2, sw_ver: 1, serial_no: "aabbccdd".to_string(), + device_name: "OnOff Light".to_string(), }; let dev_att = Box::new(dev_att::HardCodedDevAtt::new()); diff --git a/matter/Cargo.toml b/matter/Cargo.toml index 32974f8..fb6c7f7 100644 --- a/matter/Cargo.toml +++ b/matter/Cargo.toml @@ -47,6 +47,12 @@ 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" + +# 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..d4c69c1 --- /dev/null +++ b/matter/src/codec/base38.rs @@ -0,0 +1,225 @@ +/* + * + * Copyright (c) 2020-2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Base38 encoding and decoding functions. + +use crate::error::Error; + +const BASE38_CHARS: [char; 38] = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', + 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '.', +]; + +const UNUSED: u8 = 255; + +// map of base38 charater to numeric value +// subtract 45 from the character, then index into this array, if possible +const DECODE_BASE38: [u8; 46] = [ + 36, // '-', =45 + 37, // '.', =46 + UNUSED, // '/', =47 + 0, // '0', =48 + 1, // '1', =49 + 2, // '2', =50 + 3, // '3', =51 + 4, // '4', =52 + 5, // '5', =53 + 6, // '6', =54 + 7, // '7', =55 + 8, // '8', =56 + 9, // '9', =57 + UNUSED, // ':', =58 + UNUSED, // ';', =59 + UNUSED, // '<', =50 + UNUSED, // '=', =61 + UNUSED, // '>', =62 + UNUSED, // '?', =63 + UNUSED, // '@', =64 + 10, // 'A', =65 + 11, // 'B', =66 + 12, // 'C', =67 + 13, // 'D', =68 + 14, // 'E', =69 + 15, // 'F', =70 + 16, // 'G', =71 + 17, // 'H', =72 + 18, // 'I', =73 + 19, // 'J', =74 + 20, // 'K', =75 + 21, // 'L', =76 + 22, // 'M', =77 + 23, // 'N', =78 + 24, // 'O', =79 + 25, // 'P', =80 + 26, // 'Q', =81 + 27, // 'R', =82 + 28, // 'S', =83 + 29, // 'T', =84 + 30, // 'U', =85 + 31, // 'V', =86 + 32, // 'W', =87 + 33, // 'X', =88 + 34, // 'Y', =89 + 35, // 'Z', =90 +]; + +const BASE38_CHARACTERS_NEEDED_IN_NBYTES_CHUNK: [u8; 3] = [2, 4, 5]; +const RADIX: u32 = BASE38_CHARS.len() as u32; + +/// Encode a byte array into a base38 string. +/// +/// # Arguments +/// * `bytes` - byte array to encode +/// * `length` - optional length of the byte array to encode. If not specified, the entire byte array is encoded. +pub fn encode(bytes: &[u8], length: Option) -> String { + let mut offset = 0; + let mut result = String::new(); + + // if length is specified, use it, otherwise use the length of the byte array + // if length is specified but is greater than the length of the byte array, use the length of the byte array + let b_len = bytes.len(); + let length = length.map(|l| l.min(b_len)).unwrap_or(b_len); + + 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 +} + +fn encode_base38(mut value: u32, char_count: u8) -> String { + let mut result = String::new(); + for _ in 0..char_count { + let remainder = value % 38; + result.push(BASE38_CHARS[remainder as usize]); + value = (value - remainder) / 38; + } + result +} + +/// Decode a base38-encoded string into a byte slice +/// +/// # Arguments +/// * `base38_str` - base38-encoded string to decode +/// +/// Fails if the string contains invalid characters +pub fn decode(base38_str: &str) -> Result, Error> { + let mut result = Vec::new(); + let mut base38_characters_number: usize = base38_str.len(); + let mut decoded_base38_characters: usize = 0; + + while base38_characters_number > 0 { + let base38_characters_in_chunk: usize; + let bytes_in_decoded_chunk: usize; + + if base38_characters_number >= BASE38_CHARACTERS_NEEDED_IN_NBYTES_CHUNK[2] as usize { + base38_characters_in_chunk = BASE38_CHARACTERS_NEEDED_IN_NBYTES_CHUNK[2] as usize; + bytes_in_decoded_chunk = 3; + } else if base38_characters_number == BASE38_CHARACTERS_NEEDED_IN_NBYTES_CHUNK[1] as usize { + base38_characters_in_chunk = BASE38_CHARACTERS_NEEDED_IN_NBYTES_CHUNK[1] as usize; + bytes_in_decoded_chunk = 2; + } else if base38_characters_number == BASE38_CHARACTERS_NEEDED_IN_NBYTES_CHUNK[0] as usize { + base38_characters_in_chunk = BASE38_CHARACTERS_NEEDED_IN_NBYTES_CHUNK[0] as usize; + bytes_in_decoded_chunk = 1; + } else { + return Err(Error::InvalidData); + } + + let mut value = 0u32; + + for i in (1..=base38_characters_in_chunk).rev() { + let mut base38_chars = base38_str.chars(); + let v = decode_char(base38_chars.nth(decoded_base38_characters + i - 1).unwrap())?; + + value = value * RADIX + v as u32; + } + + decoded_base38_characters += base38_characters_in_chunk; + base38_characters_number -= base38_characters_in_chunk; + + for _i in 0..bytes_in_decoded_chunk { + result.push(value as u8); + value >>= 8; + } + + if value > 0 { + // encoded value is too big to represent a correct chunk of size 1, 2 or 3 bytes + return Err(Error::InvalidArgument); + } + } + + Ok(result) +} + +fn decode_char(c: char) -> Result { + let c = c as u8; + if !(45..=90).contains(&c) { + return Err(Error::InvalidData); + } + + let c = DECODE_BASE38[c as usize - 45]; + if c == UNUSED { + return Err(Error::InvalidData); + } + + Ok(c) +} + +#[cfg(test)] +mod tests { + use super::*; + const ENCODED: &str = "-MOA57ZU02IT2L2BJ00"; + const DECODED: [u8; 11] = [ + 0x88, 0xff, 0xa7, 0x91, 0x50, 0x40, 0x00, 0x47, 0x51, 0xdd, 0x02, + ]; + + #[test] + fn can_base38_encode() { + assert_eq!(encode(&DECODED, None), ENCODED); + assert_eq!(encode(&DECODED, Some(11)), ENCODED); + + // length is greater than the length of the byte array + assert_eq!(encode(&DECODED, Some(12)), ENCODED); + } + + #[test] + fn can_base38_decode() { + assert_eq!(decode(ENCODED).expect("can not decode base38"), DECODED); + } +} 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 39a903f..68c07f4 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::{print_pairing_code_and_qr, DiscoveryCapabilities}, secure_channel::{core::SecureChannel, pake::PaseMgr, spake2p::VerifierData}, transport, }; @@ -58,7 +59,9 @@ impl Matter { dev_comm: CommissioningData, ) -> Result, Error> { let mdns = Mdns::get()?; - mdns.set_values(dev_det.vid, dev_det.pid); + mdns.set_values(dev_det.vid, dev_det.pid, &dev_det.device_name); + + print_pairing_code_and_qr(&dev_det, &dev_comm, DiscoveryCapabilities::default()); let fabric_mgr = Arc::new(FabricMgr::new()?); let acl_mgr = Arc::new(AclMgr::new()?); diff --git a/matter/src/data_model/cluster_basic_information.rs b/matter/src/data_model/cluster_basic_information.rs index f78c93c..eedd826 100644 --- a/matter/src/data_model/cluster_basic_information.rs +++ b/matter/src/data_model/cluster_basic_information.rs @@ -31,12 +31,15 @@ enum Attributes { SerialNo = 0x0f, } +#[derive(Default)] pub struct BasicInfoConfig { pub vid: u16, pub pid: u16, pub hw_ver: u16, pub sw_ver: u32, pub serial_no: String, + /// Device name; up to 32 characters + pub device_name: String, } fn attr_dm_rev_new() -> Result { diff --git a/matter/src/error.rs b/matter/src/error.rs index aa326d0..60e30a5 100644 --- a/matter/src/error.rs +++ b/matter/src/error.rs @@ -24,6 +24,7 @@ use log::error; pub enum Error { AttributeNotFound, AttributeIsCustom, + BufferTooSmall, ClusterNotFound, CommandNotFound, EndpointNotFound, @@ -38,6 +39,7 @@ pub enum Error { NoHandler, NoNetworkInterface, NoNodeId, + NoMemory, NoSession, NoSpace, NoSpaceAckTable, @@ -58,6 +60,7 @@ pub enum Error { InvalidSignature, InvalidState, InvalidTime, + InvalidArgument, RwLock, TLVNotFound, TLVTypeMismatch, diff --git a/matter/src/fabric.rs b/matter/src/fabric.rs index 1e91617..bb729ae 100644 --- a/matter/src/fabric.rs +++ b/matter/src/fabric.rs @@ -381,7 +381,7 @@ impl FabricMgr { pub fn set_label(&self, index: u8, label: String) -> Result<(), Error> { let index = index as usize; let mut mgr = self.inner.write()?; - if label != "" { + if !label.is_empty() { for i in 1..MAX_SUPPORTED_FABRICS { if let Some(fabric) = &mgr.fabrics[i] { if fabric.label == label { diff --git a/matter/src/lib.rs b/matter/src/lib.rs index cb4b1e2..4ca7e5a 100644 --- a/matter/src/lib.rs +++ b/matter/src/lib.rs @@ -41,7 +41,6 @@ //! let comm_data = CommissioningData { //! verifier: VerifierData::new_with_pw(123456), //! discriminator: 250, -//! //! }; //! //! /// The basic information about this device @@ -51,6 +50,7 @@ //! hw_ver: 2, //! sw_ver: 1, //! serial_no: "aabbcc".to_string(), +//! device_name: "OnOff Light".to_string(), //! }; //! //! /// Get the Matter Object @@ -69,6 +69,7 @@ pub mod acl; pub mod cert; +pub mod codec; pub mod core; pub mod crypto; pub mod data_model; @@ -77,6 +78,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/mdns.rs b/matter/src/mdns.rs index 4995bb2..f28bea0 100644 --- a/matter/src/mdns.rs +++ b/matter/src/mdns.rs @@ -30,19 +30,20 @@ pub struct MdnsInner { vid: u16, /// Product ID pid: u16, + /// Device name + device_name: String, } pub struct Mdns { inner: Mutex, } -const SHORT_DISCRIMINATOR_MASK: u16 = 0xf00; +const SHORT_DISCRIMINATOR_MASK: u16 = 0xF00; const SHORT_DISCRIMINATOR_SHIFT: u16 = 8; static mut G_MDNS: Option> = None; static INIT: Once = Once::new(); -#[derive(Clone, Copy)] pub enum ServiceMode { /// The commissioned state Commissioned, @@ -72,28 +73,60 @@ impl Mdns { /// Set mDNS service specific values /// Values like vid, pid, discriminator etc // TODO: More things like device-type etc can be added here - pub fn set_values(&self, vid: u16, pid: u16) { + pub fn set_values(&self, vid: u16, pid: u16, device_name: &str) { let mut inner = self.inner.lock().unwrap(); inner.vid = vid; inner.pid = pid; + inner.device_name = device_name.chars().take(32).collect(); } /// Publish a mDNS service /// name - is the service name (comma separated subtypes may follow) /// mode - the current service mode + #[allow(clippy::needless_pass_by_value)] pub fn publish_service(&self, name: &str, mode: ServiceMode) -> Result { match mode { ServiceMode::Commissioned => { sys_publish_service(name, "_matter._tcp", MATTER_PORT, &[]) } ServiceMode::Commissionable(discriminator) => { - let short = (discriminator & SHORT_DISCRIMINATOR_MASK) >> SHORT_DISCRIMINATOR_SHIFT; + let inner = self.inner.lock().unwrap(); + let short = compute_short_discriminator(discriminator); let serv_type = format!("_matterc._udp,_S{},_L{}", short, discriminator); let str_discriminator = format!("{}", discriminator); - let txt_kvs = [["D", &str_discriminator], ["CM", "1"]]; + let txt_kvs = [ + ["D", &str_discriminator], + ["CM", "1"], + ["DN", &inner.device_name], + ["VP", &format!("{}+{}", inner.vid, inner.pid)], + ["SII", "5000"], /* Sleepy Idle Interval */ + ["SAI", "300"], /* Sleepy Active Interval */ + ["PH", "33"], /* Pairing Hint */ + ["PI", ""], /* Pairing Instruction */ + ]; sys_publish_service(name, &serv_type, MATTER_PORT, &txt_kvs) } } } } + +fn compute_short_discriminator(discriminator: u16) -> u16 { + (discriminator & SHORT_DISCRIMINATOR_MASK) >> SHORT_DISCRIMINATOR_SHIFT +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_compute_short_discriminator() { + let discriminator: u16 = 0b0000_1111_0000_0000; + let short = compute_short_discriminator(discriminator); + assert_eq!(short, 0b1111); + + let discriminator: u16 = 840; + let short = compute_short_discriminator(discriminator); + assert_eq!(short, 3); + } +} diff --git a/matter/src/pairing/code.rs b/matter/src/pairing/code.rs new file mode 100644 index 0000000..83d90f3 --- /dev/null +++ b/matter/src/pairing/code.rs @@ -0,0 +1,73 @@ +/* + * + * Copyright (c) 2020-2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use super::*; + +pub(super) 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 passwd = passwd_from_comm_data(comm_data); + let CommissioningData { discriminator, .. } = 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(super) 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); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::secure_channel::spake2p::VerifierData; + + #[test] + fn can_compute_pairing_code() { + let comm_data = CommissioningData { + verifier: VerifierData::new_with_pw(123456), + discriminator: 250, + }; + let pairing_code = compute_pairing_code(&comm_data); + assert_eq!(pairing_code, "00876800071"); + + let comm_data = CommissioningData { + verifier: VerifierData::new_with_pw(34567890), + discriminator: 2976, + }; + let pairing_code = compute_pairing_code(&comm_data); + assert_eq!(pairing_code, "26318621095"); + } +} diff --git a/matter/src/pairing/mod.rs b/matter/src/pairing/mod.rs new file mode 100644 index 0000000..d75e8f6 --- /dev/null +++ b/matter/src/pairing/mod.rs @@ -0,0 +1,104 @@ +/* + * + * Copyright (c) 2020-2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! This module contains the logic for generating the pairing code and the QR code for easy pairing. + +pub mod code; +pub mod qr; +pub mod vendor_identifiers; + +use log::info; +use qrcode::{render::unicode, QrCode, Version}; +use verhoeff::Verhoeff; + +use crate::{ + codec::base38, data_model::cluster_basic_information::BasicInfoConfig, error::Error, + secure_channel::spake2p::VerifierOption, CommissioningData, +}; + +use self::{ + code::{compute_pairing_code, pretty_print_pairing_code}, + qr::{payload_base38_representation, print_qr_code, QrSetupPayload}, +}; + +pub struct DiscoveryCapabilities { + on_ip_network: bool, + ble: bool, + soft_access_point: bool, +} + +impl DiscoveryCapabilities { + pub fn new(on_ip_network: bool, ble: bool, soft_access_point: bool) -> Self { + DiscoveryCapabilities { + on_ip_network, + ble, + soft_access_point, + } + } + + pub fn has_value(&self) -> bool { + self.on_ip_network || self.ble || self.soft_access_point + } +} + +impl Default for DiscoveryCapabilities { + fn default() -> Self { + DiscoveryCapabilities { + on_ip_network: true, + ble: false, + soft_access_point: false, + } + } +} + +impl DiscoveryCapabilities { + 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 + } +} + +/// Prepares and prints the pairing code and the QR code for easy pairing. +pub fn print_pairing_code_and_qr( + dev_det: &BasicInfoConfig, + comm_data: &CommissioningData, + discovery_capabilities: DiscoveryCapabilities, +) { + let pairing_code = compute_pairing_code(comm_data); + let qr_code_data = QrSetupPayload::new(dev_det, comm_data, discovery_capabilities); + let data_str = payload_base38_representation(&qr_code_data).expect("Failed to encode"); + + pretty_print_pairing_code(&pairing_code); + print_qr_code(&data_str); +} + +pub(self) fn passwd_from_comm_data(comm_data: &CommissioningData) -> u32 { + // todo: should this be part of the comm_data implementation? + match comm_data.verifier.data { + VerifierOption::Password(pwd) => pwd, + VerifierOption::Verifier(_) => 0, + } +} diff --git a/matter/src/pairing/qr.rs b/matter/src/pairing/qr.rs new file mode 100644 index 0000000..0a3509d --- /dev/null +++ b/matter/src/pairing/qr.rs @@ -0,0 +1,623 @@ +/* + * + * Copyright (c) 2020-2022 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::collections::BTreeMap; + +use crate::{ + tlv::{TLVWriter, TagType}, + utils::writebuf::WriteBuf, +}; + +use super::{ + vendor_identifiers::{is_vendor_id_valid_operationally, VendorId}, + *, +}; + +// See section 5.1.2. QR Code in the Matter specification +const LONG_BITS: usize = 12; +const VERSION_FIELD_LENGTH_IN_BITS: usize = 3; +const VENDOR_IDFIELD_LENGTH_IN_BITS: usize = 16; +const PRODUCT_IDFIELD_LENGTH_IN_BITS: usize = 16; +const COMMISSIONING_FLOW_FIELD_LENGTH_IN_BITS: usize = 2; +const RENDEZVOUS_INFO_FIELD_LENGTH_IN_BITS: usize = 8; +const PAYLOAD_DISCRIMINATOR_FIELD_LENGTH_IN_BITS: usize = LONG_BITS; +const SETUP_PINCODE_FIELD_LENGTH_IN_BITS: usize = 27; +const PADDING_FIELD_LENGTH_IN_BITS: usize = 4; +const TOTAL_PAYLOAD_DATA_SIZE_IN_BITS: usize = VERSION_FIELD_LENGTH_IN_BITS + + VENDOR_IDFIELD_LENGTH_IN_BITS + + PRODUCT_IDFIELD_LENGTH_IN_BITS + + COMMISSIONING_FLOW_FIELD_LENGTH_IN_BITS + + RENDEZVOUS_INFO_FIELD_LENGTH_IN_BITS + + PAYLOAD_DISCRIMINATOR_FIELD_LENGTH_IN_BITS + + SETUP_PINCODE_FIELD_LENGTH_IN_BITS + + PADDING_FIELD_LENGTH_IN_BITS; +const TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES: usize = TOTAL_PAYLOAD_DATA_SIZE_IN_BITS / 8; + +// Spec 5.1.4.2 CHIP-Common Reserved Tags +const SERIAL_NUMBER_TAG: u8 = 0x00; +// const PBKDFITERATIONS_TAG: u8 = 0x01; +// const BPKFSALT_TAG: u8 = 0x02; +// const NUMBER_OFDEVICES_TAG: u8 = 0x03; +// const COMMISSIONING_TIMEOUT_TAG: u8 = 0x04; + +pub enum QRCodeInfoType { + String(String), + Int32(i32), + Int64(i64), + UInt32(u32), + UInt64(u64), +} + +pub enum SerialNumber { + String(String), + UInt32(u32), +} + +pub struct OptionalQRCodeInfo { + // the tag number of the optional info + pub tag: u8, + // the data of the optional info + pub data: QRCodeInfoType, +} + +pub struct QrSetupPayload<'data> { + version: u8, + flow_type: CommissionningFlowType, + discovery_capabilities: DiscoveryCapabilities, + dev_det: &'data BasicInfoConfig, + comm_data: &'data CommissioningData, + // we use a BTreeMap to keep the order of the optional data stable + optional_data: BTreeMap, +} + +impl<'data> QrSetupPayload<'data> { + pub fn new( + dev_det: &'data BasicInfoConfig, + comm_data: &'data CommissioningData, + discovery_capabilities: DiscoveryCapabilities, + ) -> Self { + const DEFAULT_VERSION: u8 = 0; + + let mut result = QrSetupPayload { + version: DEFAULT_VERSION, + flow_type: CommissionningFlowType::Standard, + discovery_capabilities, + dev_det, + comm_data, + optional_data: BTreeMap::new(), + }; + + if !dev_det.serial_no.is_empty() { + result.add_serial_number(SerialNumber::String(dev_det.serial_no.clone())); + } + + result + } + + fn is_valid(&self) -> bool { + let passwd = passwd_from_comm_data(self.comm_data); + + // 3-bit value specifying the QR code payload version. + if self.version >= 1 << VERSION_FIELD_LENGTH_IN_BITS { + return false; + } + + if !self.discovery_capabilities.has_value() { + return false; + } + + if passwd >= 1 << SETUP_PINCODE_FIELD_LENGTH_IN_BITS { + return false; + } + + self.check_payload_common_constraints() + } + + /// A function to add an optional vendor data + /// # Arguments + /// * `tag` - tag number in the [0x80-0xFF] range + /// * `data` - Data to add + pub fn add_optional_vendor_data(&mut self, tag: u8, data: QRCodeInfoType) -> Result<(), Error> { + if !is_vendor_tag(tag) { + return Err(Error::InvalidArgument); + } + + self.optional_data + .insert(tag, OptionalQRCodeInfo { tag, data }); + Ok(()) + } + + /// A function to add an optional QR Code info CHIP object + /// # Arguments + /// * `tag` - one of the CHIP-Common Reserved Tags + /// * `data` - Data to add + pub fn add_optional_extension_data( + &mut self, + tag: u8, + data: QRCodeInfoType, + ) -> Result<(), Error> { + if !is_common_tag(tag) { + return Err(Error::InvalidArgument); + } + + self.optional_data + .insert(tag, OptionalQRCodeInfo { tag, data }); + Ok(()) + } + + pub fn get_all_optional_data(&self) -> &BTreeMap { + &self.optional_data + } + + pub fn add_serial_number(&mut self, serial_number: SerialNumber) { + match serial_number { + SerialNumber::String(serial_number) => self.add_optional_extension_data( + SERIAL_NUMBER_TAG, + QRCodeInfoType::String(serial_number), + ), + SerialNumber::UInt32(serial_number) => self.add_optional_extension_data( + SERIAL_NUMBER_TAG, + QRCodeInfoType::UInt32(serial_number), + ), + } + .expect("can not add serial number"); + } + + fn check_payload_common_constraints(&self) -> bool { + // A version not equal to 0 would be invalid for v1 and would indicate new format (e.g. version 2) + if self.version != 0 { + return false; + } + + let passwd = passwd_from_comm_data(self.comm_data); + + if !Self::is_valid_setup_pin(passwd) { + return false; + } + + // VendorID must be unspecified (0) or in valid range expected. + if !is_vendor_id_valid_operationally(self.dev_det.vid) + && (self.dev_det.vid != VendorId::CommonOrUnspecified as u16) + { + return false; + } + + // A value of 0x0000 SHALL NOT be assigned to a product since Product ID = 0x0000 is used for these specific cases: + // * To announce an anonymized Product ID as part of device discovery + // * To indicate an OTA software update file applies to multiple Product IDs equally. + // * To avoid confusion when presenting the Onboarding Payload for ECM with multiple nodes + if self.dev_det.pid == 0 && self.dev_det.vid != VendorId::CommonOrUnspecified as u16 { + return false; + } + + true + } + + fn is_valid_setup_pin(setup_pin: u32) -> bool { + const SETUP_PINCODE_MAXIMUM_VALUE: u32 = 99999998; + const SETUP_PINCODE_UNDEFINED_VALUE: u32 = 0; + + // SHALL be restricted to the values 0x0000001 to 0x5F5E0FE (00000001 to 99999998 in decimal), excluding the invalid Passcode + // values. + if setup_pin == SETUP_PINCODE_UNDEFINED_VALUE + || setup_pin > SETUP_PINCODE_MAXIMUM_VALUE + || setup_pin == 11111111 + || setup_pin == 22222222 + || setup_pin == 33333333 + || setup_pin == 44444444 + || setup_pin == 55555555 + || setup_pin == 66666666 + || setup_pin == 77777777 + || setup_pin == 88888888 + || setup_pin == 12345678 + || setup_pin == 87654321 + { + return false; + } + + true + } + + fn has_tlv(&self) -> bool { + !self.optional_data.is_empty() + } +} + +#[repr(u8)] +#[derive(Clone, Copy)] +pub enum CommissionningFlowType { + Standard = 0, + UserIntent = 1, + Custom = 2, +} + +struct TlvData { + max_data_length_in_bytes: u32, + data_length_in_bytes: Option, + data: Option>, +} + +pub(super) fn payload_base38_representation(payload: &QrSetupPayload) -> Result { + let (mut bits, tlv_data) = if payload.has_tlv() { + let buffer_size = estimate_buffer_size(payload)?; + ( + vec![0; buffer_size], + Some(TlvData { + max_data_length_in_bytes: buffer_size as u32, + data_length_in_bytes: None, + data: None, + }), + ) + } else { + (vec![0; TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES], None) + }; + + if !payload.is_valid() { + return Err(Error::InvalidArgument); + } + + payload_base38_representation_with_tlv(payload, &mut bits, tlv_data) +} + +fn estimate_buffer_size(payload: &QrSetupPayload) -> Result { + // Estimate the size of the needed buffer; initialize with the size of the standard payload. + let mut estimate = TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES; + + let data_item_size_estimate = |info: &QRCodeInfoType| { + // Each data item needs a control byte and a context tag. + let mut size: usize = 2; + + if let QRCodeInfoType::String(data) = info { + // We'll need to encode the string length and then the string data. + // Length is at most 8 bytes. + size += 8; + size += data.as_bytes().len() + } else { + // Integer. Assume it might need up to 8 bytes, for simplicity. + size += 8; + } + + size + }; + + let vendor_data = payload.get_all_optional_data(); + vendor_data.values().for_each(|data| { + estimate += data_item_size_estimate(&data.data); + }); + + estimate = estimate_struct_overhead(estimate); + + if estimate > u32::MAX as usize { + return Err(Error::NoMemory); + } + + Ok(estimate) +} + +fn estimate_struct_overhead(first_field_size: usize) -> usize { + // Estimate 4 bytes of overhead per field. This can happen for a large + // octet string field: 1 byte control, 1 byte context tag, 2 bytes + // length. + // + // The struct itself has a control byte and an end-of-struct marker. + first_field_size + 4 + 2 +} + +pub(super) fn print_qr_code(qr_data: &str) { + let needed_version = compute_qr_version(qr_data); + let code = + QrCode::with_version(qr_data, Version::Normal(needed_version), qrcode::EcLevel::M).unwrap(); + let image = code + .render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + info!("\n{}", image); +} + +fn compute_qr_version(qr_data: &str) -> i16 { + match qr_data.len() { + 0..=38 => 2, + 39..=61 => 3, + 62..=90 => 4, + _ => 5, + } +} + +fn populate_bits( + bits: &mut [u8], + offset: &mut usize, + mut input: u64, + number_of_bits: usize, + total_payload_data_size_in_bits: usize, +) -> Result<(), Error> { + if *offset + number_of_bits > total_payload_data_size_in_bits { + return Err(Error::InvalidArgument); + } + + if input >= 1u64 << number_of_bits { + return Err(Error::InvalidArgument); + } + + let mut index = *offset; + *offset += number_of_bits; + + while input != 0 { + if input & 1 == 1 { + let mask = (1 << (index % 8)) as u8; + bits[index / 8] |= mask; + } + index += 1; + input >>= 1; + } + + Ok(()) +} + +fn payload_base38_representation_with_tlv( + payload: &QrSetupPayload, + bits: &mut [u8], + mut tlv_data: Option, +) -> Result { + if let Some(tlv_data) = tlv_data.as_mut() { + generate_tlv_from_optional_data(payload, tlv_data)?; + } + + let bytes_written = generate_bit_set(payload, bits, tlv_data)?; + let base38_encoded = base38::encode(&*bits, Some(bytes_written)); + Ok(format!("MT:{}", base38_encoded)) +} + +fn generate_tlv_from_optional_data( + payload: &QrSetupPayload, + tlv_data: &mut TlvData, +) -> Result<(), Error> { + let size_needed = tlv_data.max_data_length_in_bytes as usize; + let mut tlv_buffer = vec![0u8; size_needed]; + let mut wb = WriteBuf::new(&mut tlv_buffer, size_needed); + let mut tw = TLVWriter::new(&mut wb); + + tw.start_struct(TagType::Anonymous)?; + let data = payload.get_all_optional_data(); + + for (tag, value) in data { + match &value.data { + QRCodeInfoType::String(data) => tw.utf8(TagType::Context(*tag), data.as_bytes())?, + QRCodeInfoType::Int32(data) => tw.i32(TagType::Context(*tag), *data)?, + QRCodeInfoType::Int64(data) => tw.i64(TagType::Context(*tag), *data)?, + QRCodeInfoType::UInt32(data) => tw.u32(TagType::Context(*tag), *data)?, + QRCodeInfoType::UInt64(data) => tw.u64(TagType::Context(*tag), *data)?, + } + } + + tw.end_container()?; + tlv_data.data_length_in_bytes = Some(tw.get_tail()); + tlv_data.data = Some(tlv_buffer); + + Ok(()) +} + +fn generate_bit_set( + payload: &QrSetupPayload, + bits: &mut [u8], + tlv_data: Option, +) -> Result { + let mut offset: usize = 0; + + let total_payload_size_in_bits = if let Some(tlv_data) = &tlv_data { + TOTAL_PAYLOAD_DATA_SIZE_IN_BITS + (tlv_data.data_length_in_bytes.unwrap_or_default() * 8) + } else { + TOTAL_PAYLOAD_DATA_SIZE_IN_BITS + }; + + if bits.len() * 8 < total_payload_size_in_bits { + return Err(Error::BufferTooSmall); + }; + + let passwd = passwd_from_comm_data(payload.comm_data); + + populate_bits( + bits, + &mut offset, + payload.version as u64, + VERSION_FIELD_LENGTH_IN_BITS, + total_payload_size_in_bits, + )?; + + populate_bits( + bits, + &mut offset, + payload.dev_det.vid as u64, + VENDOR_IDFIELD_LENGTH_IN_BITS, + total_payload_size_in_bits, + )?; + + populate_bits( + bits, + &mut offset, + payload.dev_det.pid as u64, + PRODUCT_IDFIELD_LENGTH_IN_BITS, + total_payload_size_in_bits, + )?; + + populate_bits( + bits, + &mut offset, + payload.flow_type as u64, + COMMISSIONING_FLOW_FIELD_LENGTH_IN_BITS, + total_payload_size_in_bits, + )?; + + populate_bits( + bits, + &mut offset, + payload.discovery_capabilities.as_bits() as u64, + RENDEZVOUS_INFO_FIELD_LENGTH_IN_BITS, + total_payload_size_in_bits, + )?; + + populate_bits( + bits, + &mut offset, + payload.comm_data.discriminator as u64, + PAYLOAD_DISCRIMINATOR_FIELD_LENGTH_IN_BITS, + total_payload_size_in_bits, + )?; + + populate_bits( + bits, + &mut offset, + passwd as u64, + SETUP_PINCODE_FIELD_LENGTH_IN_BITS, + total_payload_size_in_bits, + )?; + + populate_bits( + bits, + &mut offset, + 0, + PADDING_FIELD_LENGTH_IN_BITS, + total_payload_size_in_bits, + )?; + + if let Some(tlv_data) = tlv_data { + populate_tlv_bits(bits, &mut offset, tlv_data, total_payload_size_in_bits)?; + } + + let bytes_written = (offset + 7) / 8; + Ok(bytes_written) +} + +fn populate_tlv_bits( + bits: &mut [u8], + offset: &mut usize, + tlv_data: TlvData, + total_payload_size_in_bits: usize, +) -> Result<(), Error> { + if let (Some(data), Some(data_length_in_bytes)) = (tlv_data.data, tlv_data.data_length_in_bytes) + { + for b in data.iter().take(data_length_in_bytes) { + populate_bits(bits, offset, *b as u64, 8, total_payload_size_in_bits)?; + } + } else { + return Err(Error::InvalidArgument); + } + + Ok(()) +} + +/// Spec 5.1.4.1 Manufacture-specific tag numbers are in the range [0x80, 0xFF] +fn is_vendor_tag(tag: u8) -> bool { + !is_common_tag(tag) +} + +/// Spec 5.1.4.2 CHIPCommon tag numbers are in the range [0x00, 0x7F] +fn is_common_tag(tag: u8) -> bool { + tag < 0x80 +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::secure_channel::spake2p::VerifierData; + + #[test] + fn can_base38_encode() { + const QR_CODE: &str = "MT:YNJV7VSC00CMVH7SR00"; + + let comm_data = CommissioningData { + verifier: VerifierData::new_with_pw(34567890), + discriminator: 2976, + }; + let dev_det = BasicInfoConfig { + vid: 9050, + pid: 65279, + ..Default::default() + }; + + let disc_cap = DiscoveryCapabilities::new(false, true, false); + let qr_code_data = QrSetupPayload::new(&dev_det, &comm_data, disc_cap); + let data_str = payload_base38_representation(&qr_code_data).expect("Failed to encode"); + assert_eq!(data_str, QR_CODE) + } + + #[test] + fn can_base38_encode_with_vendor_data() { + const QR_CODE: &str = "MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1-A40"; + + let comm_data = CommissioningData { + verifier: VerifierData::new_with_pw(20202021), + discriminator: 3840, + }; + let dev_det = BasicInfoConfig { + vid: 65521, + pid: 32769, + serial_no: "1234567890".to_string(), + ..Default::default() + }; + + let disc_cap = DiscoveryCapabilities::new(true, false, false); + let qr_code_data = QrSetupPayload::new(&dev_det, &comm_data, disc_cap); + let data_str = payload_base38_representation(&qr_code_data).expect("Failed to encode"); + assert_eq!(data_str, QR_CODE) + } + + #[test] + fn can_base38_encode_with_optional_data() { + const QR_CODE: &str = + "MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1UXH34YY0V3KY.O3DKN440F710Q940"; + const OPTIONAL_DEFAULT_STRING_TAG: u8 = 0x82; // Vendor "test" tag + const OPTIONAL_DEFAULT_STRING_VALUE: &str = "myData"; + + const OPTIONAL_DEFAULT_INT_TAG: u8 = 0x83; // Vendor "test" tag + const OPTIONAL_DEFAULT_INT_VALUE: i32 = 65550; + + let comm_data = CommissioningData { + verifier: VerifierData::new_with_pw(20202021), + discriminator: 3840, + }; + let dev_det = BasicInfoConfig { + vid: 65521, + pid: 32769, + serial_no: "1234567890".to_string(), + ..Default::default() + }; + + let disc_cap = DiscoveryCapabilities::new(true, false, false); + let mut qr_code_data = QrSetupPayload::new(&dev_det, &comm_data, disc_cap); + + qr_code_data + .add_optional_vendor_data( + OPTIONAL_DEFAULT_STRING_TAG, + QRCodeInfoType::String(OPTIONAL_DEFAULT_STRING_VALUE.to_string()), + ) + .expect("Failed to add optional data"); + + // todo: check why unsigned ints are not accepted by 'chip-tool payload parse-setup-payload' + + qr_code_data + .add_optional_vendor_data( + OPTIONAL_DEFAULT_INT_TAG, + QRCodeInfoType::Int32(OPTIONAL_DEFAULT_INT_VALUE), + ) + .expect("Failed to add optional data"); + + let data_str = payload_base38_representation(&qr_code_data).expect("Failed to encode"); + assert_eq!(data_str, QR_CODE) + } +} diff --git a/matter/src/pairing/vendor_identifiers.rs b/matter/src/pairing/vendor_identifiers.rs new file mode 100644 index 0000000..62c07ac --- /dev/null +++ b/matter/src/pairing/vendor_identifiers.rs @@ -0,0 +1,10 @@ +#[repr(u16)] +pub enum VendorId { + CommonOrUnspecified = 0x0000, + TestVendor4 = 0xFFF4, +} + +pub fn is_vendor_id_valid_operationally(vendor_id: u16) -> bool { + (vendor_id != VendorId::CommonOrUnspecified as u16) + && (vendor_id <= VendorId::TestVendor4 as u16) +} diff --git a/matter/src/secure_channel/crypto_mbedtls.rs b/matter/src/secure_channel/crypto_mbedtls.rs index 7231f63..7ac4c5a 100644 --- a/matter/src/secure_channel/crypto_mbedtls.rs +++ b/matter/src/secure_channel/crypto_mbedtls.rs @@ -115,7 +115,7 @@ impl CryptoSpake2 for CryptoMbedTLS { } fn set_L(&mut self, l: &[u8]) -> Result<(), Error> { - self.L = EcPoint::from_binary(&mut self.group, l)?; + self.L = EcPoint::from_binary(&self.group, l)?; Ok(()) } diff --git a/matter/src/tlv/writer.rs b/matter/src/tlv/writer.rs index 9cefb0f..7459aa3 100644 --- a/matter/src/tlv/writer.rs +++ b/matter/src/tlv/writer.rs @@ -95,6 +95,15 @@ impl<'a, 'b> TLVWriter<'a, 'b> { self.buf.le_u8(data) } + pub fn i16(&mut self, tag_type: TagType, data: i16) -> Result<(), Error> { + if data >= i8::MIN as i16 && data <= i8::MAX as i16 { + self.i8(tag_type, data as i8) + } else { + self.put_control_tag(tag_type, WriteElementType::S16)?; + self.buf.le_i16(data) + } + } + pub fn u16(&mut self, tag_type: TagType, data: u16) -> Result<(), Error> { if data <= 0xff { self.u8(tag_type, data as u8) @@ -104,6 +113,17 @@ impl<'a, 'b> TLVWriter<'a, 'b> { } } + pub fn i32(&mut self, tag_type: TagType, data: i32) -> Result<(), Error> { + if data >= i8::MIN as i32 && data <= i8::MAX as i32 { + self.i8(tag_type, data as i8) + } else if data >= i16::MIN as i32 && data <= i16::MAX as i32 { + self.i16(tag_type, data as i16) + } else { + self.put_control_tag(tag_type, WriteElementType::S32)?; + self.buf.le_i32(data) + } + } + pub fn u32(&mut self, tag_type: TagType, data: u32) -> Result<(), Error> { if data <= 0xff { self.u8(tag_type, data as u8) @@ -115,6 +135,19 @@ impl<'a, 'b> TLVWriter<'a, 'b> { } } + pub fn i64(&mut self, tag_type: TagType, data: i64) -> Result<(), Error> { + if data >= i8::MIN as i64 && data <= i8::MAX as i64 { + self.i8(tag_type, data as i8) + } else if data >= i16::MIN as i64 && data <= i16::MAX as i64 { + self.i16(tag_type, data as i16) + } else if data >= i32::MIN as i64 && data <= i32::MAX as i64 { + self.i32(tag_type, data as i32) + } else { + self.put_control_tag(tag_type, WriteElementType::S64)?; + self.buf.le_i64(data) + } + } + pub fn u64(&mut self, tag_type: TagType, data: u64) -> Result<(), Error> { if data <= 0xff { self.u8(tag_type, data as u8) diff --git a/matter/src/utils/writebuf.rs b/matter/src/utils/writebuf.rs index b82bb02..00c5e88 100644 --- a/matter/src/utils/writebuf.rs +++ b/matter/src/utils/writebuf.rs @@ -131,6 +131,11 @@ impl<'a> WriteBuf<'a> { LittleEndian::write_u16(&mut x.buf[x.end..], data); }) } + pub fn le_i16(&mut self, data: i16) -> Result<(), Error> { + self.append_with(2, |x| { + LittleEndian::write_i16(&mut x.buf[x.end..], data); + }) + } pub fn le_u32(&mut self, data: u32) -> Result<(), Error> { self.append_with(4, |x| { @@ -138,12 +143,24 @@ impl<'a> WriteBuf<'a> { }) } + pub fn le_i32(&mut self, data: i32) -> Result<(), Error> { + self.append_with(4, |x| { + LittleEndian::write_i32(&mut x.buf[x.end..], data); + }) + } + pub fn le_u64(&mut self, data: u64) -> Result<(), Error> { self.append_with(8, |x| { LittleEndian::write_u64(&mut x.buf[x.end..], data); }) } + pub fn le_i64(&mut self, data: i64) -> Result<(), Error> { + self.append_with(8, |x| { + LittleEndian::write_i64(&mut x.buf[x.end..], data); + }) + } + pub fn le_uint(&mut self, nbytes: usize, data: u64) -> Result<(), Error> { self.append_with(nbytes, |x| { LittleEndian::write_uint(&mut x.buf[x.end..], data, nbytes); diff --git a/matter/tests/common/im_engine.rs b/matter/tests/common/im_engine.rs index b0a1d64..2ee55c8 100644 --- a/matter/tests/common/im_engine.rs +++ b/matter/tests/common/im_engine.rs @@ -95,7 +95,9 @@ impl ImEngine { hw_ver: 12, sw_ver: 13, serial_no: "aabbccdd".to_string(), + device_name: "Test Device".to_string(), }; + let dev_att = Box::new(DummyDevAtt {}); let fabric_mgr = Arc::new(FabricMgr::new().unwrap()); let acl_mgr = Arc::new(AclMgr::new_with(false).unwrap()); @@ -105,14 +107,7 @@ impl ImEngine { // Only allow the standard peer node id of the IM Engine default_acl.add_subject(IM_ENGINE_PEER_ID).unwrap(); acl_mgr.add(default_acl).unwrap(); - let dm = DataModel::new( - dev_det, - dev_att, - fabric_mgr.clone(), - acl_mgr.clone(), - pase_mgr, - ) - .unwrap(); + let dm = DataModel::new(dev_det, dev_att, fabric_mgr, acl_mgr.clone(), pase_mgr).unwrap(); { let mut d = dm.node.write().unwrap();