FactoryDataProvider.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. #
  2. # Copyright (c) 2022 Project CHIP Authors
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. import argparse
  17. import datetime
  18. import os
  19. import subprocess
  20. class FactoryDataWriter:
  21. script_dir = os.path.dirname(__file__)
  22. # CONSTANTS
  23. TEMP_FILE = script_dir + "/tmp_nvm3.s37"
  24. OUT_FILE = script_dir + "/matter_factorydata.s37" # Final output file containing the nvm3 data to flash to the device
  25. BASE_MG12_FILE = script_dir + "/base_matter_mg12_nvm3.s37"
  26. BASE_MG24_FILE = script_dir + "/base_matter_mg24_nvm3.s37"
  27. # nvm3 keys to set
  28. SERIAL_NUMBER_NVM3_KEY = "0x87200:"
  29. MANUFACTURING_DATE_NVM3_KEY = "0x87204:"
  30. SETUP_PAYLOAD_NVM3_KEY = "0x87205:"
  31. DISCRIMINATOR_NVM3_KEY = "0x87207:"
  32. ITERATIONCOUNT_NVM3_KEY = "0x87208:"
  33. SALT_NVM3_KEY = "0x87209:"
  34. VERIFIER_NVM3_KEY = "0x8720A:"
  35. PRODUCT_ID_NVM3_KEY = "0x8720B:"
  36. VENDOR_ID_NVM3_KEY = "0x8720C:"
  37. VENDOR_NAME_NVM3_KEY = "0x8720D:"
  38. PRODUCT_NAME_NVM3_KEY = "0x8720E:"
  39. HW_VER_STR_NVM3_KEY = "0x8720F:"
  40. UNIQUE_ID_NVM3_KEY = "0x8721F:"
  41. HW_VER_NVM3_KEY = "0x87308:"
  42. PRODUCT_LABEL_NVM3_KEY = "0x87210:"
  43. PRODUCT_URL_NVM3_KEY = "0x87211:"
  44. PART_NUMBER_NVM3_KEY = "0x87212:"
  45. def generate_spake2p_verifier(self):
  46. """ Generate Spake2+ verifier using the external spake2p tool
  47. Args:
  48. The whole set of args passed to the script. The required one are:
  49. gen_spake2p_path: path to spake2p executable
  50. spake2_iteration: Iteration counter for Spake2+ verifier generation
  51. passcode: Pairing passcode using in Spake2+
  52. spake2_salt: Salt used to generate Spake2+ verifier
  53. Returns:
  54. The generated verifier string
  55. """
  56. cmd = [
  57. self._args.gen_spake2p_path, 'gen-verifier',
  58. '--iteration-count', str(self._args.spake2_iteration),
  59. '--salt', self._args.spake2_salt,
  60. '--pin-code', str(self._args.passcode),
  61. '--out', '-',
  62. ]
  63. output = subprocess.check_output(cmd)
  64. output = output.decode('utf-8').splitlines()
  65. generation_results = dict(zip(output[0].split(','), output[1].split(',')))
  66. return generation_results["Verifier"]
  67. # Populates numberOfBits starting from LSB of input into bits, which is assumed to be zero-initialized
  68. def WriteBits(self, bits, offset, input, numberOfBits, totalPayloadSizeInBits):
  69. if ((offset + numberOfBits) > totalPayloadSizeInBits):
  70. print("THIS IS NOT VALID")
  71. return
  72. # input < 1u << numberOfBits);
  73. index = offset
  74. offset += numberOfBits
  75. while (input != 0):
  76. if (input & 1):
  77. bits[int(index / 8)] |= (1 << (index % 8))
  78. index += 1
  79. input >>= 1
  80. return offset
  81. def generateQrCodeBitSet(self):
  82. kVersionFieldLengthInBits = 3
  83. kVendorIDFieldLengthInBits = 16
  84. kProductIDFieldLengthInBits = 16
  85. kCommissioningFlowFieldLengthInBits = 2
  86. kRendezvousInfoFieldLengthInBits = 8
  87. kPayloadDiscriminatorFieldLengthInBits = 12
  88. kSetupPINCodeFieldLengthInBits = 27
  89. kPaddingFieldLengthInBits = 4
  90. kTotalPayloadDataSizeInBits = (kVersionFieldLengthInBits + kVendorIDFieldLengthInBits + kProductIDFieldLengthInBits +
  91. kCommissioningFlowFieldLengthInBits + kRendezvousInfoFieldLengthInBits + kPayloadDiscriminatorFieldLengthInBits +
  92. kSetupPINCodeFieldLengthInBits + kPaddingFieldLengthInBits)
  93. offset = 0
  94. fillBits = [0] * int(kTotalPayloadDataSizeInBits / 8)
  95. offset = self.WriteBits(fillBits, offset, 0, kVersionFieldLengthInBits, kTotalPayloadDataSizeInBits)
  96. offset = self.WriteBits(fillBits, offset, self._args.vendor_id, kVendorIDFieldLengthInBits, kTotalPayloadDataSizeInBits)
  97. offset = self.WriteBits(fillBits, offset, self._args.product_id, kProductIDFieldLengthInBits, kTotalPayloadDataSizeInBits)
  98. offset = self.WriteBits(fillBits, offset, self._args.commissioning_flow,
  99. kCommissioningFlowFieldLengthInBits, kTotalPayloadDataSizeInBits)
  100. offset = self.WriteBits(fillBits, offset, self._args.rendezvous_flag,
  101. kRendezvousInfoFieldLengthInBits, kTotalPayloadDataSizeInBits)
  102. offset = self.WriteBits(fillBits, offset, self._args.discriminator,
  103. kPayloadDiscriminatorFieldLengthInBits, kTotalPayloadDataSizeInBits)
  104. offset = self.WriteBits(fillBits, offset, self._args.passcode, kSetupPINCodeFieldLengthInBits, kTotalPayloadDataSizeInBits)
  105. offset = self.WriteBits(fillBits, offset, 0, kPaddingFieldLengthInBits, kTotalPayloadDataSizeInBits)
  106. return str(bytes(fillBits).hex())
  107. def __init__(self, arguments) -> None:
  108. """ Do some checks on the received arguments.
  109. Generate the Spake2+ verifier if needed and assign the values
  110. to the global variables
  111. Args:
  112. The whole set of args passed to the script.
  113. """
  114. kMaxVendorNameLength = 32
  115. kMaxProductNameLength = 32
  116. kMaxHardwareVersionStringLength = 64
  117. kMaxSerialNumberLength = 32
  118. kUniqueIDLength = 16
  119. kMaxProductUrlLenght = 256
  120. kMaxPartNumberLength = 32
  121. kMaxProductLabelLength = 64
  122. INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444,
  123. 55555555, 66666666, 77777777, 88888888, 99999999, 12345678, 87654321]
  124. assert (bool(arguments.gen_spake2p_path) != bool(arguments.spake2_verifier)
  125. ), "Provide either the spake2_verifier string or the path to the spake2 generator"
  126. assert not (arguments.passcode in INVALID_PASSCODES), "The provided passcode is invalid"
  127. self._args = arguments
  128. if self._args.unique_id:
  129. assert (len(bytearray.fromhex(self._args.unique_id)) == kUniqueIDLength), "Provide a 16 bytes unique id"
  130. if self._args.product_name:
  131. assert (len(self._args.product_name) <= kMaxProductNameLength), "Product name exceeds the size limit"
  132. if self._args.vendor_name:
  133. assert (len(self._args.vendor_name) <= kMaxVendorNameLength), "Vendor name exceeds the size limit"
  134. if self._args.hw_version_str:
  135. assert (len(self._args.hw_version_str) <= kMaxHardwareVersionStringLength), "Hardware version string exceeds the size limit"
  136. if self._args.serial_number:
  137. assert (len(self._args.serial_number) <= kMaxSerialNumberLength), "Serial number exceeds the size limit"
  138. if self._args.manufacturing_date:
  139. try:
  140. datetime.datetime.strptime(self._args.manufacturing_date, '%Y-%m-%d')
  141. except ValueError:
  142. raise ValueError("Incorrect manufacturing data format, should be YYYY-MM-DD")
  143. if self._args.commissioning_flow:
  144. assert (self._args.commissioning_flow <= 3), "Invalid commissioning flow value"
  145. if self._args.rendezvous_flag:
  146. assert (self._args.rendezvous_flag <= 7), "Invalid rendez-vous flag value"
  147. if self._args.gen_spake2p_path:
  148. self._args.spake2_verifier = self.generate_spake2p_verifier()
  149. if self._args.product_label:
  150. assert (len(self._args.product_label) <= kMaxProductLabelLength), "Product Label exceeds the size limit"
  151. if self._args.product_url:
  152. assert (len(self._args.product_url) <= kMaxProductUrlLenght), "Product URL exceeds the size limit"
  153. if self._args.part_number:
  154. assert (len(self._args.part_number) <= kMaxPartNumberLength), "Part number exceeds the size limit"
  155. def add_SerialNo_To_CMD(self, cmdList):
  156. """ Add the jtag serial command to the commander command
  157. Args:
  158. The commander command in list format
  159. """
  160. if self._args.jtag_serial:
  161. cmdList.extend(["--serialno", self._args.jtagSerial])
  162. def create_nvm3injected_image(self):
  163. """ Use commander command lines create a binary flashable to the EFR32
  164. containing the factory commissioning data in NVM3 section
  165. """
  166. isDeviceConnected = True
  167. # Retrieve the device current nvm3 data in a binary file
  168. # It will be used as base to add the new credentials
  169. inputImage = self.TEMP_FILE
  170. cmd = ['commander', 'nvm3', 'read', '-o', inputImage, ]
  171. self.add_SerialNo_To_CMD(cmd)
  172. results = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
  173. if results.returncode != 0:
  174. # No nvm3 section found. Retrieve the device info
  175. cmd = ['commander', 'device', 'info', ]
  176. self.add_SerialNo_To_CMD(cmd)
  177. try:
  178. output = subprocess.check_output(cmd)
  179. output = output.decode('utf-8').splitlines()
  180. deviceInfo = dict(map(str.strip, lines.split(':')) for lines in output[0:len(output)-1])
  181. # Only MG12 and MG24 are supported in matter currently
  182. if "EFR32MG12" in deviceInfo["Part Number"]:
  183. inputImage = self.BASE_MG12_FILE
  184. elif "EFR32MG24" in deviceInfo["Part Number"]:
  185. inputImage = self.BASE_MG24_FILE
  186. else:
  187. raise Exception('Invalid MCU')
  188. except Exception:
  189. isDeviceConnected = False
  190. print("Device not connected")
  191. # When no device is connected user needs to provide the mcu family for which those credentials are to be created
  192. if self._args.mcu_family:
  193. if "EFR32MG12" == self._args.mcu_family:
  194. inputImage = self.BASE_MG12_FILE
  195. elif "EFR32MG24" == self._args.mcu_family:
  196. inputImage = self.BASE_MG24_FILE
  197. else:
  198. print("Connect debug port or provide the mcu_family")
  199. return
  200. # Convert interger to little endian hex format and strings to hex byte array format for nvm3 storage
  201. spake2pIterationCount = self._args.spake2_iteration.to_bytes(4, 'little').hex()
  202. discriminator = self._args.discriminator.to_bytes(2, 'little').hex()
  203. saltByteArray = bytes(self._args.spake2_salt, 'utf-8').hex()
  204. verifierByteArray = bytes(self._args.spake2_verifier, 'utf-8').hex()
  205. productId = self._args.product_id.to_bytes(2, "little").hex()
  206. vendorId = self._args.vendor_id.to_bytes(2, "little").hex()
  207. # create the binary containing the new nvm3 data
  208. cmd = [
  209. "commander", "nvm3", "set", inputImage,
  210. "--object", self.DISCRIMINATOR_NVM3_KEY + str(discriminator),
  211. "--object", self.SETUP_PAYLOAD_NVM3_KEY + self.generateQrCodeBitSet(),
  212. "--object", self.ITERATIONCOUNT_NVM3_KEY + str(spake2pIterationCount),
  213. "--object", self.SALT_NVM3_KEY + str(saltByteArray),
  214. "--object", self.VERIFIER_NVM3_KEY + str(verifierByteArray),
  215. "--object", self.PRODUCT_ID_NVM3_KEY + str(productId),
  216. "--object", self.VENDOR_ID_NVM3_KEY + str(vendorId),
  217. ]
  218. if self._args.product_name:
  219. productNameByteArray = bytes(self._args.product_name, 'utf-8').hex()
  220. cmd.extend(["--object", self.PRODUCT_NAME_NVM3_KEY + str(productNameByteArray)])
  221. if self._args.vendor_name:
  222. vendorNameByteArray = bytes(self._args.vendor_name, 'utf-8').hex()
  223. cmd.extend(["--object", self.VENDOR_NAME_NVM3_KEY + str(vendorNameByteArray)])
  224. if self._args.hw_version:
  225. hwVersionByteArray = self._args.hw_version.to_bytes(2, "little").hex()
  226. cmd.extend(["--object", self.HW_VER_NVM3_KEY + str(hwVersionByteArray)])
  227. if self._args.hw_version_str:
  228. hwVersionByteArray = bytes(self._args.hw_version_str, 'utf-8').hex()
  229. cmd.extend(["--object", self.HW_VER_STR_NVM3_KEY + str(hwVersionByteArray)])
  230. if self._args.unique_id:
  231. cmd.extend(["--object", self.UNIQUE_ID_NVM3_KEY + self._args.unique_id])
  232. if self._args.manufacturing_date:
  233. dateByteArray = bytes(self._args.manufacturing_date, 'utf-8').hex()
  234. cmd.extend(["--object", self.MANUFACTURING_DATE_NVM3_KEY + str(dateByteArray)])
  235. if self._args.serial_number:
  236. serialNumberByteArray = bytes(self._args.serial_number, 'utf-8').hex()
  237. cmd.extend(["--object", self.SERIAL_NUMBER_NVM3_KEY + str(serialNumberByteArray)])
  238. if self._args.part_number:
  239. partNumberByteArray = bytes(self._args.part_number, 'utf-8').hex()
  240. cmd.extend(["--object", self.PART_NUMBER_NVM3_KEY + str(partNumberByteArray)])
  241. if self._args.product_label:
  242. productLabelByteArray = bytes(self._args.product_label, 'utf-8').hex()
  243. cmd.extend(["--object", self.PRODUCT_LABEL_NVM3_KEY + str(productLabelByteArray)])
  244. if self._args.product_url:
  245. productUrlByteArray = bytes(self._args.product_url, 'utf-8').hex()
  246. cmd.extend(["--object", self.PRODUCT_URL_NVM3_KEY + str(productUrlByteArray)])
  247. cmd.extend(["--outfile", self.OUT_FILE])
  248. results = subprocess.run(cmd)
  249. # A tempfile was create/used, delete it.
  250. if inputImage == self.TEMP_FILE:
  251. cmd = ['rm', '-rf', 'tmp_nvm3.s37', ]
  252. subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
  253. results.check_returncode
  254. # Flash the binary if the device is connected
  255. if isDeviceConnected:
  256. cmd = ['commander', 'flash', self.OUT_FILE, ]
  257. self.add_SerialNo_To_CMD(cmd)
  258. results = subprocess.run(cmd)
  259. def main():
  260. def all_int_format(i): return int(i, 0)
  261. parser = argparse.ArgumentParser(description='EFR32 NVM3 Factory data provider')
  262. parser.add_argument("--discriminator", type=all_int_format, required=True,
  263. help="[int| hex] Provide BLE pairing discriminator.")
  264. parser.add_argument("--passcode", type=all_int_format, required=True,
  265. help="[int | hex] Provide the default PASE session passcode.")
  266. parser.add_argument("--spake2_iteration", type=all_int_format, required=True,
  267. help="[int | hex int] Provide Spake2+ iteration count.")
  268. parser.add_argument("--spake2_salt", type=str, required=True,
  269. help="[string] Provide Spake2+ salt.")
  270. parser.add_argument("--spake2_verifier", type=str,
  271. help="[string] Provide Spake2+ verifier without generating it.")
  272. parser.add_argument("--gen_spake2p_path", type=str,
  273. help="[string] Provide a path to spake2p generator. It can be built from connectedhomeip/src/tools/spake2p")
  274. parser.add_argument("--mcu_family", type=str,
  275. help="[string] mcu Family target. Only need if your board isn't plugged in")
  276. parser.add_argument("--jtag_serial", type=str,
  277. help="[string] Provide the serial number of the jtag if you have more than one board connected")
  278. parser.add_argument("--product_id", type=all_int_format, default=32773,
  279. help="[int | hex int] Provide the product ID")
  280. parser.add_argument("--vendor_id", type=all_int_format, default=65521,
  281. help="[int | hex int] Provide the vendor ID")
  282. parser.add_argument("--product_name", type=str,
  283. help="[string] Provide the product name [optional]")
  284. parser.add_argument("--vendor_name", type=str,
  285. help="[string] Provide the vendor name [optional]")
  286. parser.add_argument("--hw_version", type=all_int_format,
  287. help="[int | hex int] Provide the hardware version value[optional]")
  288. parser.add_argument("--hw_version_str", type=str,
  289. help="[string] Provide the hardware version string[optional]")
  290. parser.add_argument("--product_label", type=str,
  291. help="[string] Provide the product label [optional]")
  292. parser.add_argument("--product_url", type=str,
  293. help="[string] Provide the product url [optional]")
  294. parser.add_argument("--unique_id", type=str,
  295. help="[hex_string] A 128 bits hex string unique id (without 0x) [optional]")
  296. parser.add_argument("--serial_number", type=str,
  297. help="[string] Provide serial number of the device")
  298. parser.add_argument("--manufacturing_date", type=str,
  299. help="[string] Provide Manufacturing date in YYYY-MM-DD format [optional]")
  300. parser.add_argument("--part_number", type=str,
  301. help="[string] Provide part number [optional]")
  302. parser.add_argument("--commissioning_flow", type=all_int_format, default=0,
  303. help="[int| hex] Provide Commissioning Flow: 0=Standard, 1=kUserActionRequired, 2=Custom (Default:Standard)")
  304. parser.add_argument("--rendezvous_flag", type=all_int_format, default=2,
  305. help="[int| hex] Provide Rendez-vous flag: 1=SoftAP, 2=BLE 4=OnNetwork (Default=BLE Only)")
  306. args = parser.parse_args()
  307. writer = FactoryDataWriter(args)
  308. writer.create_nvm3injected_image()
  309. if __name__ == "__main__":
  310. main()