UNPKG

zbtk

Version:

ZigBee Toolkit for Node.js

1,107 lines (1,047 loc) 40.2 kB
import { Buffer } from 'node:buffer'; import { Parser } from 'binary-parser-encoder-bump'; import { pks, decrypt } from './crypto.js'; import { key as hashKey } from './hash.js'; import traverse from 'traverse'; const formatters = { bool: value => !!value, datetime: buffer => buffer, // TODO hoist: function(object) { if (object.$hoist) { Object.assign(this, object.$hoist); return this; } return object; } }; function optional(name, options) { if (!options) { options = name; name = undefined; } return { tag: function() { return (typeof options.tag === 'string' ? this[options.tag] : options.tag.apply(this)) ? 1 : 0; }, formatter: options.formatter, defaultChoice: options.default || new Parser(), // empty parser / don't include anything choices: { 1: name ? new Parser().nest(name, { type: options.type }) : options.type } }; } function parent(context, field) { while (context) { if (context[field]) { return context[field]; } else { context = context.$parent; } } return undefined; } function generateSecureParser(name, aadLength, choices, defaultChoice = () => null) { const tempContext = {}; // TODO remove as soon as https://github.com/keichi/binary-parser/issues/263 is fixed return new Parser() .namely(name) .useContextVars() .nest('sec', { type: new Parser() .buffer('scf', { length: 1 }) // le .seek(-1) .nest('sc', { type: new Parser() .bit1('$unused') .bit1('verified_fc') .bit1('ext_nonce', { formatter: formatters.bool }) .bit2('key_id') .bit3('sec_level') }) .uint32le('counter') .buffer('src64', { length: 8 }) // le .choice(optional({ tag: function() { return this.sc.key_id === 0x1; }, // Network Key type: new Parser() .uint8('key_seqno') })) .pointer('mic', { offset: function() { return this.$root.$wpanStart + this.$root.length - 6; // 2 bytes TI CC24xx-format metadata, 4 bytes mic }, type: new Parser() .buffer('data', { length: 4 }), formatter: parser => parser.data // do not nest pointer }) }) .saveOffset('$dataOffset') .buffer('$data', { length: function() { return this.$root.length - (this.$dataOffset - this.$root.$wpanStart) - 6; // 2 bytes TI CC24xx-format metadata, 4 bytes mic; } }) .choice(optional({ tag: () => pks.length, // if there are any pre-shared keys, try to decrypt the payload with it default: new Parser().buffer('data', { // keep the raw data readUntil: () => true, // do not read any additional data formatter: function() { return this.$data; } }), type: new Parser() .pointer('$aad', { offset: function() { return parent(this, '$zbee_aadStart'); }, type: new Parser().buffer('data', { length: aadLength }), formatter: parser => parser.data // do not nest pointer }) .buffer('$decrypt', { // override the data field with the decrypted data readUntil: () => true, // do not read any additional data formatter: function() { // try to decrypt with any one of the pre-shared keys let lastErr; for (let k of pks) { if (this.sec.sc.key_id === 0x2) { // key-transport key k = hashKey(k, 0x00); } else if (this.sec.sc.key_id === 0x3) { // key-load key k = hashKey(k, 0x02); } try { return decrypt(this.$data, k, this.sec.src64, this.sec.counter, this.sec.scf, this.$aad, this.sec.mic); } catch (err) { lastErr = err; } } if (process.env.ZBTK_PARSE_FAIL_DECRYPT) { throw lastErr; } else { return 'DECRYPT_FAIL'; // in case decryption fails, keep the raw data } } }) .choice({ tag: function() { const decrypt = this.$decrypt; if (!Buffer.isBuffer(decrypt)) { // decryption failed, keep original data only return 0xF; } else { // keep decrypted data only tempContext.data = this.$data = decrypt; return parent(this, 'fc').type; } }, defaultChoice: defaultChoice(tempContext), // TODO no longer needed as a function, as soon as tempContext can be removed choices: { ...(choices(tempContext)), // TODO no longer needed as a function, as soon as tempContext can be removed 0xF: new Parser().buffer('data', { // keep the raw data readUntil: () => true, // do not read any additional data formatter: function() { return this.$data; } }) } }) })); } function zclAttrValueChoice(typeTag = 'type[0]', complex = true) { return { tag: typeTag, choices: { 0x00: new Parser(), // null / no data to read from the buffer 0x08: new Parser().buffer('value', { length: 8 }), // data8 0x09: new Parser().buffer('value', { length: 16 }), // data16 0x0a: new Parser().buffer('value', { length: 24 }), // data24 0x0b: new Parser().buffer('value', { length: 32 }), // data32 0x0c: new Parser().buffer('value', { length: 40 }), // data40 0x0d: new Parser().buffer('value', { length: 48 }), // data48 0x0e: new Parser().buffer('value', { length: 56 }), // data56 0x0f: new Parser().buffer('value', { length: 64 }), // data64 0x10: new Parser().bit8('value', { formatter: formatters.bool }), // bool 0x18: new Parser().buffer('value', { length: 1 }), // bits8 0x19: new Parser().buffer('value', { length: 2 }), // bits16 0x1a: new Parser().buffer('value', { length: 3 }), // bits24 0x1b: new Parser().buffer('value', { length: 4 }), // bits32 0x1c: new Parser().buffer('value', { length: 5 }), // bits40 0x1d: new Parser().buffer('value', { length: 6 }), // bits48 0x1e: new Parser().buffer('value', { length: 7 }), // bits56 0x1f: new Parser().buffer('value', { length: 8 }), // bits64 0x20: new Parser().uint8('value'), // uint8 0x21: new Parser().uint16le('value'), // uint16 0x22: new Parser().bit24('value'), // uint24 / TODO? 0x23: new Parser().uint32le('value'), // uint32 0x24: new Parser().uint32le('$value').bit8('$value2'), // uint40 / TODO 0x25: new Parser().uint32le('$value').bit16('$value2'), // uint48 / TODO 0x26: new Parser().uint32le('$value').bit24('$value2'), // uint56 / TODO 0x27: new Parser().uint64le('value'), // uint64 0x28: new Parser().int8('value'), // int8 0x29: new Parser().int16le('value'), // int16 0x2a: new Parser().bit24('value'), // int24 / TODO? 0x2b: new Parser().int32le('value'), // int32 0x2c: new Parser().int32le('$value').bit8('$value2'), // int40 / TODO 0x2d: new Parser().int32le('$value').bit16('$value2'), // int48 / TODO 0x2e: new Parser().int32le('$value').bit24('$value2'), // int56 / TODO 0x2f: new Parser().int64le('value'), // int64 0x30: new Parser().uint8('value'), // enum8 0x31: new Parser().uint16le('value'), // enum16 0x38: new Parser().floatle('value'), // sfloat / TODO? 0x39: new Parser().floatle('value'), // float 0x3a: new Parser().doublele('value'), // double 0x41: new Parser().uint8('$length').buffer('value', { length: '$length' }), // ostr 0x42: new Parser().uint8('$length').string('value', { length: '$length' }), // cstr 0x43: new Parser().uint16le('$length').buffer('value', { length: '$length' }), // lostr 0x44: new Parser().uint16le('$length').string('value', { length: '$length' }), // lcstr ...(complex ? { 0x48: new Parser().uint8('$el_type').uint16le('$el_num') // array .array('value', { length: '$el_num', type: new Parser() .choice(zclAttrValueChoice('$el_type', false)) }), // 0x4c: new Parser().struct('struct', { length: x }), // TODO / struct 0x50: new Parser().uint8('$el_type').uint16le('$el_num') // set .array('value', { length: '$el_num', type: new Parser() .choice(zclAttrValueChoice('$el_type', false)) }), 0x51: new Parser().uint8('$el_type').uint16le('$el_num') // bag .array('value', { length: '$el_num', type: new Parser() .choice(zclAttrValueChoice('$el_type', false)) }) } : {}), 0xe0: new Parser() // time .nest('value', { type: new Parser() .uint8('hours') .uint8('mins') .uint8('secs') .uint8('csecs') }), 0xe1: new Parser() // date .nest('value', { type: new Parser() .uint8('year') .uint8('month') .uint8('day') .uint8('weekd') }), 0xe2: new Parser().uint32le('value'), // utc 0xe8: new Parser().buffer('value', { length: 2 }), // cluster_id 0xe9: new Parser().buffer('value', { length: 2 }), // attr_id 0xea: new Parser().buffer('value', { length: 4 }), // bacnet_oid 0xf0: new Parser().buffer('value', { length: 8 }), // ieee_addr 0xf1: new Parser().buffer('value', { length: 16 }) // security_key } }; } // Capability Information (Named Parser / Not Exposed!) // ATTENTION: Sometimes loading this named parser results in a __parser_cinfo not defined, thus the const is used instead const cinfoParser = new Parser() .namely('cinfo') .bit1('alloc', { formatter: formatters.bool }) .bit1('security', { formatter: formatters.bool }) .bit2('$unused') .bit1('idle_rx', { formatter: formatters.bool }) .bit1('power', { formatter: formatters.bool }) .bit1('fdd', { formatter: formatters.bool }) .bit1('alt_coord', { formatter: formatters.bool }); // ZigBee Cluster Library Attributes // ATTENTION: cannot be made into a named sub-parser, as type must be a Parser object if the variable name is omitted const zclAttrsParser = new Parser() .array('attrs', { readUntil: 'eof', type: new Parser() .buffer('id', { length: 2 }) .choice({ tag: function() { return this.$parent.cmd.id[0] !== 0x00 ? 1 : 0; // Read Attributes }, defaultChoice: new Parser(), // optional choices: { 1: new Parser() .choice({ tag: '$parent.cmd.id[0]', defaultChoice: new Parser(), // optional choices: { 0x01: new Parser().buffer('status', { length: 1 }) } // Read Attributes Response }) .buffer('type', { length: 1 }) .choice(zclAttrValueChoice()) } }) }); const parsers = {}; // ZigBee Device Profile parsers.zbee_zdp = new Parser() .namely('zbee_zdp') .useContextVars() .uint8('seqno') .choice(optional({ tag: function() { return this.$parent.cluster.readUInt16LE(0) & 0x8000; // Responses }, type: new Parser() .buffer('status', { length: 1 }) // le })) .choice({ tag: function() { return this.$parent.cluster.readUInt16LE(0); }, choices: { 0x0000: new Parser() // Network Address Request .buffer('ext_addr', { length: 8 }) // le .buffer('req_type', { length: 1 }) .uint8('index'), 0x8000: new Parser() // Network Address Response .buffer('ext_addr', { length: 8 }) // le .buffer('nwk_addr', { length: 2 }), // le 0x0001: new Parser() // Ext. Device ID Request .buffer('nwk_addr', { length: 2 }) // le .buffer('req_type', { length: 1 }) .uint8('index'), 0x8001: new Parser() // Ext. Device ID Response .buffer('ext_addr', { length: 8 }) // le .buffer('nwk_addr', { length: 2 }), // le 0x0002: new Parser() // Node Descriptor Request .buffer('nwk_addr', { length: 2 }), // le 0x8002: new Parser() // Node Descriptor Response .buffer('nwk_addr', { length: 2 }) // le .nest('node', { type: new Parser() .nest('freq', { type: new Parser() .bit1('eu_sub_ghz', { formatter: formatters.bool }) .bit1('mhz2400', { formatter: formatters.bool }) .bit1('mhz900', { formatter: formatters.bool }) .bit1('mhz868', { formatter: formatters.bool }) .bit5('$unused') .bit1('frag_support', { formatter: formatters.bool }) .bit1('user', { formatter: formatters.bool }) .bit1('complex', { formatter: formatters.bool }) .bit3('type') }) .nest('cinfo', { type: 'cinfo' }) .buffer('manufacturer', { length: 2 }) // le .uint8('max_buffer') .uint8('max_incoming_transfer') .nest('server', { type: new Parser() .bit7('stack_compliance_revision') .bit2('$unused') .bit1('nwk_mgr', { formatter: formatters.bool }) .bit1('bak_disc', { formatter: formatters.bool }) .bit1('pri_disc', { formatter: formatters.bool }) .bit1('bak_bind', { formatter: formatters.bool }) .bit1('pri_bind', { formatter: formatters.bool }) .bit1('bak_trust', { formatter: formatters.bool }) .bit1('pri_trust', { formatter: formatters.bool }) }) .skip(1) .uint16le('max_outgoing_transfer') .nest('dcf', { type: new Parser() .bit6('$unused') .bit1('esdla', { formatter: formatters.bool }) .bit1('eaela', { formatter: formatters.bool }) }) }), 0x0004: new Parser() // Simple Descriptor Request .buffer('nwk_addr', { length: 2 }) // le .uint8('endpoint'), 0x8004: new Parser() // Simple Descriptor Response .buffer('nwk_addr', { length: 2 }) .uint8('simple_length') .buffer('simple_desc', { length: 'simple_length' }), 0x0005: new Parser() // Active Endpoint Request .buffer('nwk_addr', { length: 2 }), // le 0x8005: new Parser() // Active Endpoint Response .buffer('nwk_addr', { length: 2 }) // le .uint8('ep_count') .array('endpoints', { length: 'ep_count', type: 'uint8' }), 0x0006: new Parser() // Match Descriptor Request .buffer('nwk_addr', { length: 2 }) // le .buffer('profile', { length: 2 }) // le .uint8('in_count') .array('in_clusters', { length: 'in_count', formatter: array => array.map(item => item.cluster_id), type: new Parser() .buffer('cluster_id', { length: 2 }) // le }) .uint8('out_count') .array('out_clusters', { length: 'out_count', formatter: array => array.map(item => item.cluster_id), type: new Parser() .buffer('cluster_id', { length: 2 }) // le }), 0x8006: new Parser() // Match Descriptor Response .buffer('nwk_addr', { length: 2 }) // le .uint8('ep_count') .array('endpoints', { length: 'ep_count', type: 'uint8' }), 0x0013: new Parser() // Device Announcement .buffer('nwk_addr', { length: 2 }) // le .buffer('ext_addr', { length: 8 }) // le .nest('cinfo', { type: 'cinfo' }), 0x0021: new Parser() // Bind Request .buffer('src64', { length: 8 }) // le .uint8('src_ep') .buffer('cluster', { length: 2 }) // le .buffer('addr_mode', { length: 1 }) .buffer('dst64', { length: 8 }) // le .uint8('dst_ep'), 0x8021: new Parser(), // Bind Response 0x0032: new Parser() // Routing Table Request .uint8('index'), 0x8032: new Parser() // Routing Table Response .choice(optional({ tag: function() { return this.status === 0x00; // 0x84 -> not supported }, type: new Parser() .buffer('data', { readUntil: 'eof' }) // TODO })), 0x0034: new Parser() // Leave Request .buffer('ext_addr', { length: 8 }) // le .nest('leave', { type: new Parser() .bit1('rejoin', { formatter: formatters.bool }) .bit1('remove', { formatter: formatters.bool }) .bit6('$unused') }), 0x8034: new Parser(), // Leave Response 0x0036: new Parser() // Permit Join Request .uint8('duration') .uint8('significance') } }); // ZigBee Cluster Library parsers.zbee_zcl = new Parser() .namely('zbee_zcl') .useContextVars() .buffer('fcf', { length: 1 }) // frame control field .seek(-1) .nest('fc', { type: new Parser() .bit3('$unused') .bit1('ddr', { formatter: formatters.bool }) .bit1('dir') .bit1('ms', { formatter: formatters.bool }) .bit2('type') }) .nest('cmd', { type: new Parser() .choice({ tag: function() { return this.$parent.fc.ms ? 1 : 0; }, defaultChoice: new Parser(), // optional choices: { 1: new Parser().buffer('mc', { length: 2 }) } // manufacturer code }) .uint8('tsn') .buffer('id', { length: 1 }) .choice({ tag: 'id[0]', defaultChoice: new Parser(), // optional choices: { 0x0b: new Parser() // Default Response .buffer('id_rsp', { length: 1 }) } }) }) .choice({ tag: 'cmd.id[0]', choices: { 0x00: zclAttrsParser, // Read Attributes 0x01: zclAttrsParser, // Read Attributes Response 0x02: zclAttrsParser, // Write Attributes 0x04: new Parser() // Write Attributes Response .buffer('status', { length: 1 }), 0x06: new Parser() // Configure Reporting .buffer('attrs', { readUntil: 'eof' }) // TODO remove .array('$attrs', { // TODO, in order to properly parse this array, we need knowledge about properties of each attribute, don't parse it right now readUntil: () => true, // do not read any data type: new Parser() .buffer('dir', { length: 1 }) .buffer('attr_id', { length: 2 }) .buffer('type', { length: 1 }) .uint16le('minint') .uint16le('maxint') }), 0x07: new Parser() // Configure Reporting Response .buffer('status', { length: 1 }), 0x0a: zclAttrsParser, // Report Attributes 0x0b: new Parser() // Default Response .buffer('status', { length: 1 }), 0x0c: new Parser() // Discover Attributes .buffer('start', { length: 2 }) .uint8('maxnum'), 0x0d: new Parser() // Discover Attributes Response .skip(1) .array('attrs', { readUntil: 'eof', type: new Parser() .buffer('attr_id', { length: 2 }) .buffer('type', { length: 1 }) }) } }); // ZigBee Application Support Layer Command Frame parsers.zbee_aps_cmd = new Parser() .namely('zbee_aps_cmd') .useContextVars() .buffer('id', { length: 1 }) .choice({ tag: 'id[0]', choices: { 0x05: new Parser() // Transport Key .buffer('key_type', { length: 1 }) .buffer('key', { length: 16 }) .uint8('seqno') .buffer('dst', { length: 8 }) .buffer('src', { length: 8 }) } }); // ZigBee Secure / Encrypted Application Support Layer Frame parsers.zbee_aps_secure = generateSecureParser('zbee_aps_secure', 15, tempContext => ({ 0x0: new Parser(), // APS Data / TODO 0x1: new Parser().wrapped('cmd', { // APS Cmd type: 'zbee_aps_cmd', readUntil: () => true, // do not read any additional data wrapper: function() { return tempContext.data; // TODO replace with this.$data } }) })); // ZigBee Application Support Layer Data parsers.zbee_aps = new Parser() .namely('zbee_aps') .useContextVars() .saveOffset('$zbee_aadStart') .buffer('fcf', { length: 1 }) // frame control field .seek(-1) .nest('fc', { type: new Parser() .bit1('ext_header', { formatter: formatters.bool }) .bit1('ack_req', { formatter: formatters.bool }) .bit1('security', { formatter: formatters.bool }) .bit1('ack_format', { formatter: formatters.bool }) .bit2('delivery') .bit2('type') }) .choice(optional({ tag: function() { // if either 0x0 / Data, or 0x2 / Ack with ack_format not set to parse endpoint data return this.fc.type === 0x0 || (this.fc.type === 0x2 && !this.fc.ack_format) ? 1 : 0; }, type: new Parser() .uint8('dst') .buffer('cluster', { length: 2 }) .buffer('profile', { length: 2 }) .uint8('src') })) .uint8('counter') .choice({ tag: function() { return this.fc.security ? 0xF : this.fc.type; }, formatter: formatters.hoist, choices: { 0x1: new Parser(), // Cmd / TODO 0xF: new Parser().nest('$hoist', { type: 'zbee_aps_secure' }) }, defaultChoice: new Parser() .choice({ tag: 'fc.type', choices: { 0x02: new Parser() // Ack }, defaultChoice: new Parser() .choice({ tag: function() { return this.profile.readUInt16LE(0); }, choices: { 0x0000: new Parser() // Zigbee Device Profile (ZDP) .nest('zbee_zdp', { type: 'zbee_zdp' }) }, defaultChoice: new Parser() .choice({ tag: function() { return this.cluster.readUInt16LE(0); }, defaultChoice: new Parser() // all ZCL clusters .choice({ tag: 'fc.type', choices: { 0x00: new Parser().nest('zbee_zcl', { // Data type: 'zbee_zcl' }), 0x02: new Parser() // Ack } }), choices: { 0x0019: new Parser() // ignore OTA Upgrade cluster messages for now / TODO .buffer('data', { readUntil: 'eof' }) } }) }) }) }); // ZigBee Network Layer Command Frame parsers.zbee_nwk_cmd = new Parser() .namely('zbee_nwk_cmd') .useContextVars() .buffer('id', { length: 1 }) .choice({ tag: 'id[0]', choices: { 0x01: new Parser().nest('route', { // Mandy-to-One Route Request type: new Parser().nest('opts', { type: new Parser() .bit1('$unused') .bit1('mcast', { formatter: formatters.bool }) .bit1('dest_ext', { formatter: formatters.bool }) .bit2('many2one') .bit3('$unused') }) .uint8('id') .buffer('dest', { length: 2 }) .uint8('cost') }), 0x02: new Parser() // Route Reply .nest('opts', { type: new Parser() .bit1('$unused') .bit1('mcast', { formatter: formatters.bool }) .bit1('resp_ext', { formatter: formatters.bool }) .bit1('orig_ext', { formatter: formatters.bool }) .bit4('$unused') }) .nest('route', { type: new Parser() .uint8('id') .buffer('orig', { length: 2 }) // le .buffer('resp', { length: 2 }) // le .uint8('cost') .choice(optional({ tag: function() { return this.$parent.opts.orig_ext; }, type: new Parser() .buffer('orig_ext', { length: 8 }) // le })) .choice(optional({ tag: function() { return this.$parent.opts.resp_ext; }, type: new Parser() .buffer('resp_ext', { length: 8 }) // le })) }), 0x03: new Parser() // Network Status .buffer('status', { length: 1 }) .nest('route', { type: new Parser() .buffer('dest', { length: 2 }) // le }), 0x04: new Parser() // Leave .nest('leave', { type: new Parser() .bit1('children', { formatter: formatters.bool }) .bit1('request', { formatter: formatters.bool }) .bit1('rejoin', { formatter: formatters.bool }) .bit5('$unused') }), 0x05: new Parser().nest('relay', { // Route Record type: new Parser() .uint8('count') .array('relay', { length: 'count', type: new Parser() .buffer('address', { length: 2 }), // le formatter: array => array.map(item => item.address) }) }), 0x06: new Parser() // Rejoin Request .nest('cinfo', { type: cinfoParser }), 0x07: new Parser() // Rejoin Response .buffer('addr', { length: 2 }) // le .buffer('status', { length: 1 }), 0x08: new Parser().nest('link', { // Link Status type: new Parser() .bit1('$unused') .bit1('last', { formatter: formatters.bool }) .bit1('first', { formatter: formatters.bool }) .bit5('count') .array('items', { length: 'count', type: new Parser() .buffer('address', { length: 2 }) // le .bit1('$unused') .bit3('outgoing_cost') .bit1('$unused') .bit3('incoming_cost') }) }), 0x0b: new Parser() // End Device Timeout Request .int8('ed_tmo_req') .buffer('ed_config', { length: 1 }), 0x0c: new Parser() // End Device Timeout Response .buffer('status', { length: 1 }) .nest('ed_prnt_info', { type: new Parser() .bit5('$unused') .bit1('power_negotiation_supported', { formatter: formatters.bool }) .bit1('ed_tmo_req_keepalive', { formatter: formatters.bool }) .bit1('mac_data_poll_keepalive', { formatter: formatters.bool }) }) } }); // ZigBee Command Frame parsers.zbee_cmd = new Parser() .namely('zbee_cmd') .useContextVars() .buffer('id', { length: 1 }) .choice({ tag: 'id[0]', choices: { 0x01: new Parser() // Association Request .bit1('alloc_addr', { formatter: formatters.bool }) .bit1('sec_capable', { formatter: formatters.bool }) .bit2('$unused') .bit1('idle_rx', { formatter: formatters.bool }) .bit1('power_src') .bit1('device_type') .bit1('alt_coord', { formatter: formatters.bool }), 0x02: new Parser() // Association Response .nest('assoc', { type: new Parser() .buffer('addr', { length: 2 }) // le .buffer('status', { length: 1 }) }), 0x04: new Parser(), // Data Request 0x05: new Parser() // Route Record .uint8('relay_count') .buffer('relay_device', { length: 2 }), 0x07: new Parser() // Beacon Request } }); // ZigBee Secure / Encrypted Network Layer Frame parsers.zbee_nwk_secure = generateSecureParser('zbee_nwk_secure', function() { const fc = parent(this, 'fc'); return 22 + (fc.ext_dst ? 8 : 0) + (fc.ext_src ? 8 : 0) + (fc.src_route ? 2 + (parent(this, 'relay').count * 2) : 0); }, tempContext => ({ 0x0: new Parser().wrapped('zbee_aps', { // NWK Data type: 'zbee_aps', readUntil: () => true, // do not read any additional data wrapper: function() { return tempContext.data; // TODO replace with this.$data } }), 0x1: new Parser().wrapped('cmd', { // NWK Cmd type: 'zbee_nwk_cmd', readUntil: () => true, // do not read any additional data wrapper: function() { return tempContext.data; // TODO replace with this.$data } }) })); // ZigBee Network Layer Data parsers.zbee_nwk = new Parser() .namely('zbee_nwk') .useContextVars() .saveOffset('$zbee_aadStart') .buffer('fcf', { length: 2 }) // le .seek(-2) .nest('fc', { type: new Parser() .bit2('discovery') .bit4('proto_version') .bit2('type') .bit2('$unused') .bit1('end_device_initiator', { formatter: formatters.bool }) .bit1('ext_src', { formatter: formatters.bool }) .bit1('ext_dst', { formatter: formatters.bool }) .bit1('src_route', { formatter: formatters.bool }) .bit1('security', { formatter: formatters.bool }) .bit1('multicast', { formatter: formatters.bool }) }) .buffer('dst', { length: 2 }) // le .buffer('src', { length: 2 }) // le .uint8('radius') .uint8('seqno') .choice({ tag: function() { return this.fc.src_route ? 1 : 0; }, defaultChoice: new Parser(), // optional choices: { 1: new Parser().nest('relay', { type: new Parser() .uint8('count') .uint8('index') .array('relay', { length: 'count', type: new Parser() .buffer('address', { length: 2 }), // le formatter: array => array.map(item => item.address) }) }) } }) .choice({ tag: function() { return this.fc.ext_dst ? 1 : 0; }, defaultChoice: new Parser(), // optional choices: { 1: new Parser().buffer('dst64', { length: 8 }) } // le }) .choice({ tag: function() { return this.fc.ext_src ? 1 : 0; }, defaultChoice: new Parser(), // optional choices: { 1: new Parser().buffer('src64', { length: 8 }) } // le }) .choice({ tag: function() { return this.fc.security ? 0xF : this.fc.type; }, formatter: formatters.hoist, choices: { 0x0: new Parser().nest('zbee_aps', { // Data type: 'zbee_aps' }), 0x1: new Parser().nest('cmd', { // Cmd type: 'zbee_nwk_cmd' }), 0xF: new Parser().nest('$hoist', { type: 'zbee_nwk_secure' }) } }) .seek(4); // skip 4 bytes mic, as we already parsed it before; // ZigBee Beacon parsers.zbee_beacon = new Parser() .namely('zbee_beacon') .useContextVars() .buffer('protocol', { length: 1 }) .bit1('end_dev', { formatter: formatters.bool }) .bit4('depth') .bit1('router', { formatter: formatters.bool }) .bit4('version') .bit4('profile') .buffer('ext_panid', { length: 8 }) // le .buffer('tx_offset', { length: 3 }) // le, .buffer('update_id', { length: 1 }); // IEEE 802.15.4 Low-Rate Wireless PAN (WPAN) parsers.wpan = new Parser() .namely('wpan') .useContextVars() .buffer('fcf', { length: 2 }) // frame control field .seek(-2) .nest('fc', { type: new Parser() .bit1('reserved', { formatter: formatters.bool }) .bit1('pan_id_compression', { formatter: formatters.bool }) .bit1('ack_request', { formatter: formatters.bool }) .bit1('pending', { formatter: formatters.bool }) .bit1('security', { formatter: formatters.bool }) .bit3('type') .bit2('src_addr_mode') .bit2('version') .bit2('dst_addr_mode') .bit1('ie_present', { formatter: formatters.bool }) .bit1('seqno_suppression', { formatter: formatters.bool }) }) .choice({ tag: function() { return this.fc.seqno_suppression ? 1 : 0; }, choices: { 0: new Parser() .uint8('seq_no'), 1: new Parser() } }) .choice({ tag: 'fc.type', choices: { 0x2: new Parser() // Ack }, defaultChoice: new Parser() /* Implements Table 7-6 of IEEE 802.15.4-2015 * * Destination Address Source Address Destination PAN ID Source PAN ID PAN ID Compression *------------------------------------------------------------------------------------------------- * 1. Not Present Not Present Not Present Not Present 0 * 2. Not Present Not Present Present Not Present 1 * 3. Present Not Present Present Not Present 0 * 4. Present Not Present Not Present Not Present 1 * * 5. Not Present Present Not Present Present 0 * 6. Not Present Present Not Present Not Present 1 * * 7. Extended Extended Present Not Present 0 * 8. Extended Extended Not Present Not Present 1 * * 9. Short Short Present Present 0 * 10. Short Extended Present Present 0 * 11. Extended Short Present Present 0 * * 12. Short Extended Present Not Present 1 * 13. Extended Short Present Not Present 1 * 14. Short Short Present Not Present 1 */ .choice(optional({ tag: function() { if (this.fc.version === 0x0 || this.fc.version === 0x1) { // IEEE Std 802.15.4-2003, IEEE Std 802.15.4-2005 if (this.fc.dst_addr_mode !== 0x0 && this.fc.src_addr_mode !== 0x0) { return true; } else if (this.fc.pan_id_compression) { return false; // invalid pan_id_compression! } else { return this.fc.dst_addr_mode !== 0x0 && this.fc.src_addr_mode === 0x0; } } else if (this.fc.version === 0x2) { // IEEE Std 802.15.4-2015, determine based on Table 7-6 if (this.fc.type === 0x0 || this.fc.type === 0x1 || this.fc.type === 0x2 || this.fc.type === 0x3) { // Beacon, Data, Ack or Cmd return !( // define the conditions where the Dst. PAN ID is *NOT* present (this.fc.dst_addr_mode === 0x0 && this.fc.src_addr_mode === 0x0 && !this.fc.pan_id_compression) || // row 1. (this.fc.dst_addr_mode !== 0x0 && this.fc.src_addr_mode === 0x0 && this.fc.pan_id_compression) || // row 4. (this.fc.dst_addr_mode === 0x0 && this.fc.src_addr_mode !== 0x0) || // row 5. + 6. (this.fc.dst_addr_mode === 0x3 && this.fc.src_addr_mode === 0x3 && this.fc.pan_id_compression) // row 8. ); } } return false; }, type: new Parser() .buffer('dst_pan', { length: 2 }) // le })) .choice({ tag: 'fc.dst_addr_mode', choices: { 0x0: new Parser(), // None 0x2: new Parser() // Short .buffer('dst16', { length: 2 }), // le 0x3: new Parser() // Long / Ext. .buffer('dst64', { length: 8 }) // le } }) .choice(optional({ tag: function() { if (this.fc.version === 0x0 || this.fc.version === 0x1) { // IEEE Std 802.15.4-2003, IEEE Std 802.15.4-2005 if (this.fc.dst_addr_mode !== 0x0 && this.fc.src_addr_mode !== 0x0) { return !this.fc.pan_id_compression; } else if (this.fc.pan_id_compression) { return false; // invalid pan_id_compression! } else { return this.fc.dst_addr_mode === 0x0 && this.fc.src_addr_mode !== 0x0; } } else if (this.fc.version === 0x2) { // IEEE Std 802.15.4-2015, determine based on Table 7-6 if (this.fc.type === 0x0 || this.fc.type === 0x1 || this.fc.type === 0x2 || this.fc.type === 0x3) { // Beacon, Data, Ack or Cmd return ( // define the conditions where the Src. PAN ID *IS* present (this.fc.dst_addr_mode === 0x0 && this.fc.src_addr_mode !== 0x0 && !this.fc.pan_id_compression) || // row 5. (this.fc.dst_addr_mode === 0x2 && this.fc.src_addr_mode === 0x2 && !this.fc.pan_id_compression) || // row 9. (this.fc.dst_addr_mode === 0x2 && this.fc.src_addr_mode === 0x3 && !this.fc.pan_id_compression) || // row 10. (this.fc.dst_addr_mode === 0x3 && this.fc.src_addr_mode === 0x2 && !this.fc.pan_id_compression) // row 11. ); } } return false; }, type: new Parser() .buffer('src_pan', { length: 2 }) // le })) .choice({ tag: 'fc.src_addr_mode', choices: { 0x0: new Parser(), // None 0x2: new Parser() // Short .buffer('src16', { length: 2 }), // le 0x3: new Parser() // Long / Ext. .buffer('src64', { length: 8 }) // le } }) .choice({ tag: 'fc.type', choices: { 0x0: new Parser() // Beacon .bit1('assoc_permit', { formatter: formatters.bool }) .bit1('bcn_coord', { formatter: formatters.bool }) .bit1('$unused') .bit1('battery_ext', { formatter: formatters.bool }) .bit4('cap') .bit4('superframe_order') .bit4('beacon_order') .buffer('gts', { length: 2 }) .nest('zbee_beacon', { type: 'zbee_beacon' }), 0x1: new Parser() // Data .nest('zbee_nwk', { type: 'zbee_nwk' }), 0x3: new Parser() // Command .nest('cmd', { type: 'zbee_cmd' }) } }) }) .buffer('ti_cc24xx_metadata', { length: 2 }); // ZigBee Encapsulation Protocol parsers.zep = new Parser() .namely('zep') .useContextVars() .string('protocol_id', { length: 2 }) .uint8('version') .bit8('type') .uint8('channel_id') .uint16('device_id') .bit8('lqi_mode') .uint8('lqi') .buffer('time', { length: 8, formatter: formatters.datetime }) .uint32('seqno') .seek(10) // reserved .uint8('length') .saveOffset('$wpanStart') .nest('wpan', { type: 'wpan' }); /** * Parse ZigBee packet data. * * By default it will parse the data as a ZigBee Encapsulation Protocol (ZEP) packet. * Other packet types can be parsed by specifying the type. * * @param {Buffer} data the packet data to parse * @param {string} [type='zep'] the type of packet to parse * @returns {object} the parsed packet data */ export function parse(data, type = 'zep') { if (!(type in parsers)) { throw new TypeError(`Unknown packet type: ${type}`); } const result = Object.defineProperty(parsers[type].parse(data), 'toString', { value: function() { return jsonStringify(this); }, configurable: true }); if (process.env.ZBTK_PARSE_KEEP_TEMP) { return result; } // deep clean up / remove any $ temporary fields / variables from the result return traverse(result).forEach(function() { if (this.key && this.key.startsWith('$')) { this.remove(); } }); } export default parse; import { stdinMiddleware, jsonStringify } from './utils.js'; export const commands = [ { command: 'parse [data]', desc: 'Packet Binary Parser', builder: yargs => stdinMiddleware(yargs .option('type', { desc: 'Type of packet to parse', type: 'string', choices: Object.keys(parsers), default: 'zep' }), { desc: 'Data to parse' }) .example('$0 parse 4558020113fffe0029d84f48995f78359c000a91aa000000000000000000000502003ffecb', 'Parse the given data as a ZigBee Encapsulation Protocol (ZEP) packet') .example('echo -n 4558020113fffe0029d84f48995f78359c000a91aa000000000000000000000502003ffecb | $0 parse', 'Parse the given data from stdin as a ZigBee Encapsulation Protocol (ZEP) packet') .version(false) .help(), handler: argv => { console.log(`${parse(argv.data, argv.type)}`); } } ];