From b74ae4ff01d55f12b31ea7a5b3f407ed4e3cc0b7 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 12 Jan 2023 13:13:50 +0100 Subject: [PATCH 1/8] Prep for optional TLV data --- matter/src/error.rs | 1 + matter/src/pairing/mod.rs | 4 +- matter/src/pairing/qr.rs | 174 +++++++++++++++++++++++++++++++++++--- 3 files changed, 164 insertions(+), 15 deletions(-) diff --git a/matter/src/error.rs b/matter/src/error.rs index 68e7f21..60e30a5 100644 --- a/matter/src/error.rs +++ b/matter/src/error.rs @@ -39,6 +39,7 @@ pub enum Error { NoHandler, NoNetworkInterface, NoNodeId, + NoMemory, NoSession, NoSpace, NoSpaceAckTable, diff --git a/matter/src/pairing/mod.rs b/matter/src/pairing/mod.rs index 96233bc..f27266d 100644 --- a/matter/src/pairing/mod.rs +++ b/matter/src/pairing/mod.rs @@ -32,7 +32,7 @@ use crate::{ use self::{ code::{compute_pairing_code, pretty_print_pairing_code}, - qr::{payload_base38_representation, print_qr_code, QrCodeData}, + qr::{payload_base38_representation, print_qr_code, QrSetupPayload}, }; pub struct DiscoveryCapabilities { @@ -88,7 +88,7 @@ pub fn print_pairing_code_and_qr( discovery_capabilities: DiscoveryCapabilities, ) { let pairing_code = compute_pairing_code(comm_data); - let qr_code_data = QrCodeData::new(dev_det, comm_data, discovery_capabilities); + 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); diff --git a/matter/src/pairing/qr.rs b/matter/src/pairing/qr.rs index 4e7bd29..1bca10b 100644 --- a/matter/src/pairing/qr.rs +++ b/matter/src/pairing/qr.rs @@ -15,11 +15,14 @@ * limitations under the License. */ +use std::collections::HashMap; + 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; @@ -39,15 +42,43 @@ const TOTAL_PAYLOAD_DATA_SIZE_IN_BITS: usize = VERSION_FIELD_LENGTH_IN_BITS + PADDING_FIELD_LENGTH_IN_BITS; const TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES: usize = TOTAL_PAYLOAD_DATA_SIZE_IN_BITS / 8; -pub struct QrCodeData<'data> { +// 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, + optional_data: HashMap, } -impl<'data> QrCodeData<'data> { +impl<'data> QrSetupPayload<'data> { pub fn new( dev_det: &'data BasicInfoConfig, comm_data: &'data CommissioningData, @@ -55,12 +86,13 @@ impl<'data> QrCodeData<'data> { ) -> Self { const DEFAULT_VERSION: u8 = 0; - QrCodeData { + QrSetupPayload { version: DEFAULT_VERSION, flow_type: CommissionningFlowType::Standard, discovery_capabilities, dev_det, comm_data, + optional_data: HashMap::new(), } } @@ -81,6 +113,55 @@ impl<'data> QrCodeData<'data> { 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) -> &HashMap { + &self.optional_data + } + + pub fn add_serial_number(&mut self, serial_number: SerialNumber) -> Result<(), Error> { + 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), + ), + } + } + 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 { @@ -133,6 +214,10 @@ impl<'data> QrCodeData<'data> { true } + + fn has_tlv(&self) -> bool { + !self.optional_data.is_empty() + } } #[repr(u8)] @@ -147,14 +232,67 @@ struct TlvData { data_length_in_bytes: u32, } -pub(super) fn payload_base38_representation(payload: &QrCodeData) -> Result { - let mut bits: [u8; TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES] = [0; TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES]; +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 { + data_length_in_bytes: buffer_size as u32, + }), + ) + } 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, None) + payload_base38_representation_with_tlv(payload, &mut bits, tlv_data) +} + +fn estimate_buffer_size(payload: &QrSetupPayload) -> Result { + // Estimate the size of the needed buffer. + let mut estimate = 0; + + 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.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. + // todo: recursive process other fields + first_field_size + 4 } pub(super) fn print_qr_code(qr_data: &str) { @@ -198,9 +336,9 @@ fn populate_bits( } fn payload_base38_representation_with_tlv( - payload: &QrCodeData, - bits: &mut [u8; TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES], - tlv_data: Option<&TlvData>, + payload: &QrSetupPayload, + bits: &mut [u8], + tlv_data: Option, ) -> Result { generate_bit_set(payload, bits, tlv_data)?; let base38_encoded = base38::encode(&*bits); @@ -208,9 +346,9 @@ fn payload_base38_representation_with_tlv( } fn generate_bit_set( - payload: &QrCodeData, - bits: &mut [u8; TOTAL_PAYLOAD_DATA_SIZE_IN_BYTES], - tlv_data: Option<&TlvData>, + payload: &QrSetupPayload, + bits: &mut [u8], + tlv_data: Option, ) -> Result<(), Error> { let mut offset: usize = 0; let total_payload_size_in_bits = if let Some(tlv_data) = tlv_data { @@ -313,8 +451,18 @@ mod tests { }; let disc_cap = DiscoveryCapabilities::new(false, true, false); - let qr_code_data = QrCodeData::new(&dev_det, &comm_data, disc_cap); + 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) } } + +/// 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 +} From f39cff0bc24ba45904be927b628449669fbd3b0c Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 12 Jan 2023 15:42:21 +0100 Subject: [PATCH 2/8] Encode optional data fields --- matter/src/codec/base38.rs | 5 +- matter/src/pairing/qr.rs | 170 +++++++++++++++++++++++++++++++------ 2 files changed, 148 insertions(+), 27 deletions(-) diff --git a/matter/src/codec/base38.rs b/matter/src/codec/base38.rs index 8bea287..8beec25 100644 --- a/matter/src/codec/base38.rs +++ b/matter/src/codec/base38.rs @@ -20,8 +20,7 @@ const BASE38_CHARS: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-."; /// Encodes a byte array into a base38 string. -pub fn encode(bytes: &[u8]) -> String { - let length = bytes.len(); +pub fn encode(bytes: &[u8], length: usize) -> String { let mut offset = 0; let mut result = String::new(); @@ -75,6 +74,6 @@ mod tests { const DECODED: [u8; 11] = [ 0x88, 0xff, 0xa7, 0x91, 0x50, 0x40, 0x00, 0x47, 0x51, 0xdd, 0x02, ]; - assert_eq!(encode(&DECODED), ENCODED); + assert_eq!(encode(&DECODED, 11), ENCODED); } } diff --git a/matter/src/pairing/qr.rs b/matter/src/pairing/qr.rs index 1bca10b..3107e63 100644 --- a/matter/src/pairing/qr.rs +++ b/matter/src/pairing/qr.rs @@ -15,7 +15,12 @@ * limitations under the License. */ -use std::collections::HashMap; +use std::collections::BTreeMap; + +use crate::{ + tlv::{TLVWriter, TagType}, + utils::writebuf::WriteBuf, +}; use super::{ vendor_identifiers::{is_vendor_id_valid_operationally, VendorId}, @@ -75,7 +80,8 @@ pub struct QrSetupPayload<'data> { discovery_capabilities: DiscoveryCapabilities, dev_det: &'data BasicInfoConfig, comm_data: &'data CommissioningData, - optional_data: HashMap, + // we use a BTreeMap to keep the order of the optional data stable + optional_data: BTreeMap, } impl<'data> QrSetupPayload<'data> { @@ -92,7 +98,7 @@ impl<'data> QrSetupPayload<'data> { discovery_capabilities, dev_det, comm_data, - optional_data: HashMap::new(), + optional_data: BTreeMap::new(), } } @@ -145,7 +151,7 @@ impl<'data> QrSetupPayload<'data> { Ok(()) } - pub fn get_all_optional_data(&self) -> &HashMap { + pub fn get_all_optional_data(&self) -> &BTreeMap { &self.optional_data } @@ -229,7 +235,9 @@ pub enum CommissionningFlowType { } struct TlvData { - data_length_in_bytes: u32, + max_data_length_in_bytes: u32, + data_length_in_bytes: Option, + data: Option>, } pub(super) fn payload_base38_representation(payload: &QrSetupPayload) -> Result { @@ -238,7 +246,9 @@ pub(super) fn payload_base38_representation(payload: &QrSetupPayload) -> Result< ( vec![0; buffer_size], Some(TlvData { - data_length_in_bytes: buffer_size as u32, + max_data_length_in_bytes: buffer_size as u32, + data_length_in_bytes: None, + data: None, }), ) } else { @@ -264,7 +274,7 @@ fn estimate_buffer_size(payload: &QrSetupPayload) -> Result { // We'll need to encode the string length and then the string data. // Length is at most 8 bytes. size += 8; - size += data.len(); + size += data.as_bytes().len() } else { // Integer. Assume it might need up to 8 bytes, for simplicity. size += 8; @@ -291,7 +301,6 @@ 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. - // todo: recursive process other fields first_field_size + 4 } @@ -338,21 +347,63 @@ fn populate_bits( fn payload_base38_representation_with_tlv( payload: &QrSetupPayload, bits: &mut [u8], - tlv_data: Option, + mut tlv_data: Option, ) -> Result { - generate_bit_set(payload, bits, tlv_data)?; - let base38_encoded = base38::encode(&*bits); + 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, 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) => { + if data.len() > 256 { + tw.str16(TagType::Context(*tag), data.as_bytes())?; + } else { + tw.str8(TagType::Context(*tag), data.as_bytes())?; + } + } + // todo: check i32 -> u32?? + QRCodeInfoType::Int32(data) => tw.u32(TagType::Context(*tag), *data as u32)?, + // todo: check i64 -> u64?? + QRCodeInfoType::Int64(data) => tw.u64(TagType::Context(*tag), *data as u64)?, + 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<(), Error> { +) -> 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 * 8) as usize + + 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 }; @@ -425,12 +476,48 @@ fn generate_bit_set( total_payload_size_in_bits, )?; - // todo: add tlv data - // ReturnErrorOnFailure(populateTLVBits(bits.data(), offset, tlvDataStart, tlvDataLengthInBytes, totalPayloadSizeInBits)); + 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 pos in 0..data_length_in_bytes { + populate_bits( + bits, + offset, + data[pos] 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::*; @@ -455,14 +542,49 @@ mod tests { let data_str = payload_base38_representation(&qr_code_data).expect("Failed to encode"); assert_eq!(data_str, QR_CODE) } -} -/// 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) -} + #[test] + fn can_base38_encode_with_optional_data() { + // todo: this must be validated! + const QR_CODE: &str = "MT:YNJV7VSC00CMVH70V3P0-ISA0DK5N1K8SQ1RYCU1WET70.QT52B.E232XZE0O0"; + const OPTIONAL_DEFAULT_STRING_TAG: u8 = 0x82; // Vendor "test" tag + const OPTIONAL_DEFAULT_STRING_VALUE: &str = "myData"; -/// Spec 5.1.4.2 CHIPCommon tag numbers are in the range [0x00, 0x7F] -fn is_common_tag(tag: u8) -> bool { - tag < 0x80 + const OPTIONAL_DEFAULT_INT_TAG: u8 = 0x83; // Vendor "test" tag + const OPTIONAL_DEFAULT_INT_VALUE: u32 = 12; + + let comm_data = CommissioningData { + passwd: 34567890, + discriminator: 2976, + ..Default::default() + }; + let dev_det = BasicInfoConfig { + vid: 9050, + pid: 65279, + ..Default::default() + }; + + let disc_cap = DiscoveryCapabilities::new(false, true, false); + let mut qr_code_data = QrSetupPayload::new(&dev_det, &comm_data, disc_cap); + qr_code_data + .add_serial_number(SerialNumber::String("123456789".to_string())) + .expect("Failed to add serial number"); + + 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"); + + qr_code_data + .add_optional_vendor_data( + OPTIONAL_DEFAULT_INT_TAG, + QRCodeInfoType::UInt32(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) + } } From 0569cfc771f73392013b438a4a89cc53f70e259e Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 13 Jan 2023 11:40:16 +0100 Subject: [PATCH 3/8] More idiomatic logic --- matter/src/codec/base38.rs | 5 +++-- matter/src/pairing/qr.rs | 10 ++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/matter/src/codec/base38.rs b/matter/src/codec/base38.rs index 8beec25..7536ca7 100644 --- a/matter/src/codec/base38.rs +++ b/matter/src/codec/base38.rs @@ -55,10 +55,11 @@ pub fn encode(bytes: &[u8], length: usize) -> String { fn encode_base38(mut value: u32, char_count: u8) -> String { let mut result = String::new(); + let chars = BASE38_CHARS.chars(); for _ in 0..char_count { - let mut chars = BASE38_CHARS.chars(); + let mut use_chars = chars.clone(); let remainder = value % 38; - result.push(chars.nth(remainder as usize).unwrap()); + result.push(use_chars.nth(remainder as usize).unwrap()); value = (value - remainder) / 38; } result diff --git a/matter/src/pairing/qr.rs b/matter/src/pairing/qr.rs index 3107e63..bc86e90 100644 --- a/matter/src/pairing/qr.rs +++ b/matter/src/pairing/qr.rs @@ -492,14 +492,8 @@ fn populate_tlv_bits( ) -> Result<(), Error> { if let (Some(data), Some(data_length_in_bytes)) = (tlv_data.data, tlv_data.data_length_in_bytes) { - for pos in 0..data_length_in_bytes { - populate_bits( - bits, - offset, - data[pos] as u64, - 8, - total_payload_size_in_bits, - )?; + 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); From 563e0cbd9b57ed4f204513bbc6d6ee12c4c78bfa Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 13 Jan 2023 12:12:57 +0100 Subject: [PATCH 4/8] Ditch the chars() iterator --- matter/src/codec/base38.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/matter/src/codec/base38.rs b/matter/src/codec/base38.rs index 7536ca7..975c5c9 100644 --- a/matter/src/codec/base38.rs +++ b/matter/src/codec/base38.rs @@ -17,7 +17,10 @@ //! Base38 encoding functions. -const BASE38_CHARS: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-."; +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', '-', '.', +]; /// Encodes a byte array into a base38 string. pub fn encode(bytes: &[u8], length: usize) -> String { @@ -55,11 +58,9 @@ pub fn encode(bytes: &[u8], length: usize) -> String { fn encode_base38(mut value: u32, char_count: u8) -> String { let mut result = String::new(); - let chars = BASE38_CHARS.chars(); for _ in 0..char_count { - let mut use_chars = chars.clone(); let remainder = value % 38; - result.push(use_chars.nth(remainder as usize).unwrap()); + result.push(BASE38_CHARS[remainder as usize]); value = (value - remainder) / 38; } result From 8605cf6e4167ee6a4610b14f63fa6025ac694303 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 13 Jan 2023 12:56:56 +0100 Subject: [PATCH 5/8] Base38 decoding --- matter/src/codec/base38.rs | 138 +++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 6 deletions(-) diff --git a/matter/src/codec/base38.rs b/matter/src/codec/base38.rs index 975c5c9..ddaf199 100644 --- a/matter/src/codec/base38.rs +++ b/matter/src/codec/base38.rs @@ -15,14 +15,72 @@ * limitations under the License. */ -//! Base38 encoding functions. +//! 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', '-', '.', ]; -/// Encodes a byte array into a base38 string. +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. pub fn encode(bytes: &[u8], length: usize) -> String { let mut offset = 0; let mut result = String::new(); @@ -66,16 +124,84 @@ fn encode_base38(mut value: u32, char_count: u8) -> String { result } +/// Decode a base38-encoded string into a byte slice +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() { - const ENCODED: &str = "-MOA57ZU02IT2L2BJ00"; - const DECODED: [u8; 11] = [ - 0x88, 0xff, 0xa7, 0x91, 0x50, 0x40, 0x00, 0x47, 0x51, 0xdd, 0x02, - ]; assert_eq!(encode(&DECODED, 11), ENCODED); } + + #[test] + fn can_base38_decode() { + assert_eq!(decode(ENCODED).expect("can not decode base38"), DECODED); + } } From 8071a7b931c7ee50b0b12a76df361cf3071b0c75 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 13 Jan 2023 13:05:22 +0100 Subject: [PATCH 6/8] Optional length when encoding --- matter/src/codec/base38.rs | 22 ++++++++++++++++++++-- matter/src/pairing/qr.rs | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/matter/src/codec/base38.rs b/matter/src/codec/base38.rs index ddaf199..d4c69c1 100644 --- a/matter/src/codec/base38.rs +++ b/matter/src/codec/base38.rs @@ -81,10 +81,19 @@ 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. -pub fn encode(bytes: &[u8], length: usize) -> 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) { @@ -125,6 +134,11 @@ fn encode_base38(mut value: u32, char_count: u8) -> String { } /// 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(); @@ -197,7 +211,11 @@ mod tests { #[test] fn can_base38_encode() { - assert_eq!(encode(&DECODED, 11), ENCODED); + 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] diff --git a/matter/src/pairing/qr.rs b/matter/src/pairing/qr.rs index bc86e90..01c8fd0 100644 --- a/matter/src/pairing/qr.rs +++ b/matter/src/pairing/qr.rs @@ -354,7 +354,7 @@ fn payload_base38_representation_with_tlv( } let bytes_written = generate_bit_set(payload, bits, tlv_data)?; - let base38_encoded = base38::encode(&*bits, bytes_written); + let base38_encoded = base38::encode(&*bits, Some(bytes_written)); Ok(format!("MT:{}", base38_encoded)) } From c8cc5b7ea1531c5e913471d8edbcac4880703dc1 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 13 Jan 2023 17:38:39 +0100 Subject: [PATCH 7/8] Fix buffer and write strings as utf8 --- matter/src/pairing/qr.rs | 83 +++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/matter/src/pairing/qr.rs b/matter/src/pairing/qr.rs index 01c8fd0..dce8229 100644 --- a/matter/src/pairing/qr.rs +++ b/matter/src/pairing/qr.rs @@ -263,8 +263,8 @@ pub(super) fn payload_base38_representation(payload: &QrSetupPayload) -> Result< } fn estimate_buffer_size(payload: &QrSetupPayload) -> Result { - // Estimate the size of the needed buffer. - let mut estimate = 0; + // 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. @@ -301,7 +301,9 @@ 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. - first_field_size + 4 + // + // 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) { @@ -371,14 +373,9 @@ fn generate_tlv_from_optional_data( let data = payload.get_all_optional_data(); for (tag, value) in data { + println!("tag: {tag:?}"); match &value.data { - QRCodeInfoType::String(data) => { - if data.len() > 256 { - tw.str16(TagType::Context(*tag), data.as_bytes())?; - } else { - tw.str8(TagType::Context(*tag), data.as_bytes())?; - } - } + QRCodeInfoType::String(data) => tw.utf8(TagType::Context(*tag), data.as_bytes())?, // todo: check i32 -> u32?? QRCodeInfoType::Int32(data) => tw.u32(TagType::Context(*tag), *data as u32)?, // todo: check i64 -> u64?? @@ -409,6 +406,7 @@ fn generate_bit_set( }; if bits.len() * 8 < total_payload_size_in_bits { + println!("{:?} vs {total_payload_size_in_bits}", bits.len() * 8); return Err(Error::BufferTooSmall); }; @@ -538,30 +536,54 @@ mod tests { } #[test] - fn can_base38_encode_with_optional_data() { - // todo: this must be validated! - const QR_CODE: &str = "MT:YNJV7VSC00CMVH70V3P0-ISA0DK5N1K8SQ1RYCU1WET70.QT52B.E232XZE0O0"; - 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: u32 = 12; + fn can_base38_encode_with_vendor_data() { + const QR_CODE: &str = "MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1-A40"; let comm_data = CommissioningData { - passwd: 34567890, - discriminator: 2976, + passwd: 20202021, + discriminator: 3840, ..Default::default() }; let dev_det = BasicInfoConfig { - vid: 9050, - pid: 65279, + vid: 65521, + pid: 32769, ..Default::default() }; - let disc_cap = DiscoveryCapabilities::new(false, true, false); + 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_serial_number(SerialNumber::String("123456789".to_string())) + .add_serial_number(SerialNumber::String("1234567890".to_string())) + .expect("Failed to add serial number"); + + 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.O39C40"; + 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: u32 = 12; + + let comm_data = CommissioningData { + passwd: 20202021, + discriminator: 3840, + ..Default::default() + }; + let dev_det = BasicInfoConfig { + vid: 65521, + pid: 32769, + ..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_serial_number(SerialNumber::String("1234567890".to_string())) .expect("Failed to add serial number"); qr_code_data @@ -571,12 +593,13 @@ mod tests { ) .expect("Failed to add optional data"); - qr_code_data - .add_optional_vendor_data( - OPTIONAL_DEFAULT_INT_TAG, - QRCodeInfoType::UInt32(OPTIONAL_DEFAULT_INT_VALUE), - ) - .expect("Failed to add optional data"); + // todo: check why u32 is not accepted by 'chip-tool payload parse-setup-payload' + // qr_code_data + // .add_optional_vendor_data( + // OPTIONAL_DEFAULT_INT_TAG, + // QRCodeInfoType::UInt32(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) From 23b4473eba757849499db9674b6c6a392e41e4dc Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 14 Jan 2023 11:01:08 +0100 Subject: [PATCH 8/8] Support writing of signed int --- matter/src/pairing/qr.rs | 30 ++++++++++++++---------------- matter/src/tlv/writer.rs | 33 +++++++++++++++++++++++++++++++++ matter/src/utils/writebuf.rs | 17 +++++++++++++++++ 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/matter/src/pairing/qr.rs b/matter/src/pairing/qr.rs index dce8229..7dbf47b 100644 --- a/matter/src/pairing/qr.rs +++ b/matter/src/pairing/qr.rs @@ -373,13 +373,10 @@ fn generate_tlv_from_optional_data( let data = payload.get_all_optional_data(); for (tag, value) in data { - println!("tag: {tag:?}"); match &value.data { QRCodeInfoType::String(data) => tw.utf8(TagType::Context(*tag), data.as_bytes())?, - // todo: check i32 -> u32?? - QRCodeInfoType::Int32(data) => tw.u32(TagType::Context(*tag), *data as u32)?, - // todo: check i64 -> u64?? - QRCodeInfoType::Int64(data) => tw.u64(TagType::Context(*tag), *data as u64)?, + 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)?, } @@ -406,7 +403,6 @@ fn generate_bit_set( }; if bits.len() * 8 < total_payload_size_in_bits { - println!("{:?} vs {total_payload_size_in_bits}", bits.len() * 8); return Err(Error::BufferTooSmall); }; @@ -562,12 +558,13 @@ mod tests { #[test] fn can_base38_encode_with_optional_data() { - const QR_CODE: &str = "MT:-24J0AFN00KA064IJ3P0IXZB0DK5N1K8SQ1RYCU1UXH34YY0V3KY.O39C40"; + 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: u32 = 12; + const OPTIONAL_DEFAULT_INT_TAG: u8 = 0x83; // Vendor "test" tag + const OPTIONAL_DEFAULT_INT_VALUE: i32 = 65550; let comm_data = CommissioningData { passwd: 20202021, @@ -593,13 +590,14 @@ mod tests { ) .expect("Failed to add optional data"); - // todo: check why u32 is not accepted by 'chip-tool payload parse-setup-payload' - // qr_code_data - // .add_optional_vendor_data( - // OPTIONAL_DEFAULT_INT_TAG, - // QRCodeInfoType::UInt32(OPTIONAL_DEFAULT_INT_VALUE), - // ) - // .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/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);