import { getErrorMessage } from "@zwave-js/shared" ; import * as crypto from "crypto" ; import MemoryMap from "nrf-intel-hex" ; import { ZWaveError , ZWaveErrorCodes } from "../error/ZWaveError" ; import { CRC16_CCITT } from "./crc" ; export type FirmwareFileFormat = "aeotec" | "otz" | "ota" | "hex" | "hec" | "gecko" | "bin" ; export interface Firmware { data : Buffer ; firmwareTarget ? : number ; } const firmwareIndicators = { aeotec : Buffer . from ( "Zensys.ZWave" , "utf8" ) , gecko : 0xeb17a603 , hec : Buffer . from ( "HSENC2" , "ascii" ) } ; param filename param rawData export function guessFirmwareFileFormat ( filename , rawData ) { if ( filename . endsWith ( ".bin" ) ) { return "bin" ; } else if ( ( filename . endsWith ( ".exe" ) || filename . endsWith ( ".ex_" ) ) && rawData . includes ( firmwareIndicators . aeotec ) ) { return "aeotec" ; } else if ( /\.(hex|ota|otz)$/ . test ( filename ) ) { return filename . slice ( - 3 ) as FirmwareFileFormat ; } else if ( filename . endsWith ( ".gbl" ) && rawData . readUInt32BE ( 0 ) === firmwareIndicators . gecko ) { return "gecko" ; } else if ( filename . endsWith ( ".hec" ) && rawData . slice ( 0 , firmwareIndicators . hec . length ) . equals ( firmwareIndicators . hec ) ) { return "hec" ; } else { throw new ZWaveError ( "Could not detect firmware format" , ZWaveErrorCodes . Invalid_Firmware_File ) ; } } /**
 * Extracts the firmware data from a file. The following formats are available:
 * - `"aeotec"` - A Windows executable (.exe or .ex_) that contains Aeotec's upload tool
 * - `"otz"` - A compressed firmware file in Intel HEX format
 * - `"ota"` or `"hex"` - An uncompressed firmware file in Intel HEX format
 * - `"hec"` - An encrypted Intel HEX firmware file
 * - `"gecko"` - A binary gecko bootloader firmware file with `.gbl` extension
 *
 * The returned firmware data and target can be used to start a firmware update process with `node.beginFirmwareUpdate`
 */ export function extractFirmware ( rawData , format ) { switch ( format ) { case "aeotec" : return extractFirmwareAeotec ( rawData ) ; case "otz" : case "ota" : if ( rawData . every ( ( b ) => b <= 127 ) ) { try { return extractFirmwareHEX ( rawData ) ; } catch ( e ) { if ( e instanceof ZWaveError && e . code === ZWaveErrorCodes . Argument_Invalid ) { } else { throw e ; } } } return extractFirmwareRAW ( rawData ) ; case "hex" : return extractFirmwareHEX ( rawData ) ; case "hec" : return extractFirmwareHEC ( rawData ) ; case "gecko" : return extractFirmwareRAW ( rawData ) ; case "bin" : return extractFirmwareRAW ( rawData ) ; } } function extractFirmwareRAW ( data ) { return { data } ; } function extractFirmwareAeotec ( data ) { if ( data . readUInt16BE ( 0 ) !== 0x4d5a ) { throw new ZWaveError ( "This does not appear to be a valid Aeotec updater (not an executable)!" , ZWaveErrorCodes . Argument_Invalid ) ; } const firmwareStart = data . readUInt32BE ( data . length - 8 ) ; const firmwareLength = data . readUInt32BE ( data . length - 4 ) ; let numControlBytes = 8 ; if ( data . includes ( Buffer . from ( "ImageCalcCrc16" , "ascii" ) ) ) { numControlBytes += 2 ; } switch ( true ) { case firmwareStart + firmwareLength === data . length - 256 - numControlBytes : break ; case firmwareStart + firmwareLength === data . length - 256 - 8 : numControlBytes = 8 ; break ; default : throw new ZWaveError ( "This does not appear to be a valid Aeotec updater (invalid firmware length)!" , ZWaveErrorCodes . Argument_Invalid ) ; } const firmwareData = data . slice ( firmwareStart , firmwareStart + firmwareLength ) ; const firmwareNameBytes = data . slice ( data . length - 256 - numControlBytes ) . slice ( 0 , 256 ) ; if ( numControlBytes === 10 ) { const checksum = data . readUInt16BE ( data . length - 10 ) ; const actualChecksum = CRC16_CCITT ( Buffer . concat ( [ firmwareData , firmwareNameBytes ] ) , 0xfe95 ) ; if ( checksum !== actualChecksum ) { throw new ZWaveError ( "This does not appear to be a valid Aeotec updater (invalid checksum)!" , ZWaveErrorCodes . Argument_Invalid ) ; } } const firmwareTarget = firmwareNameBytes [ 0 ] < 0x20 ? firmwareNameBytes [ 0 ] : undefined ; const firmwareNameOffset = firmwareTarget == undefined ? 0 : 1 ; const firmwareName = firmwareNameBytes . slice ( firmwareNameOffset , firmwareNameBytes . indexOf ( 0 , firmwareNameOffset ) ) . toString ( "utf8" ) ; if ( ! /^[a-zA-Z0-9_ -]+$/ . test ( firmwareName ) ) { throw new ZWaveError ( "This does not appear to be a valid Aeotec updater (invalid firmware name)!" , ZWaveErrorCodes . Argument_Invalid ) ; } const ret = { data : firmwareData } ; if ( firmwareTarget != undefined ) { ret . firmwareTarget = firmwareTarget ; } if ( /__TargetZwave__/ . test ( firmwareName ) ) { ret . firmwareTarget = 0 ; } else { const match = /__TargetMcu(\d)__/ . exec ( firmwareName ) ; if ( match ) ret . firmwareTarget = + match [ 1 ] ; } return ret ; } function extractFirmwareHEX ( dataHEX ) { try { if ( Buffer . isBuffer ( dataHEX ) ) { dataHEX = dataHEX . toString ( "ascii" ) ; } const memMap = MemoryMap . fromHex ( dataHEX ) ; let data = Buffer . from ( [ ] ) ; for ( const [ offset , chunk ] of memMap . entries ( ) ) { data = Buffer . concat ( [ data , Buffer . alloc ( offset - data . length , 0xff ) , chunk , ] ) ; } return { data } ; } catch ( e ) { if ( /Malformed/ . test ( getErrorMessage ( e ) ) ) { throw new ZWaveError ( "Could not parse HEX firmware file!" , ZWaveErrorCodes . Argument_Invalid ) ; } else { throw e ; } } } function extractFirmwareHEC ( data ) { const key = "d7a68def0f4a1241940f6cb8017121d15f0e2682e258c9f7553e706e834923b7" ; const iv = "0e6519297530583708612a2823663844" ; const decipher = crypto . createDecipheriv ( "aes-256-cbc" , Buffer . from ( key , "hex" ) , Buffer . from ( iv , "hex" ) ) ; const ciphertext = Buffer . from ( data . slice ( 6 ) . toString ( "ascii" ) , "base64" ) ; const plaintext = Buffer . concat ( [ decipher . update ( ciphertext ) , decipher . final ( ) , ] ) . toString ( "ascii" ) . replace ( / /g , "\n" ) ; return extractFirmwareHEX ( plaintext ) ; }