| # |
| # Copyright (c) 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. |
| # |
| |
| import argparse |
| import base64 |
| import datetime |
| import os |
| import struct |
| import subprocess |
| import sys |
| |
| |
| class FactoryDataWriter: |
| script_dir = os.path.dirname(__file__) |
| # CONSTANTS |
| TEMP_FILE = script_dir + "/tmp_nvm3.s37" |
| OUT_FILE = script_dir + "/matter_factorydata.s37" # Final output file containing the nvm3 data to flash to the device |
| BASE_MG12_FILE = script_dir + "/base_matter_mg12_nvm3.s37" |
| BASE_MG24_FILE = script_dir + "/base_matter_mg24_nvm3.s37" |
| # nvm3 keys to set |
| SERIAL_NUMBER_NVM3_KEY = "0x87200:" |
| MANUFACTURING_DATE_NVM3_KEY = "0x87204:" |
| SETUP_PAYLOAD_NVM3_KEY = "0x87205:" |
| DISCRIMINATOR_NVM3_KEY = "0x87207:" |
| ITERATIONCOUNT_NVM3_KEY = "0x87208:" |
| SALT_NVM3_KEY = "0x87209:" |
| VERIFIER_NVM3_KEY = "0x8720A:" |
| PRODUCT_ID_NVM3_KEY = "0x8720B:" |
| VENDOR_ID_NVM3_KEY = "0x8720C:" |
| VENDOR_NAME_NVM3_KEY = "0x8720D:" |
| PRODUCT_NAME_NVM3_KEY = "0x8720E:" |
| HW_VER_STR_NVM3_KEY = "0x8720F:" |
| UNIQUE_ID_NVM3_KEY = "0x8721F:" |
| HW_VER_NVM3_KEY = "0x87308:" |
| PRODUCT_LABEL_NVM3_KEY = "0x87210:" |
| PRODUCT_URL_NVM3_KEY = "0x87211:" |
| PART_NUMBER_NVM3_KEY = "0x87212:" |
| |
| def generate_spake2p_verifier(self): |
| """ Generate Spake2+ verifier using the external spake2p tool |
| |
| Args: |
| The whole set of args passed to the script. The required one are: |
| gen_spake2p_path: path to spake2p executable |
| spake2_iteration: Iteration counter for Spake2+ verifier generation |
| passcode: Pairing passcode using in Spake2+ |
| spake2_salt: Salt used to generate Spake2+ verifier |
| |
| Returns: |
| The generated verifier string |
| """ |
| cmd = [ |
| self._args.gen_spake2p_path, 'gen-verifier', |
| '--iteration-count', str(self._args.spake2_iteration), |
| '--salt', self._args.spake2_salt, |
| '--pin-code', str(self._args.passcode), |
| '--out', '-', |
| ] |
| output = subprocess.check_output(cmd) |
| output = output.decode('utf-8').splitlines() |
| generation_results = dict(zip(output[0].split(','), output[1].split(','))) |
| return generation_results["Verifier"] |
| |
| # Populates numberOfBits starting from LSB of input into bits, which is assumed to be zero-initialized |
| def WriteBits(self, bits, offset, input, numberOfBits, totalPayloadSizeInBits): |
| if ((offset + numberOfBits) > totalPayloadSizeInBits): |
| print("THIS IS NOT VALID") |
| return |
| # input < 1u << numberOfBits); |
| |
| index = offset |
| offset += numberOfBits |
| while (input != 0): |
| if (input & 1): |
| bits[int(index / 8)] |= (1 << (index % 8)) |
| index += 1 |
| input >>= 1 |
| |
| return offset |
| |
| def generateQrCodeBitSet(self): |
| kVersionFieldLengthInBits = 3 |
| kVendorIDFieldLengthInBits = 16 |
| kProductIDFieldLengthInBits = 16 |
| kCommissioningFlowFieldLengthInBits = 2 |
| kRendezvousInfoFieldLengthInBits = 8 |
| kPayloadDiscriminatorFieldLengthInBits = 12 |
| kSetupPINCodeFieldLengthInBits = 27 |
| kPaddingFieldLengthInBits = 4 |
| |
| kTotalPayloadDataSizeInBits = (kVersionFieldLengthInBits + kVendorIDFieldLengthInBits + kProductIDFieldLengthInBits + |
| kCommissioningFlowFieldLengthInBits + kRendezvousInfoFieldLengthInBits + kPayloadDiscriminatorFieldLengthInBits + |
| kSetupPINCodeFieldLengthInBits + kPaddingFieldLengthInBits) |
| |
| offset = 0 |
| fillBits = [0] * int(kTotalPayloadDataSizeInBits / 8) |
| offset = self.WriteBits(fillBits, offset, 0, kVersionFieldLengthInBits, kTotalPayloadDataSizeInBits) |
| offset = self.WriteBits(fillBits, offset, self._args.vendor_id, kVendorIDFieldLengthInBits, kTotalPayloadDataSizeInBits) |
| offset = self.WriteBits(fillBits, offset, self._args.product_id, kProductIDFieldLengthInBits, kTotalPayloadDataSizeInBits) |
| offset = self.WriteBits(fillBits, offset, self._args.commissioning_flow, |
| kCommissioningFlowFieldLengthInBits, kTotalPayloadDataSizeInBits) |
| offset = self.WriteBits(fillBits, offset, self._args.rendezvous_flag, |
| kRendezvousInfoFieldLengthInBits, kTotalPayloadDataSizeInBits) |
| offset = self.WriteBits(fillBits, offset, self._args.discriminator, |
| kPayloadDiscriminatorFieldLengthInBits, kTotalPayloadDataSizeInBits) |
| offset = self.WriteBits(fillBits, offset, self._args.passcode, kSetupPINCodeFieldLengthInBits, kTotalPayloadDataSizeInBits) |
| offset = self.WriteBits(fillBits, offset, 0, kPaddingFieldLengthInBits, kTotalPayloadDataSizeInBits) |
| |
| return str(bytes(fillBits).hex()) |
| |
| def __init__(self, arguments) -> None: |
| """ Do some checks on the received arguments. |
| Generate the Spake2+ verifier if needed and assign the values |
| to the global variables |
| |
| Args: |
| The whole set of args passed to the script. |
| """ |
| kMaxVendorNameLength = 32 |
| kMaxProductNameLength = 32 |
| kMaxHardwareVersionStringLength = 64 |
| kMaxSerialNumberLength = 32 |
| kUniqueIDLength = 16 |
| kMaxProductUrlLenght = 256 |
| kMaxPartNumberLength = 32 |
| kMaxProductLabelLength = 64 |
| |
| INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444, |
| 55555555, 66666666, 77777777, 88888888, 99999999, 12345678, 87654321] |
| |
| assert (bool(arguments.gen_spake2p_path) != bool(arguments.spake2_verifier) |
| ), "Provide either the spake2_verifier string or the path to the spake2 generator" |
| assert not (arguments.passcode in INVALID_PASSCODES), "The provided passcode is invalid" |
| |
| self._args = arguments |
| |
| if self._args.unique_id: |
| assert (len(bytearray.fromhex(self._args.unique_id)) == kUniqueIDLength), "Provide a 16 bytes unique id" |
| if self._args.product_name: |
| assert (len(self._args.product_name) <= kMaxProductNameLength), "Product name exceeds the size limit" |
| if self._args.vendor_name: |
| assert (len(self._args.vendor_name) <= kMaxVendorNameLength), "Vendor name exceeds the size limit" |
| if self._args.hw_version_str: |
| assert (len(self._args.hw_version_str) <= kMaxHardwareVersionStringLength), "Hardware version string exceeds the size limit" |
| if self._args.serial_number: |
| assert (len(self._args.serial_number) <= kMaxSerialNumberLength), "Serial number exceeds the size limit" |
| if self._args.manufacturing_date: |
| try: |
| datetime.datetime.strptime(self._args.manufacturing_date, '%Y-%m-%d') |
| except ValueError: |
| raise ValueError("Incorrect manufacturing data format, should be YYYY-MM-DD") |
| if self._args.commissioning_flow: |
| assert (self._args.commissioning_flow <= 3), "Invalid commissioning flow value" |
| if self._args.rendezvous_flag: |
| assert (self._args.rendezvous_flag <= 7), "Invalid rendez-vous flag value" |
| if self._args.gen_spake2p_path: |
| self._args.spake2_verifier = self.generate_spake2p_verifier() |
| if self._args.product_label: |
| assert (len(self._args.product_label) <= kMaxProductLabelLength), "Product Label exceeds the size limit" |
| if self._args.product_url: |
| assert (len(self._args.product_url) <= kMaxProductUrlLenght), "Product URL exceeds the size limit" |
| if self._args.part_number: |
| assert (len(self._args.part_number) <= kMaxPartNumberLength), "Part number exceeds the size limit" |
| |
| def add_SerialNo_To_CMD(self, cmdList): |
| """ Add the jtag serial command to the commander command |
| |
| Args: |
| The commander command in list format |
| """ |
| if self._args.jtag_serial: |
| cmdList.extend(["--serialno", self._args.jtagSerial]) |
| |
| def create_nvm3injected_image(self): |
| """ Use commander command lines create a binary flashable to the EFR32 |
| containing the factory commissioning data in NVM3 section |
| """ |
| isDeviceConnected = True |
| |
| # Retrieve the device current nvm3 data in a binary file |
| # It will be used as base to add the new credentials |
| inputImage = self.TEMP_FILE |
| cmd = ['commander', 'nvm3', 'read', '-o', inputImage, ] |
| self.add_SerialNo_To_CMD(cmd) |
| results = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) |
| |
| if results.returncode != 0: |
| # No nvm3 section found. Retrieve the device info |
| cmd = ['commander', 'device', 'info', ] |
| self.add_SerialNo_To_CMD(cmd) |
| try: |
| output = subprocess.check_output(cmd) |
| output = output.decode('utf-8').splitlines() |
| deviceInfo = dict(map(str.strip, lines.split(':')) for lines in output[0:len(output)-1]) |
| # Only MG12 and MG24 are supported in matter currently |
| if "EFR32MG12" in deviceInfo["Part Number"]: |
| inputImage = self.BASE_MG12_FILE |
| elif "EFR32MG24" in deviceInfo["Part Number"]: |
| inputImage = self.BASE_MG24_FILE |
| else: |
| raise Exception('Invalid MCU') |
| except: |
| isDeviceConnected = False |
| print("Device not connected") |
| # When no device is connected user needs to provide the mcu family for which those credentials are to be created |
| if self._args.mcu_family: |
| if "EFR32MG12" == self._args.mcu_family: |
| inputImage = self.BASE_MG12_FILE |
| elif "EFR32MG24" == self._args.mcu_family: |
| inputImage = self.BASE_MG24_FILE |
| else: |
| print("Connect debug port or provide the mcu_family") |
| return |
| |
| # Convert interger to little endian hex format and strings to hex byte array format for nvm3 storage |
| spake2pIterationCount = self._args.spake2_iteration.to_bytes(4, 'little').hex() |
| discriminator = self._args.discriminator.to_bytes(2, 'little').hex() |
| saltByteArray = bytes(self._args.spake2_salt, 'utf-8').hex() |
| verifierByteArray = bytes(self._args.spake2_verifier, 'utf-8').hex() |
| |
| productId = self._args.product_id.to_bytes(2, "little").hex() |
| vendorId = self._args.vendor_id.to_bytes(2, "little").hex() |
| |
| # create the binary containing the new nvm3 data |
| cmd = [ |
| "commander", "nvm3", "set", inputImage, |
| "--object", self.DISCRIMINATOR_NVM3_KEY + str(discriminator), |
| "--object", self.SETUP_PAYLOAD_NVM3_KEY + self.generateQrCodeBitSet(), |
| "--object", self.ITERATIONCOUNT_NVM3_KEY + str(spake2pIterationCount), |
| "--object", self.SALT_NVM3_KEY + str(saltByteArray), |
| "--object", self.VERIFIER_NVM3_KEY + str(verifierByteArray), |
| "--object", self.PRODUCT_ID_NVM3_KEY + str(productId), |
| "--object", self.VENDOR_ID_NVM3_KEY + str(vendorId), |
| ] |
| |
| if self._args.product_name: |
| productNameByteArray = bytes(self._args.product_name, 'utf-8').hex() |
| cmd.extend(["--object", self.PRODUCT_NAME_NVM3_KEY + str(productNameByteArray)]) |
| |
| if self._args.vendor_name: |
| vendorNameByteArray = bytes(self._args.vendor_name, 'utf-8').hex() |
| cmd.extend(["--object", self.VENDOR_NAME_NVM3_KEY + str(vendorNameByteArray)]) |
| |
| if self._args.hw_version: |
| hwVersionByteArray = self._args.hw_version.to_bytes(2, "little").hex() |
| cmd.extend(["--object", self.HW_VER_NVM3_KEY + str(hwVersionByteArray)]) |
| |
| if self._args.hw_version_str: |
| hwVersionByteArray = bytes(self._args.hw_version_str, 'utf-8').hex() |
| cmd.extend(["--object", self.HW_VER_STR_NVM3_KEY + str(hwVersionByteArray)]) |
| |
| if self._args.unique_id: |
| cmd.extend(["--object", self.UNIQUE_ID_NVM3_KEY + self._args.unique_id]) |
| |
| if self._args.manufacturing_date: |
| dateByteArray = bytes(self._args.manufacturing_date, 'utf-8').hex() |
| cmd.extend(["--object", self.MANUFACTURING_DATE_NVM3_KEY + str(dateByteArray)]) |
| |
| if self._args.serial_number: |
| serialNumberByteArray = bytes(self._args.serial_number, 'utf-8').hex() |
| cmd.extend(["--object", self.SERIAL_NUMBER_NVM3_KEY + str(serialNumberByteArray)]) |
| |
| if self._args.part_number: |
| partNumberByteArray = bytes(self._args.part_number, 'utf-8').hex() |
| cmd.extend(["--object", self.PART_NUMBER_NVM3_KEY + str(partNumberByteArray)]) |
| |
| if self._args.product_label: |
| productLabelByteArray = bytes(self._args.product_label, 'utf-8').hex() |
| cmd.extend(["--object", self.PRODUCT_LABEL_NVM3_KEY + str(productLabelByteArray)]) |
| |
| if self._args.product_url: |
| productUrlByteArray = bytes(self._args.product_url, 'utf-8').hex() |
| cmd.extend(["--object", self.PRODUCT_URL_NVM3_KEY + str(productUrlByteArray)]) |
| |
| cmd.extend(["--outfile", self.OUT_FILE]) |
| results = subprocess.run(cmd) |
| |
| # A tempfile was create/used, delete it. |
| if inputImage == self.TEMP_FILE: |
| cmd = ['rm', '-rf', 'tmp_nvm3.s37', ] |
| subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) |
| |
| results.check_returncode |
| |
| # Flash the binary if the device is connected |
| if isDeviceConnected: |
| cmd = ['commander', 'flash', self.OUT_FILE, ] |
| self.add_SerialNo_To_CMD(cmd) |
| results = subprocess.run(cmd) |
| |
| |
| def main(): |
| def all_int_format(i): return int(i, 0) |
| parser = argparse.ArgumentParser(description='EFR32 NVM3 Factory data provider') |
| |
| parser.add_argument("--discriminator", type=all_int_format, required=True, |
| help="[int| hex] Provide BLE pairing discriminator.") |
| parser.add_argument("--passcode", type=all_int_format, required=True, |
| help="[int | hex] Provide the default PASE session passcode.") |
| parser.add_argument("--spake2_iteration", type=all_int_format, required=True, |
| help="[int | hex int] Provide Spake2+ iteration count.") |
| parser.add_argument("--spake2_salt", type=str, required=True, |
| help="[string] Provide Spake2+ salt.") |
| parser.add_argument("--spake2_verifier", type=str, |
| help="[string] Provide Spake2+ verifier without generating it.") |
| parser.add_argument("--gen_spake2p_path", type=str, |
| help="[string] Provide a path to spake2p generator. It can be built from connectedhomeip/src/tools/spake2p") |
| parser.add_argument("--mcu_family", type=str, |
| help="[string] mcu Family target. Only need if your board isn't plugged in") |
| parser.add_argument("--jtag_serial", type=str, |
| help="[string] Provide the serial number of the jtag if you have more than one board connected") |
| parser.add_argument("--product_id", type=all_int_format, default=32773, |
| help="[int | hex int] Provide the product ID") |
| parser.add_argument("--vendor_id", type=all_int_format, default=65521, |
| help="[int | hex int] Provide the vendor ID") |
| parser.add_argument("--product_name", type=str, |
| help="[string] Provide the product name [optional]") |
| parser.add_argument("--vendor_name", type=str, |
| help="[string] Provide the vendor name [optional]") |
| parser.add_argument("--hw_version", type=all_int_format, |
| help="[int | hex int] Provide the hardware version value[optional]") |
| parser.add_argument("--hw_version_str", type=str, |
| help="[string] Provide the hardware version string[optional]") |
| parser.add_argument("--product_label", type=str, |
| help="[string] Provide the product label [optional]") |
| parser.add_argument("--product_url", type=str, |
| help="[string] Provide the product url [optional]") |
| parser.add_argument("--unique_id", type=str, |
| help="[hex_string] A 128 bits hex string unique id (without 0x) [optional]") |
| parser.add_argument("--serial_number", type=str, |
| help="[string] Provide serial number of the device") |
| parser.add_argument("--manufacturing_date", type=str, |
| help="[string] Provide Manufacturing date in YYYY-MM-DD format [optional]") |
| parser.add_argument("--part_number", type=str, |
| help="[string] Provide part number [optional]") |
| parser.add_argument("--commissioning_flow", type=all_int_format, default=0, |
| help="[int| hex] Provide Commissioning Flow: 0=Standard, 1=kUserActionRequired, 2=Custom (Default:Standard)") |
| parser.add_argument("--rendezvous_flag", type=all_int_format, default=2, |
| help="[int| hex] Provide Rendez-vous flag: 1=SoftAP, 2=BLE 4=OnNetwork (Default=BLE Only)") |
| |
| args = parser.parse_args() |
| writer = FactoryDataWriter(args) |
| writer.create_nvm3injected_image() |
| |
| |
| if __name__ == "__main__": |
| main() |