package.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. # Copyright (c) 2015, Nordic Semiconductor
  2. # All rights reserved.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are met:
  6. #
  7. # * Redistributions of source code must retain the above copyright notice, this
  8. # list of conditions and the following disclaimer.
  9. #
  10. # * Redistributions in binary form must reproduce the above copyright notice,
  11. # this list of conditions and the following disclaimer in the documentation
  12. # and/or other materials provided with the distribution.
  13. #
  14. # * Neither the name of Nordic Semiconductor ASA nor the names of its
  15. # contributors may be used to endorse or promote products derived from
  16. # this software without specific prior written permission.
  17. #
  18. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  19. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  20. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  22. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  23. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  24. # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  25. # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  26. # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  27. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. # Python standard library
  29. import os
  30. import tempfile
  31. import shutil
  32. # 3rd party libraries
  33. from zipfile import ZipFile
  34. import hashlib
  35. # Nordic libraries
  36. from nordicsemi.exceptions import NordicSemiException
  37. from nordicsemi.dfu.nrfhex import *
  38. from nordicsemi.dfu.init_packet import *
  39. from nordicsemi.dfu.manifest import ManifestGenerator, Manifest
  40. from nordicsemi.dfu.model import HexType, FirmwareKeys
  41. from nordicsemi.dfu.crc16 import *
  42. from signing import Signing
  43. class Package(object):
  44. """
  45. Packages and unpacks Nordic DFU packages. Nordic DFU packages are zip files that contains firmware and meta-information
  46. necessary for utilities to perform a DFU on nRF5X devices.
  47. The internal data model used in Package is a dictionary. The dictionary is expressed like this in
  48. json format:
  49. {
  50. "manifest": {
  51. "bootloader": {
  52. "bin_file": "asdf.bin",
  53. "dat_file": "asdf.dat",
  54. "init_packet_data": {
  55. "application_version": null,
  56. "device_revision": null,
  57. "device_type": 5,
  58. "firmware_hash": "asdfasdkfjhasdkfjashfkjasfhaskjfhkjsdfhasjkhf",
  59. "softdevice_req": [
  60. 17,
  61. 18
  62. ]
  63. }
  64. }
  65. }
  66. Attributes application, bootloader, softdevice, softdevice_bootloader shall not be put into the manifest if they are null
  67. """
  68. DEFAULT_DEV_TYPE = 0xFFFF
  69. DEFAULT_DEV_REV = 0xFFFF
  70. DEFAULT_APP_VERSION = 0xFFFFFFFF
  71. DEFAULT_SD_REQ = [0xFFFE]
  72. DEFAULT_DFU_VER = 0.5
  73. DEFAULT_MESH_APP_ID = 0x0000
  74. DEFAULT_MESH_BOOTLOADER_ID = 0xFF00
  75. MANIFEST_FILENAME = "manifest.json"
  76. def __init__(self,
  77. dev_type=DEFAULT_DEV_TYPE,
  78. dev_rev=DEFAULT_DEV_REV,
  79. company_id = None,
  80. app_id=DEFAULT_MESH_APP_ID,
  81. app_version=DEFAULT_APP_VERSION,
  82. bootloader_id=DEFAULT_MESH_BOOTLOADER_ID,
  83. sd_req=DEFAULT_SD_REQ,
  84. app_fw=None,
  85. bootloader_fw=None,
  86. softdevice_fw=None,
  87. dfu_ver=DEFAULT_DFU_VER,
  88. key_file=None,
  89. mesh=False):
  90. """
  91. Constructor that requires values used for generating a Nordic DFU package.
  92. :param int dev_type: Device type init-packet field
  93. :param int dev_rev: Device revision init-packet field
  94. :param int company_id: Company ID for Mesh init-packet field
  95. :param int application_id: Application ID for Mesh init-packet field
  96. :param int app_version: App version init-packet field
  97. :param int bootloader_id: Bootloader ID for mesh init-packet field
  98. :param list sd_req: Softdevice Requirement init-packet field
  99. :param str app_fw: Path to application firmware file
  100. :param str bootloader_fw: Path to bootloader firmware file
  101. :param str softdevice_fw: Path to softdevice firmware file
  102. :param float dfu_ver: DFU version to use when generating init-packet
  103. :param str key_file: Path to Signing key file (PEM)
  104. :return: None
  105. """
  106. self.dfu_ver = dfu_ver
  107. self.is_mesh = mesh
  108. init_packet_vars = {}
  109. if dev_type is not None:
  110. init_packet_vars[PacketField.DEVICE_TYPE] = dev_type
  111. if dev_rev is not None:
  112. init_packet_vars[PacketField.DEVICE_REVISION] = dev_rev
  113. if app_version is not None:
  114. init_packet_vars[PacketField.APP_VERSION] = app_version
  115. if sd_req is not None:
  116. init_packet_vars[PacketField.REQUIRED_SOFTDEVICES_ARRAY] = sd_req
  117. if mesh:
  118. if company_id is not None:
  119. init_packet_vars[PacketField.NORDIC_PROPRIETARY_OPT_DATA_MESH_COMPANY_ID] = company_id
  120. if app_id is not None:
  121. init_packet_vars[PacketField.NORDIC_PROPRIETARY_OPT_DATA_MESH_APPLICATION_ID] = app_id
  122. if bootloader_id is not None:
  123. init_packet_vars[PacketField.NORDIC_PROPRIETARY_OPT_DATA_MESH_BOOTLOADER_ID] = bootloader_id
  124. self.firmwares_data = {}
  125. if app_fw:
  126. self.__add_firmware_info(HexType.APPLICATION,
  127. app_fw,
  128. init_packet_vars)
  129. if bootloader_fw:
  130. self.__add_firmware_info(HexType.BOOTLOADER,
  131. bootloader_fw,
  132. init_packet_vars)
  133. if softdevice_fw:
  134. self.__add_firmware_info(HexType.SOFTDEVICE,
  135. softdevice_fw,
  136. init_packet_vars)
  137. self.key_file = None
  138. if key_file:
  139. self.dfu_ver = 0.8
  140. self.key_file = key_file
  141. elif mesh:
  142. self.dfu_ver = 0.8
  143. def generate_package(self, filename, preserve_work_directory=False):
  144. """
  145. Generates a Nordic DFU package. The package is a zip file containing firmware(s) and metadata required
  146. for Nordic DFU applications to perform DFU onn nRF5X devices.
  147. :param str filename: Filename for generated package.
  148. :param bool preserve_work_directory: True to preserve the temporary working directory.
  149. Useful for debugging of a package, and if the user wants to look at the generated package without having to
  150. unzip it.
  151. :return: None
  152. """
  153. work_directory = self.__create_temp_workspace()
  154. if Package._is_bootloader_softdevice_combination(self.firmwares_data):
  155. # Removing softdevice and bootloader data from dictionary and adding the combined later
  156. softdevice_fw_data = self.firmwares_data.pop(HexType.SOFTDEVICE)
  157. bootloader_fw_data = self.firmwares_data.pop(HexType.BOOTLOADER)
  158. softdevice_fw_name = softdevice_fw_data[FirmwareKeys.FIRMWARE_FILENAME]
  159. bootloader_fw_name = bootloader_fw_data[FirmwareKeys.FIRMWARE_FILENAME]
  160. new_filename = "sd_bl.bin"
  161. sd_bl_file_path = os.path.join(work_directory, new_filename)
  162. nrf_hex = nRFHex(softdevice_fw_name, bootloader_fw_name)
  163. nrf_hex.tobinfile(sd_bl_file_path)
  164. softdevice_size = nrf_hex.size()
  165. bootloader_size = nrf_hex.bootloadersize()
  166. self.__add_firmware_info(HexType.SD_BL,
  167. sd_bl_file_path,
  168. softdevice_fw_data[FirmwareKeys.INIT_PACKET_DATA],
  169. softdevice_size,
  170. bootloader_size)
  171. for hex_type in self.firmwares_data:
  172. firmware = self.firmwares_data[hex_type]
  173. # Normalize the firmware file and store it in the work directory
  174. firmware[FirmwareKeys.BIN_FILENAME] = \
  175. Package.normalize_firmware_to_bin(work_directory, firmware[FirmwareKeys.FIRMWARE_FILENAME])
  176. # Calculate the hash for the .bin file located in the work directory
  177. bin_file_path = os.path.join(work_directory, firmware[FirmwareKeys.BIN_FILENAME])
  178. init_packet_data = firmware[FirmwareKeys.INIT_PACKET_DATA]
  179. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_IS_MESH] = self.is_mesh
  180. if self.dfu_ver <= 0.5:
  181. firmware_hash = Package.calculate_crc16(bin_file_path)
  182. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_FIRMWARE_CRC16] = firmware_hash
  183. elif self.dfu_ver == 0.6:
  184. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_EXT_PACKET_ID] = INIT_PACKET_USES_CRC16
  185. firmware_hash = Package.calculate_crc16(bin_file_path)
  186. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_FIRMWARE_CRC16] = firmware_hash
  187. elif self.dfu_ver == 0.7:
  188. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_EXT_PACKET_ID] = INIT_PACKET_USES_HASH
  189. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_FIRMWARE_LENGTH] = int(Package.calculate_file_size(bin_file_path))
  190. firmware_hash = Package.calculate_sha256_hash(bin_file_path)
  191. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_FIRMWARE_HASH] = firmware_hash
  192. elif self.dfu_ver == 0.8:
  193. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_EXT_PACKET_ID] = INIT_PACKET_EXT_USES_ECDS
  194. firmware_hash = Package.calculate_sha256_hash(bin_file_path)
  195. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_FIRMWARE_LENGTH] = int(Package.calculate_file_size(bin_file_path))
  196. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_FIRMWARE_HASH] = firmware_hash
  197. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_MESH_TYPE] = hex_type
  198. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_MESH_START_ADDR] = 0xFFFFFFFF
  199. if self.key_file:
  200. temp_packet = self._create_init_packet(firmware, self.is_mesh)
  201. if self.is_mesh:
  202. # mesh continues the hash for the firmware, instead of hashing it twice.
  203. with open(bin_file_path, 'rb') as fw_file:
  204. temp_packet += fw_file.read()
  205. signer = Signing()
  206. signer.load_key(self.key_file)
  207. signature = signer.sign(temp_packet)
  208. init_packet_data[PacketField.NORDIC_PROPRIETARY_OPT_DATA_INIT_PACKET_ECDS] = signature
  209. # Store the .dat file in the work directory
  210. init_packet = self._create_init_packet(firmware, self.is_mesh)
  211. init_packet_filename = firmware[FirmwareKeys.BIN_FILENAME].replace(".bin", ".dat")
  212. with open(os.path.join(work_directory, init_packet_filename), 'wb') as init_packet_file:
  213. init_packet_file.write(init_packet)
  214. firmware[FirmwareKeys.DAT_FILENAME] = \
  215. init_packet_filename
  216. # Store the manifest to manifest.json
  217. manifest = self.create_manifest()
  218. with open(os.path.join(work_directory, Package.MANIFEST_FILENAME), "w") as manifest_file:
  219. manifest_file.write(manifest)
  220. # Package the work_directory to a zip file
  221. Package.create_zip_package(work_directory, filename)
  222. # Delete the temporary directory
  223. if not preserve_work_directory:
  224. shutil.rmtree(work_directory)
  225. @staticmethod
  226. def __create_temp_workspace():
  227. return tempfile.mkdtemp(prefix="nrf_dfu_")
  228. @staticmethod
  229. def create_zip_package(work_directory, filename):
  230. files = os.listdir(work_directory)
  231. with ZipFile(filename, 'w') as package:
  232. for _file in files:
  233. file_path = os.path.join(work_directory, _file)
  234. package.write(file_path, _file)
  235. @staticmethod
  236. def calculate_file_size(firmware_filename):
  237. b = os.path.getsize(firmware_filename)
  238. return b
  239. @staticmethod
  240. def calculate_sha256_hash(firmware_filename):
  241. read_buffer = 4096
  242. digest = hashlib.sha256()
  243. with open(firmware_filename, 'rb') as firmware_file:
  244. while True:
  245. data = firmware_file.read(read_buffer)
  246. if data:
  247. digest.update(data)
  248. else:
  249. break
  250. return digest.digest()
  251. @staticmethod
  252. def calculate_crc16(firmware_filename):
  253. """
  254. Calculates CRC16 has on provided firmware filename
  255. :type str firmware_filename:
  256. """
  257. data_buffer = b''
  258. read_size = 4096
  259. with open(firmware_filename, 'rb') as firmware_file:
  260. while True:
  261. data = firmware_file.read(read_size)
  262. if data:
  263. data_buffer += data
  264. else:
  265. break
  266. return calc_crc16(data_buffer, 0xffff)
  267. def create_manifest(self):
  268. manifest = ManifestGenerator(self.dfu_ver, self.firmwares_data)
  269. return manifest.generate_manifest()
  270. @staticmethod
  271. def _is_bootloader_softdevice_combination(firmwares):
  272. return (HexType.BOOTLOADER in firmwares) and (HexType.SOFTDEVICE in firmwares)
  273. def __add_firmware_info(self, firmware_type, filename, init_packet_data, sd_size=None, bl_size=None):
  274. self.firmwares_data[firmware_type] = {
  275. FirmwareKeys.FIRMWARE_FILENAME: filename,
  276. FirmwareKeys.INIT_PACKET_DATA: init_packet_data.copy(),
  277. # Copying init packet to avoid using the same for all firmware
  278. }
  279. if firmware_type == HexType.SD_BL:
  280. self.firmwares_data[firmware_type][FirmwareKeys.SD_SIZE] = sd_size
  281. self.firmwares_data[firmware_type][FirmwareKeys.BL_SIZE] = bl_size
  282. @staticmethod
  283. def _create_init_packet(firmware_data, is_mesh = False):
  284. if is_mesh:
  285. p = PacketMesh(firmware_data[FirmwareKeys.INIT_PACKET_DATA])
  286. else:
  287. p = Packet(firmware_data[FirmwareKeys.INIT_PACKET_DATA])
  288. return p.generate_packet()
  289. @staticmethod
  290. def normalize_firmware_to_bin(work_directory, firmware_path):
  291. firmware_filename = os.path.basename(firmware_path)
  292. new_filename = firmware_filename.replace(".hex", ".bin")
  293. new_filepath = os.path.join(work_directory, new_filename)
  294. if not os.path.exists(new_filepath):
  295. temp = nRFHex(firmware_path)
  296. temp.tobinfile(new_filepath)
  297. return new_filepath
  298. @staticmethod
  299. def unpack_package(package_path, target_dir):
  300. """
  301. Unpacks a Nordic DFU package.
  302. :param str package_path: Path to the package
  303. :param str target_dir: Target directory to unpack the package to
  304. :return: Manifest Manifest: Returns a manifest back to the user. The manifest is a parse datamodel
  305. of the manifest found in the Nordic DFU package.
  306. """
  307. if not os.path.isfile(package_path):
  308. raise NordicSemiException("Package {0} not found.".format(package_path))
  309. target_dir = os.path.abspath(target_dir)
  310. target_base_path = os.path.dirname(target_dir)
  311. if not os.path.exists(target_base_path):
  312. raise NordicSemiException("Base path to target directory {0} does not exist.".format(target_base_path))
  313. if not os.path.isdir(target_base_path):
  314. raise NordicSemiException("Base path to target directory {0} is not a directory.".format(target_base_path))
  315. if os.path.exists(target_dir):
  316. raise NordicSemiException(
  317. "Target directory {0} exists, not able to unpack to that directory.",
  318. target_dir)
  319. with ZipFile(package_path, 'r') as pkg:
  320. pkg.extractall(target_dir)
  321. with open(os.path.join(target_dir, Package.MANIFEST_FILENAME), 'r') as f:
  322. _json = f.read()
  323. """:type :str """
  324. return Manifest.from_json(_json)