commit e8c1bd4127af0c38fa9e860a37e34dd91d4472e0 Author: FlUxIuS Date: Wed Jan 15 10:24:15 2020 +0100 Release commit diff --git a/LoRa_PHYDecode.py b/LoRa_PHYDecode.py new file mode 100644 index 0000000..8cb936e --- /dev/null +++ b/LoRa_PHYDecode.py @@ -0,0 +1,35 @@ +#!/usr/bin/en python + +from __future__ import print_function + +# LoRa PHYdecoder - parse LoRa PHY decoded by gr-lora +# Copyright (C) 2020 Sebastien Dudek (@FlUxIuS) at @PentHertz + +import binascii +from layers.loraphy import * +import argparse + +def decodePHY(pkt): + decoded = LoRa(pkt[UDP].load) + print (repr(decoded)) + +def filterpkt(pkt, port): + if pkt.haslayer(UDP): + if pkt[UDP].dport == port: + return True + return False + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Monitor and decode MAC PHY packets.') + parser.add_argument('-p', '--port', dest='port', default=40868, + help='TAP PORT to listen on (default: UDP 40868)') + parser.add_argument('-i', '--iface', dest='iface', default='lo', + help='Interface to monitor (default: local)') + + args = parser.parse_args() + iface = args.iface + port = int(args.port) + + sniff(prn=decodePHY, + lfilter=lambda pkt:filterpkt(pkt, port), + iface=iface) diff --git a/README.md b/README.md new file mode 100644 index 0000000..316ec85 --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# LoRa Craft + +LoRa Craft is a small set of tools to receive signal with SDR, decode et craft LoRaWAN packets on top of a **gr-lora** GNU Radio module. + +This repository will be completed with others tools soon, depending on needs during assessments :) + +## Dependencies + +* Python 2 or 3 +* Scapy +* GNU Radio 3.8 +* gr-lora from [rpp0](https://github.com/rpp0): [link here](https://github.com/rpp0/gr-lora) +* A Software-Defined Radio equipment (USRP, bladeRF, RTL-SDR dongle, etc.) + +## Receive signal and decode its data + +### Receive + +To receive a signal, an example of a GRC schema is available in folder `grc_examples/usrp_LoRa_decode_to_UDP.grc` for USRP, as shown as follows: + +![alt text](https://github.com/FlUxIuS/LoRaCraft/blob/master/img/completeschema.png "Schema to receive LoRa signal") + +The channel frequency, as well as the spread facto and the bandwidth must be set correctly to valid values with the help of the FFT and waterfall sinks: + +![alt text](https://github.com/FlUxIuS/LoRaCraft/blob/master/img/frequencydet_zoomout_sf12bw125.png "Waterfall and FFT sinks") + +Note: Multiple frequencies can be used by targets. This would implies to include multiple receivers in GRC. + +For more information on how to detect LoRa signal, please take a look at the following post: TODO. + +### Decode + +Once the receiver is running with the SDR equipment, we use the script `LoRa_PHYDecode.py`: + +```bash +$ python LoRa_PHYDecode.py -h 1 ↵ +usage: LoRa_PHYDecode.py [-h] [-p PORT] [-i IFACE] + +Monitor and decode MAC PHY packets. + +optional arguments: + -h, --help show this help message and exit + -p PORT, --port PORT TAP PORT to listen on (default: UDP 40868) + -i IFACE, --iface IFACE + Interface to monitor (default: local) +``` + +By default, the script can be run as follows to decode received LoRa frames: + +```bash +$ sudo python LoRa_PHYDecode.py +] FCtrl=[] FCnt=0 FPort=2 DataPayload='i\x06D\x94\x97\x08\xce!\xd9' MIC=0x4b516899 CRC=0x96e1 |> +... +] FCtrl=[] FCnt=0 FPort=2 DataPayload='penthertz' MIC=0x20a5fcba CRC=0xcdc |> +] FCtrl=[] FCnt=0 FOpts_up=[] FOpts_down=[] FPort=92 DataPayload='' MIC=0x31c753f |> +] FCtrl=[] FCnt=0 FOpts_up=[] FOpts_down=[] FPort=92 DataPayload='' MIC=0x31c753f | +``` + +## Generate packets + +To generate packets, you can instanciate a Scapy packet as follows: + +```python +>>> from layers.loraphy import * +>>> pkt = LoRa() +>>> pkt + +``` + +And start to fill it. + +After crafting your packet, you can use [python-loranode](https://github.com/rpp0/python-loranode) as follows: + +```python +>>> from binascii +>>> from loranode import RN2483Controller +>>> to_send = binascii.hexlify(str(pkt))[3:] +>>> c = RN2483Controller("/dev/ttyACM0") # Choose the correct /dev device here +>>> c.set_sf(7) # choose your spreading factor here +>>> c.set_bw(150) # choose the bandwidth here +>>> c.set_cr("4/8") # Set 4/8 coding for example +>>> c.send_p2p(to_send) +``` + +Note that you should skip the first three bytes (Preambule, PHDR, PHDR_CRC), before sending it with `send_p2p` method. + +## LoRa crypto helpers + +Few helpers have been implemented to calculate MIC field, encrypt and decrypt packet: + +* `JoinAcceptPayload_decrypt`: decrypt Join-accept payloads; +* `JoinAcceptPayload_encrypt`: encrypt Join-accept payloads; +* `getPHY_CMAC`: compute MIC field of a packet using a provided key; +* `checkMIC`: check MIC of a packet against a provided key. + +As an example, to check if the key `000102030405060708090A0B0C0D0E0F` is used to compute MIC on the following Join-request, we can write a little script as follows: + +```python +>>> from layers.loraphy import * +>>> from lutil.crypto import * +>>> key = "000102030405060708090A0B0C0D0E0F" +>>> p = '000000006c6f7665636166656d656565746f6f00696953024c49' +>>> pkt = LoRa(binascii.unhexlify(p)) +>>> pkt +] MIC=0x53024c49 |> +>>> checkMIC(binascii.unhexlify(key), str(pkt)) +True +``` + +To check if `000102030405060708090A0B0C0D0E0F` key is used to encrypt a Join-accept message, we can combine `JoinAcceptPayload_decrypt` and `checkMIC` as follows: + +```python +>>> pkt = "000000200836e287a9805cb7ee9e5fff7c9ee97a" +>>> ja = JoinAcceptPayload_decrypt(binascii.unhexlify(key), binascii.unhexlify(pkt)) +>>> ja +'ghi#\x01\x00\xb2\\C\x03\x00\x00{\x06O\x8a' +>>> Join_Accept(ja) +> +>>> p = "\x00\x00\x00\x20"+ja # adding headers +>>> checkMIC(key.decode("hex"), p) +>>> True +``` + +## TODO + +* More helpers for other types of payloads +* Implement helpers to transmit signal with dongles more easily +* Transmit packets with SDR +* Support gr-lora from Bastille: [link here](https://github.com/BastilleResearch/gr-lora) + +Feel free to contribute if you have cool scripts/tools to share :)! diff --git a/USRPB20xmini_LoRa_receive_UDP.py b/USRPB20xmini_LoRa_receive_UDP.py new file mode 100755 index 0000000..1b8905a --- /dev/null +++ b/USRPB20xmini_LoRa_receive_UDP.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: LoRa Receiver to UDP socket with USRP +# Author: FlUxIuS +# Copyright: PentHertz +# Description: Example of a schema that receive LoRa frames and write to an UDP socket to be decoded with the Scapy Layer +# GNU Radio version: 3.8.0.0 + +from distutils.version import StrictVersion + +if __name__ == '__main__': + import ctypes + import sys + if sys.platform.startswith('linux'): + try: + x11 = ctypes.cdll.LoadLibrary('libX11.so') + x11.XInitThreads() + except: + print("Warning: failed to XInitThreads()") + +from PyQt5 import Qt +from gnuradio import eng_notation +from gnuradio import qtgui +from gnuradio.filter import firdes +import sip +from gnuradio import gr +import sys +import signal +from argparse import ArgumentParser +from gnuradio.eng_arg import eng_float, intx +from gnuradio import uhd +import time +from gnuradio.qtgui import Range, RangeWidget +import lora +from gnuradio import qtgui + +class LoRa_usrp_receive_to_UDP(gr.top_block, Qt.QWidget): + + def __init__(self): + gr.top_block.__init__(self, "LoRa Receiver to UDP socket with USRP") + Qt.QWidget.__init__(self) + self.setWindowTitle("LoRa Receiver to UDP socket with USRP") + qtgui.util.check_set_qss() + try: + self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc')) + except: + pass + self.top_scroll_layout = Qt.QVBoxLayout() + self.setLayout(self.top_scroll_layout) + self.top_scroll = Qt.QScrollArea() + self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame) + self.top_scroll_layout.addWidget(self.top_scroll) + self.top_scroll.setWidgetResizable(True) + self.top_widget = Qt.QWidget() + self.top_scroll.setWidget(self.top_widget) + self.top_layout = Qt.QVBoxLayout(self.top_widget) + self.top_grid_layout = Qt.QGridLayout() + self.top_layout.addLayout(self.top_grid_layout) + + self.settings = Qt.QSettings("GNU Radio", "LoRa_usrp_receive_to_UDP") + + try: + if StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"): + self.restoreGeometry(self.settings.value("geometry").toByteArray()) + else: + self.restoreGeometry(self.settings.value("geometry")) + except: + pass + + ################################################## + # Variables + ################################################## + self.samp_rate = samp_rate = 1e6 + self.freq_slider = freq_slider = 868e6 + self.channel_freq = channel_freq = 868.2e6 + + ################################################## + # Blocks + ################################################## + self._freq_slider_range = Range(867e6, 869e6, 1, 868e6, 200) + self._freq_slider_win = RangeWidget(self._freq_slider_range, self.set_freq_slider, 'Frequency', "counter_slider", float) + self.top_grid_layout.addWidget(self._freq_slider_win) + self._channel_freq_tool_bar = Qt.QToolBar(self) + self._channel_freq_tool_bar.addWidget(Qt.QLabel('Channel frequency' + ": ")) + self._channel_freq_line_edit = Qt.QLineEdit(str(self.channel_freq)) + self._channel_freq_tool_bar.addWidget(self._channel_freq_line_edit) + self._channel_freq_line_edit.returnPressed.connect( + lambda: self.set_channel_freq(eng_notation.str_to_num(str(self._channel_freq_line_edit.text())))) + self.top_grid_layout.addWidget(self._channel_freq_tool_bar) + self.uhd_usrp_source_0 = uhd.usrp_source( + ",".join(("", "")), + uhd.stream_args( + cpu_format="fc32", + args='', + channels=[], + ), + ) + self.uhd_usrp_source_0.set_center_freq(freq_slider, 0) + self.uhd_usrp_source_0.set_gain(0, 0) + self.uhd_usrp_source_0.set_antenna('RX2', 0) + self.uhd_usrp_source_0.set_bandwidth(samp_rate, 0) + self.uhd_usrp_source_0.set_samp_rate(samp_rate) + self.uhd_usrp_source_0.set_time_unknown_pps(uhd.time_spec()) + self.qtgui_waterfall_sink_x_0 = qtgui.waterfall_sink_c( + 1024, #size + firdes.WIN_HAMMING, #wintype + channel_freq, #fc + samp_rate, #bw + "", #name + 1 #number of inputs + ) + self.qtgui_waterfall_sink_x_0.set_update_time(0.10) + self.qtgui_waterfall_sink_x_0.enable_grid(True) + self.qtgui_waterfall_sink_x_0.enable_axis_labels(True) + + + + labels = ['', '', '', '', '', + '', '', '', '', ''] + colors = [0, 0, 0, 0, 0, + 0, 0, 0, 0, 0] + alphas = [1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0] + + for i in range(1): + if len(labels[i]) == 0: + self.qtgui_waterfall_sink_x_0.set_line_label(i, "Data {0}".format(i)) + else: + self.qtgui_waterfall_sink_x_0.set_line_label(i, labels[i]) + self.qtgui_waterfall_sink_x_0.set_color_map(i, colors[i]) + self.qtgui_waterfall_sink_x_0.set_line_alpha(i, alphas[i]) + + self.qtgui_waterfall_sink_x_0.set_intensity_range(-140, 10) + + self._qtgui_waterfall_sink_x_0_win = sip.wrapinstance(self.qtgui_waterfall_sink_x_0.pyqwidget(), Qt.QWidget) + self.top_grid_layout.addWidget(self._qtgui_waterfall_sink_x_0_win) + self.qtgui_freq_sink_x_0 = qtgui.freq_sink_c( + 1024, #size + firdes.WIN_BLACKMAN_hARRIS, #wintype + freq_slider, #fc + samp_rate, #bw + "", #name + 1 + ) + self.qtgui_freq_sink_x_0.set_update_time(0.10) + self.qtgui_freq_sink_x_0.set_y_axis(-140, 10) + self.qtgui_freq_sink_x_0.set_y_label('Relative Gain', 'dB') + self.qtgui_freq_sink_x_0.set_trigger_mode(qtgui.TRIG_MODE_FREE, 0.0, 0, "") + self.qtgui_freq_sink_x_0.enable_autoscale(False) + self.qtgui_freq_sink_x_0.enable_grid(False) + self.qtgui_freq_sink_x_0.set_fft_average(1.0) + self.qtgui_freq_sink_x_0.enable_axis_labels(True) + self.qtgui_freq_sink_x_0.enable_control_panel(True) + + + + labels = ['', '', '', '', '', + '', '', '', '', ''] + widths = [1, 1, 1, 1, 1, + 1, 1, 1, 1, 1] + colors = ["blue", "red", "green", "black", "cyan", + "magenta", "yellow", "dark red", "dark green", "dark blue"] + alphas = [1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0] + + for i in range(1): + if len(labels[i]) == 0: + self.qtgui_freq_sink_x_0.set_line_label(i, "Data {0}".format(i)) + else: + self.qtgui_freq_sink_x_0.set_line_label(i, labels[i]) + self.qtgui_freq_sink_x_0.set_line_width(i, widths[i]) + self.qtgui_freq_sink_x_0.set_line_color(i, colors[i]) + self.qtgui_freq_sink_x_0.set_line_alpha(i, alphas[i]) + + self._qtgui_freq_sink_x_0_win = sip.wrapinstance(self.qtgui_freq_sink_x_0.pyqwidget(), Qt.QWidget) + self.top_grid_layout.addWidget(self._qtgui_freq_sink_x_0_win) + self.lora_message_socket_sink_0 = lora.message_socket_sink('127.0.0.1', 40868, 0) + self.lora_lora_receiver_0 = lora.lora_receiver(samp_rate, 868e6, [channel_freq], 125000, 7, False, 4, True, False, False, 1, False, False) + + + + ################################################## + # Connections + ################################################## + self.msg_connect((self.lora_lora_receiver_0, 'frames'), (self.lora_message_socket_sink_0, 'in')) + self.connect((self.uhd_usrp_source_0, 0), (self.lora_lora_receiver_0, 0)) + self.connect((self.uhd_usrp_source_0, 0), (self.qtgui_freq_sink_x_0, 0)) + self.connect((self.uhd_usrp_source_0, 0), (self.qtgui_waterfall_sink_x_0, 0)) + + def closeEvent(self, event): + self.settings = Qt.QSettings("GNU Radio", "LoRa_usrp_receive_to_UDP") + self.settings.setValue("geometry", self.saveGeometry()) + event.accept() + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.qtgui_freq_sink_x_0.set_frequency_range(self.freq_slider, self.samp_rate) + self.qtgui_waterfall_sink_x_0.set_frequency_range(self.channel_freq, self.samp_rate) + self.uhd_usrp_source_0.set_samp_rate(self.samp_rate) + self.uhd_usrp_source_0.set_bandwidth(self.samp_rate, 0) + + def get_freq_slider(self): + return self.freq_slider + + def set_freq_slider(self, freq_slider): + self.freq_slider = freq_slider + self.qtgui_freq_sink_x_0.set_frequency_range(self.freq_slider, self.samp_rate) + self.uhd_usrp_source_0.set_center_freq(self.freq_slider, 0) + + def get_channel_freq(self): + return self.channel_freq + + def set_channel_freq(self, channel_freq): + self.channel_freq = channel_freq + Qt.QMetaObject.invokeMethod(self._channel_freq_line_edit, "setText", Qt.Q_ARG("QString", eng_notation.num_to_str(self.channel_freq))) + self.qtgui_waterfall_sink_x_0.set_frequency_range(self.channel_freq, self.samp_rate) + + + +def main(top_block_cls=LoRa_usrp_receive_to_UDP, options=None): + + if StrictVersion("4.5.0") <= StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"): + style = gr.prefs().get_string('qtgui', 'style', 'raster') + Qt.QApplication.setGraphicsSystem(style) + qapp = Qt.QApplication(sys.argv) + + tb = top_block_cls() + tb.start() + tb.show() + + def sig_handler(sig=None, frame=None): + Qt.QApplication.quit() + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + timer = Qt.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + + def quitting(): + tb.stop() + tb.wait() + qapp.aboutToQuit.connect(quitting) + qapp.exec_() + + +if __name__ == '__main__': + main() diff --git a/capture/capture_sf7bw125.pcapng b/capture/capture_sf7bw125.pcapng new file mode 100644 index 0000000..bb363a3 Binary files /dev/null and b/capture/capture_sf7bw125.pcapng differ diff --git a/grc_examples/usrp_LoRa_decode_to_UDP.grc b/grc_examples/usrp_LoRa_decode_to_UDP.grc new file mode 100644 index 0000000..04782b3 --- /dev/null +++ b/grc_examples/usrp_LoRa_decode_to_UDP.grc @@ -0,0 +1,648 @@ +options: + parameters: + author: FlUxIuS + category: '[GRC Hier Blocks]' + cmake_opt: '' + comment: '' + copyright: PentHertz + description: Example of a schema that receive LoRa frames and write to an UDP + socket to be decoded with the Scapy Layer + gen_cmake: 'On' + gen_linking: dynamic + generate_options: qt_gui + hier_block_src_path: '.:' + id: LoRa_usrp_receive_to_UDP + max_nouts: '0' + output_language: python + placement: (0,0) + qt_qss_theme: '' + realtime_scheduling: '' + run: 'True' + run_command: '{python} -u {filename}' + run_options: prompt + sizing_mode: fixed + thread_safe_setters: '' + title: LoRa Receiver to UDP socket with USRP + window_size: '' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [8, 8] + rotation: 0 + state: enabled + +blocks: +- name: channel_freq + id: variable_qtgui_entry + parameters: + comment: '' + gui_hint: '' + label: Channel frequency + type: real + value: 868.2e6 + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [0, 284.0] + rotation: 0 + state: true +- name: freq_slider + id: variable_qtgui_range + parameters: + comment: '' + gui_hint: '' + label: Frequency + min_len: '200' + orient: Qt.Horizontal + rangeType: float + start: 867e6 + step: '1' + stop: 869e6 + value: 868e6 + widget: counter_slider + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [8, 140.0] + rotation: 0 + state: true +- name: samp_rate + id: variable + parameters: + comment: '' + value: 1e6 + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [192, 12.0] + rotation: 0 + state: enabled +- name: lora_lora_receiver_0 + id: lora_lora_receiver + parameters: + affinity: '' + alias: '' + bandwidth: '125000' + center_freq: 868e6 + channel_list: channel_freq + comment: '' + conj: 'False' + cr: '4' + crc: 'True' + decimation: '1' + disable_channelization: 'False' + disable_drift_correction: 'False' + implicit: 'False' + maxoutbuf: '0' + minoutbuf: '0' + reduced_rate: 'False' + samp_rate: samp_rate + sf: '7' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [648, 108.0] + rotation: 0 + state: true +- name: lora_message_socket_sink_0 + id: lora_message_socket_sink + parameters: + affinity: '' + alias: '' + comment: '' + ip: 127.0.0.1 + layer: '0' + port: '40868' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [904, 140.0] + rotation: 0 + state: true +- name: qtgui_freq_sink_x_0 + id: qtgui_freq_sink_x + parameters: + affinity: '' + alias: '' + alpha1: '1.0' + alpha10: '1.0' + alpha2: '1.0' + alpha3: '1.0' + alpha4: '1.0' + alpha5: '1.0' + alpha6: '1.0' + alpha7: '1.0' + alpha8: '1.0' + alpha9: '1.0' + autoscale: 'False' + average: '1.0' + axislabels: 'True' + bw: samp_rate + color1: '"blue"' + color10: '"dark blue"' + color2: '"red"' + color3: '"green"' + color4: '"black"' + color5: '"cyan"' + color6: '"magenta"' + color7: '"yellow"' + color8: '"dark red"' + color9: '"dark green"' + comment: '' + ctrlpanel: 'True' + fc: freq_slider + fftsize: '1024' + freqhalf: 'True' + grid: 'False' + gui_hint: '' + label: Relative Gain + label1: '' + label10: '''''' + label2: '''''' + label3: '''''' + label4: '''''' + label5: '''''' + label6: '''''' + label7: '''''' + label8: '''''' + label9: '''''' + legend: 'True' + maxoutbuf: '0' + minoutbuf: '0' + name: '""' + nconnections: '1' + showports: 'False' + tr_chan: '0' + tr_level: '0.0' + tr_mode: qtgui.TRIG_MODE_FREE + tr_tag: '""' + type: complex + units: dB + update_time: '0.10' + width1: '1' + width10: '1' + width2: '1' + width3: '1' + width4: '1' + width5: '1' + width6: '1' + width7: '1' + width8: '1' + width9: '1' + wintype: firdes.WIN_BLACKMAN_hARRIS + ymax: '10' + ymin: '-140' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [648, 288.0] + rotation: 0 + state: true +- name: qtgui_waterfall_sink_x_0 + id: qtgui_waterfall_sink_x + parameters: + affinity: '' + alias: '' + alpha1: '1.0' + alpha10: '1.0' + alpha2: '1.0' + alpha3: '1.0' + alpha4: '1.0' + alpha5: '1.0' + alpha6: '1.0' + alpha7: '1.0' + alpha8: '1.0' + alpha9: '1.0' + axislabels: 'True' + bw: samp_rate + color1: '0' + color10: '0' + color2: '0' + color3: '0' + color4: '0' + color5: '0' + color6: '0' + color7: '0' + color8: '0' + color9: '0' + comment: '' + fc: channel_freq + fftsize: '1024' + freqhalf: 'True' + grid: 'True' + gui_hint: '' + int_max: '10' + int_min: '-140' + label1: '' + label10: '' + label2: '' + label3: '' + label4: '' + label5: '' + label6: '' + label7: '' + label8: '' + label9: '' + legend: 'True' + maxoutbuf: '0' + minoutbuf: '0' + name: '""' + nconnections: '1' + showports: 'False' + type: complex + update_time: '0.10' + wintype: firdes.WIN_HAMMING + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [648, 440.0] + rotation: 0 + state: true +- name: uhd_usrp_source_0 + id: uhd_usrp_source + parameters: + affinity: '' + alias: '' + ant0: RX2 + ant1: RX2 + ant10: RX2 + ant11: RX2 + ant12: RX2 + ant13: RX2 + ant14: RX2 + ant15: RX2 + ant16: RX2 + ant17: RX2 + ant18: RX2 + ant19: RX2 + ant2: RX2 + ant20: RX2 + ant21: RX2 + ant22: RX2 + ant23: RX2 + ant24: RX2 + ant25: RX2 + ant26: RX2 + ant27: RX2 + ant28: RX2 + ant29: RX2 + ant3: RX2 + ant30: RX2 + ant31: RX2 + ant4: RX2 + ant5: RX2 + ant6: RX2 + ant7: RX2 + ant8: RX2 + ant9: RX2 + bw0: samp_rate + bw1: '0' + bw10: '0' + bw11: '0' + bw12: '0' + bw13: '0' + bw14: '0' + bw15: '0' + bw16: '0' + bw17: '0' + bw18: '0' + bw19: '0' + bw2: '0' + bw20: '0' + bw21: '0' + bw22: '0' + bw23: '0' + bw24: '0' + bw25: '0' + bw26: '0' + bw27: '0' + bw28: '0' + bw29: '0' + bw3: '0' + bw30: '0' + bw31: '0' + bw4: '0' + bw5: '0' + bw6: '0' + bw7: '0' + bw8: '0' + bw9: '0' + center_freq0: freq_slider + center_freq1: '0' + center_freq10: '0' + center_freq11: '0' + center_freq12: '0' + center_freq13: '0' + center_freq14: '0' + center_freq15: '0' + center_freq16: '0' + center_freq17: '0' + center_freq18: '0' + center_freq19: '0' + center_freq2: '0' + center_freq20: '0' + center_freq21: '0' + center_freq22: '0' + center_freq23: '0' + center_freq24: '0' + center_freq25: '0' + center_freq26: '0' + center_freq27: '0' + center_freq28: '0' + center_freq29: '0' + center_freq3: '0' + center_freq30: '0' + center_freq31: '0' + center_freq4: '0' + center_freq5: '0' + center_freq6: '0' + center_freq7: '0' + center_freq8: '0' + center_freq9: '0' + clock_rate: 0e0 + clock_source0: '' + clock_source1: '' + clock_source2: '' + clock_source3: '' + clock_source4: '' + clock_source5: '' + clock_source6: '' + clock_source7: '' + comment: '' + dc_offs_enb0: '""' + dc_offs_enb1: '""' + dc_offs_enb10: '""' + dc_offs_enb11: '""' + dc_offs_enb12: '""' + dc_offs_enb13: '""' + dc_offs_enb14: '""' + dc_offs_enb15: '""' + dc_offs_enb16: '""' + dc_offs_enb17: '""' + dc_offs_enb18: '""' + dc_offs_enb19: '""' + dc_offs_enb2: '""' + dc_offs_enb20: '""' + dc_offs_enb21: '""' + dc_offs_enb22: '""' + dc_offs_enb23: '""' + dc_offs_enb24: '""' + dc_offs_enb25: '""' + dc_offs_enb26: '""' + dc_offs_enb27: '""' + dc_offs_enb28: '""' + dc_offs_enb29: '""' + dc_offs_enb3: '""' + dc_offs_enb30: '""' + dc_offs_enb31: '""' + dc_offs_enb4: '""' + dc_offs_enb5: '""' + dc_offs_enb6: '""' + dc_offs_enb7: '""' + dc_offs_enb8: '""' + dc_offs_enb9: '""' + dev_addr: '""' + dev_args: '""' + gain0: '0' + gain1: '0' + gain10: '0' + gain11: '0' + gain12: '0' + gain13: '0' + gain14: '0' + gain15: '0' + gain16: '0' + gain17: '0' + gain18: '0' + gain19: '0' + gain2: '0' + gain20: '0' + gain21: '0' + gain22: '0' + gain23: '0' + gain24: '0' + gain25: '0' + gain26: '0' + gain27: '0' + gain28: '0' + gain29: '0' + gain3: '0' + gain30: '0' + gain31: '0' + gain4: '0' + gain5: '0' + gain6: '0' + gain7: '0' + gain8: '0' + gain9: '0' + iq_imbal_enb0: '""' + iq_imbal_enb1: '""' + iq_imbal_enb10: '""' + iq_imbal_enb11: '""' + iq_imbal_enb12: '""' + iq_imbal_enb13: '""' + iq_imbal_enb14: '""' + iq_imbal_enb15: '""' + iq_imbal_enb16: '""' + iq_imbal_enb17: '""' + iq_imbal_enb18: '""' + iq_imbal_enb19: '""' + iq_imbal_enb2: '""' + iq_imbal_enb20: '""' + iq_imbal_enb21: '""' + iq_imbal_enb22: '""' + iq_imbal_enb23: '""' + iq_imbal_enb24: '""' + iq_imbal_enb25: '""' + iq_imbal_enb26: '""' + iq_imbal_enb27: '""' + iq_imbal_enb28: '""' + iq_imbal_enb29: '""' + iq_imbal_enb3: '""' + iq_imbal_enb30: '""' + iq_imbal_enb31: '""' + iq_imbal_enb4: '""' + iq_imbal_enb5: '""' + iq_imbal_enb6: '""' + iq_imbal_enb7: '""' + iq_imbal_enb8: '""' + iq_imbal_enb9: '""' + lo_export0: 'False' + lo_export1: 'False' + lo_export10: 'False' + lo_export11: 'False' + lo_export12: 'False' + lo_export13: 'False' + lo_export14: 'False' + lo_export15: 'False' + lo_export16: 'False' + lo_export17: 'False' + lo_export18: 'False' + lo_export19: 'False' + lo_export2: 'False' + lo_export20: 'False' + lo_export21: 'False' + lo_export22: 'False' + lo_export23: 'False' + lo_export24: 'False' + lo_export25: 'False' + lo_export26: 'False' + lo_export27: 'False' + lo_export28: 'False' + lo_export29: 'False' + lo_export3: 'False' + lo_export30: 'False' + lo_export31: 'False' + lo_export4: 'False' + lo_export5: 'False' + lo_export6: 'False' + lo_export7: 'False' + lo_export8: 'False' + lo_export9: 'False' + lo_source0: internal + lo_source1: internal + lo_source10: internal + lo_source11: internal + lo_source12: internal + lo_source13: internal + lo_source14: internal + lo_source15: internal + lo_source16: internal + lo_source17: internal + lo_source18: internal + lo_source19: internal + lo_source2: internal + lo_source20: internal + lo_source21: internal + lo_source22: internal + lo_source23: internal + lo_source24: internal + lo_source25: internal + lo_source26: internal + lo_source27: internal + lo_source28: internal + lo_source29: internal + lo_source3: internal + lo_source30: internal + lo_source31: internal + lo_source4: internal + lo_source5: internal + lo_source6: internal + lo_source7: internal + lo_source8: internal + lo_source9: internal + maxoutbuf: '0' + minoutbuf: '0' + nchan: '1' + norm_gain0: 'False' + norm_gain1: 'False' + norm_gain10: 'False' + norm_gain11: 'False' + norm_gain12: 'False' + norm_gain13: 'False' + norm_gain14: 'False' + norm_gain15: 'False' + norm_gain16: 'False' + norm_gain17: 'False' + norm_gain18: 'False' + norm_gain19: 'False' + norm_gain2: 'False' + norm_gain20: 'False' + norm_gain21: 'False' + norm_gain22: 'False' + norm_gain23: 'False' + norm_gain24: 'False' + norm_gain25: 'False' + norm_gain26: 'False' + norm_gain27: 'False' + norm_gain28: 'False' + norm_gain29: 'False' + norm_gain3: 'False' + norm_gain30: 'False' + norm_gain31: 'False' + norm_gain4: 'False' + norm_gain5: 'False' + norm_gain6: 'False' + norm_gain7: 'False' + norm_gain8: 'False' + norm_gain9: 'False' + num_mboards: '1' + otw: '' + rx_agc0: Default + rx_agc1: Default + rx_agc10: Default + rx_agc11: Default + rx_agc12: Default + rx_agc13: Default + rx_agc14: Default + rx_agc15: Default + rx_agc16: Default + rx_agc17: Default + rx_agc18: Default + rx_agc19: Default + rx_agc2: Default + rx_agc20: Default + rx_agc21: Default + rx_agc22: Default + rx_agc23: Default + rx_agc24: Default + rx_agc25: Default + rx_agc26: Default + rx_agc27: Default + rx_agc28: Default + rx_agc29: Default + rx_agc3: Default + rx_agc30: Default + rx_agc31: Default + rx_agc4: Default + rx_agc5: Default + rx_agc6: Default + rx_agc7: Default + rx_agc8: Default + rx_agc9: Default + samp_rate: samp_rate + sd_spec0: '' + sd_spec1: '' + sd_spec2: '' + sd_spec3: '' + sd_spec4: '' + sd_spec5: '' + sd_spec6: '' + sd_spec7: '' + show_lo_controls: 'False' + stream_args: '' + stream_chans: '[]' + sync: sync + time_source0: '' + time_source1: '' + time_source2: '' + time_source3: '' + time_source4: '' + time_source5: '' + time_source6: '' + time_source7: '' + type: fc32 + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [272, 228.0] + rotation: 0 + state: true + +connections: +- [lora_lora_receiver_0, frames, lora_message_socket_sink_0, in] +- [uhd_usrp_source_0, '0', lora_lora_receiver_0, '0'] +- [uhd_usrp_source_0, '0', qtgui_freq_sink_x_0, '0'] +- [uhd_usrp_source_0, '0', qtgui_waterfall_sink_x_0, '0'] + +metadata: + file_format: 1 diff --git a/img/completeschema.png b/img/completeschema.png new file mode 100644 index 0000000..0c41796 Binary files /dev/null and b/img/completeschema.png differ diff --git a/img/frequencydet_zoomout_sf12bw125.png b/img/frequencydet_zoomout_sf12bw125.png new file mode 100644 index 0000000..41dd466 Binary files /dev/null and b/img/frequencydet_zoomout_sf12bw125.png differ diff --git a/layers/__init__.py b/layers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/layers/loraphy.py b/layers/loraphy.py new file mode 100644 index 0000000..008f9e8 --- /dev/null +++ b/layers/loraphy.py @@ -0,0 +1,657 @@ +# -*- coding: utf-8 -*- + +# LoRa Scapy layers +# Copyright (C) 2020 Sebastien Dudek (@FlUxIuS) at @PentHertz + +from scapy.all import * + +class FCtrl_DownLink(Packet): + name = "FCtrl_DownLink" + fields_desc = [BitField("ADR", 0, 1), + BitField("ADRACKReq", 0, 1), + BitField("ACK", 0, 1), + BitField("FPending", 0, 1), + BitFieldLenField("FOptsLen", 0, 4)] + + def extract_padding(self, p): + return "", p + + +class FCtrl_UpLink(Packet): + name = "FCtrl_UpLink" + fields_desc = [BitField("ADR", 0, 1), + BitField("ADRACKReq", 0, 1), + BitField("ACK", 0, 1), + BitField("ClassB", 0, 1), + BitFieldLenField("FOptsLen", 0, 4)] + + def extract_padding(self, p): + return "", p + + +class DevAddrElem(Packet): + name = "DevAddrElem" + fields_desc = [XByteField("NwkID", 0x0), + LEX3BytesField("NwkAddr", b"\x00"*3)] + + def extract_padding(self, p): + return "", p + + +CIDs_up = {0x01: "ResetInd", + 0x02: "LinkCheckReq", + 0x03: "LinkADRReq", + 0x04: "DutyCycleReq", + 0x05: "RXParamSetupReq", + 0x06: "DevStatusReq", + 0x07: "NewChannelReq", + 0x08: "RXTimingSetupReq", + 0x09: "TxParamSetupReq", # LoRa 1.1 specs from here + 0x0A: "DlChannelReq", + 0x0B: "RekeyInd", + 0x0C: "ADRParamSetupReq", + 0x0D: "DeviceTimeReq", + 0x0E: "ForceRejoinReq", + 0x0F: "RejoinParamSetupReq"} # end of LoRa 1.1 specs + + +CIDs_down = {0x01: "ResetConf", + 0x02: "LinkCheckAns", + 0x03: "LinkADRAns", + 0x04: "DutyCycleAns", + 0x05: "RXParamSetupAns", + 0x06: "DevStatusAns", + 0x07: "NewChannelAns", + 0x08: "RXTimingSetupAns", + 0x09: "TxParamSetupAns", # LoRa 1.1 specs from here + 0x0A: "DlChannelAns", + 0x0B: "RekeyConf", + 0x0C: "ADRParamSetupAns", + 0x0D: "DeviceTimeAns", + 0x0F: "RejoinParamSetupAns"} # end of LoRa 1.1 specs + + +class ResetInd(Packet): + name = "ResetInd" + fields_desc = [ByteField("Dev_version", 0)] + + +class ResetConf(Packet): + name = "ResetConf" + fields_desc = [ByteField("Serv_version", 0)] + + +class LinkCheckReq(Packet): + name = "LinkCheckReq" + fields_desc = [] + + +class LinkCheckAns(Packet): + name = "LinkCheckAns" + fields_desc = [ByteField("Margin", 0), + ByteField("GwCnt", 0)] + + +class DataRate_TXPower(Packet): + name = "DataRate_TXPower" + fields_desc = [XBitField("DataRate", 0, 4), + XBitField("TXPower", 0, 4)] + + +class Redundancy(Packet): + name = "Redundancy" + fields_desc = [XBitField("RFU", 0, 1), + XBitField("ChMaskCntl", 0, 3), + XBitField("NbTrans", 0, 4)] + + +class LinkADRReq(Packet): + name = "LinkADRReq" + fields_desc = [DataRate_TXPower, + XShortField("ChMask", 0), + Redundancy] + + +class LinkADRAns_Status(Packet): + name = "LinkADRAns_Status" + fields_desc = [BitField("RFU", 0, 5), + BitField("PowerACK", 0, 1), + BitField("ChannelMaskACK", 0, 1)] + + +class LinkADRAns(Packet): + name = "LinkADRAns" + fields_desc = [LinkADRAns_Status] + + +class DutyCyclePL(Packet): + name = "DutyCyclePL" + fields_desc = [BitField("MaxDCycle", 0, 4)] + + +class DutyCycleReq(Packet): + name = "DutyCycleReq" + fields_desc = [DutyCyclePL] + + +class DutyCycleAns(Packet): + name = "DutyCycleAns" + fields_desc = [] + + +class DLsettings(Packet): + name = "DLsettings" + fields_desc = [BitField("RFU", 0, 1), + BitField("RX1DRoffset", 0, 3), + BitField("RX2DataRate", 0, 4)] + + +class RXParamSetupReq(Packet): + name = "RXParamSetupReq" + fields_desc = [DLsettings, + X3BytesField("Frequency", 0)] + + +class RXParamSetupAns_Status(Packet): + name = "RXParamSetupAns_Status" + fields_desc = [XBitField("RFU", 0, 5), + BitField("RX1DRoffsetACK", 0, 1), + BitField("RX2DatarateACK", 0, 1), + BitField("ChannelACK", 0, 1)] + + +class RXParamSetupAns(Packet): + name = "RXParamSetupAns" + fields_desc = [RXParamSetupAns_Status] + +Battery_state = {0: "End-device connected to external source", + 255: "Battery level unknown"} + + +class DevStatusReq(Packet): + name = "DevStatusReq" + fields_desc = [ByteEnumField("Battery", 0, Battery_state), + ByteField("Margin", 0)] + + +class DevStatusAns_Status(Packet): + name = "DevStatusAns_Status" + fields_desc = [XBitField("RFU", 0, 2), + XBitField("Margin", 0, 6)] + + +class DevStatusAns(Packet): + name = "DevStatusAns" + fields_desc = [DevStatusAns_Status] + + +class DrRange(Packet): + name = "DrRange" + fields_desc = [XBitField("MaxDR", 0, 4), + XBitField("MinDR", 0, 4)] + + +class NewChannelReq(Packet): + name = "NewChannelReq" + fields_desc = [ByteField("ChIndex", 0), + X3BytesField("Freq", 0), + DrRange] + + +class NewChannelAns_Status(Packet): + name = "NewChannelAns_Status" + fields_desc = [XBitField("RFU", 0, 6), + BitField("Dataraterangeok", 0, 1), + BitField("Channelfrequencyok", 0, 1)] + + +class NewChannelAns(Packet): + name = "NewChannelAns" + fields_desc = [NewChannelAns_Status] + + +class RXTimingSetupReq_Settings(Packet): + name = "RXTimingSetupReq_Settings" + fields_desc = [XBitField("RFU", 0, 4), + XBitField("Del", 0, 4)] + + +class RXTimingSetupReq(Packet): + name = "RXTimingSetupReq" + fields_desc = [RXTimingSetupReq_Settings] + + +class RXTimingSetupAns(Packet): + name = "RXTimingSetupAns" + fields_desc = [] + + +# Specific commands for LoRa 1.1 here + + +MaxEIRPs = {0: "8 dbm", + 1: "10 dbm", + 2: "12 dbm", + 3: "13 dbm", + 4: "14 dbm", + 5: "16 dbm", + 6: "18 dbm", + 7: "20 dbm", + 8: "21 dbm", + 9: "24 dbm", + 10: "26 dbm", + 11: "27 dbm", + 12: "29 dbm", + 13: "30 dbm", + 14: "33 dbm", + 15: "36 dbm"} + + +DwellTimes = {0: "No limit", + 1: "400 ms"} + + +class EIRP_DwellTime(Packet): + name = "EIRP_DwellTime" + fields_desc = [BitField("RFU", 0b0, 2), + BitEnumField("DownlinkDwellTime", 0b0, 1, DwellTimes), + BitEnumField("UplinkDwellTime", 0b0, 1, DwellTimes), + BitEnumField("MaxEIRP", 0b0000, 4, MaxEIRPs)] + + +class TxParamSetupReq(Packet): + name = "TxParamSetupReq" + fields_desc = [EIRP_DwellTime] + + +class TxParamSetupAns(Packet): + name = "TxParamSetupAns" + fields_desc = [] + + +class DlChannelReq(Packet): + name = "DlChannelReq" + fields_desc = [ByteField("ChIndex", 0), + X3BytesField("Freq", 0)] + + +class DlChannelAns(Packet): + name = "DlChannelAns" + fields_desc = [ByteField("Status", 0)] + + +class DevLoraWANversion(Packet): + name = "DevLoraWANversion" + fields_desc = [BitField("RFU", 0b0000, 4), + BitField("Minor", 0b0001, 4)] + + +class RekeyInd(Packet): + name = "RekeyInd" + fields_desc = [PacketListField("LoRaWANversion", b"", + DevLoraWANversion, length_from=lambda pkt:1)] + + +class RekeyConf(Packet): + name = "RekeyConf" + fields_desc = [ByteField("ServerVersion", 0)] + + +class ADRparam(Packet): + name = "ADRparam" + fields_desc = [BitField("Limit_exp", 0b0000, 4), + BitField("Delay_exp", 0b0000, 4)] + + +class ADRParamSetupReq(Packet): + name = "ADRParamSetupReq" + fields_desc = [ADRparam] + + +class ADRParamSetupAns(Packet): + name = "ADRParamSetupReq" + fields_desc = [] + + +class DeviceTimeReq(Packet): + name = "DeviceTimeReq" + fields_desc = [] + + +class DeviceTimeAns(Packet): + name = "DeviceTimeAns" + fields_desc = [IntField("SecondsSinceEpoch", 0), + ByteField("FracSecond", 0x00)] + + +class ForceRejoinReq(Packet): + name ="ForceRejoinReq" + fields_desc = [BitField("RFU", 0, 2), + BitField("Period", 0, 3), + BitField("Max_Retries", 0, 3), + BitField("RFU", 0, 1), + BitField("RejoinType", 0, 3), + BitField("DR", 0, 4)] + + +class RejoinParamSetupReq(Packet): + name = "RejoinParamSetupReq" + fields_desc = [BitField("MaxTimeN", 0, 4), + BitField("MaxCountN", 0, 4)] + + +class RejoinParamSetupAns(Packet): + name = "RejoinParamSetupAns" + fields_desc = [BitField("RFU", 0, 7), + BitField("TimeOK", 0, 1)] + + +# End of specific 1.1 commands + + +class MACCommand_up(Packet): + name = "MACCommand_up" + fields_desc = [ByteEnumField("CID", 0, CIDs_up), + ConditionalField(PacketListField("Reset", b"", + ResetInd, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x01)), + ConditionalField(PacketListField("LinkCheck", b"", + LinkCheckReq, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x02)), + ConditionalField(PacketListField("LinkADR", b"", + LinkADRReq, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x03)), + ConditionalField(PacketListField("DutyCycle", b"", + DutyCycleReq, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x04)), + ConditionalField(PacketListField("RXParamSetup", b"", + RXParamSetupReq, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x05)), + ConditionalField(PacketListField("DevStatus", b"", + DevStatusReq, + length_from=lambda pkt:2), + lambda pkt:(pkt.CID == 0x06)), + ConditionalField(PacketListField("NewChannel", b"", + NewChannelReq, + length_from=lambda pkt:5), + lambda pkt:(pkt.CID == 0x07)), + ConditionalField(PacketListField("RXTimingSetup", b"", + RXTimingSetupReq, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x08)), + ConditionalField(PacketListField("TxParamSetup", b"", # specific to 1.1 from here + TxParamSetupReq, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x09)), + ConditionalField(PacketListField("DlChannel", b"", + DlChannelReq, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x0A)), + ConditionalField(PacketListField("Rekey", b"", + RekeyInd, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0B)), + ConditionalField(PacketListField("ADRParamSetup", b"", + ADRParamSetupReq, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0C)), + ConditionalField(PacketListField("DeviceTime", b"", + DeviceTimeReq, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x0D)), + ConditionalField(PacketListField("ForceRejoin", b"", + ForceRejoinReq, + length_from=lambda pkt:2), + lambda pkt:(pkt.CID == 0x0E)), + ConditionalField(PacketListField("RejoinParamSetup", b"", + RejoinParamSetupReq, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0F))] + + def extract_padding(self, p): + return "", p + + +class MACCommand_down(Packet): + name = "MACCommand_down" + fields_desc = [ByteEnumField("CID", 0, CIDs_up), + ConditionalField(PacketListField("Reset", b"", + ResetConf, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x01)), + ConditionalField(PacketListField("LinkCheck", b"", + LinkCheckAns, + length_from=lambda pkt:2), + lambda pkt:(pkt.CID == 0x02)), + ConditionalField(PacketListField("LinkADR", b"", + LinkADRAns, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x03)), + ConditionalField(PacketListField("DutyCycle", b"", + DutyCycleAns, + length_from=lambda pkt:4), + lambda pkt:(pkt.CID == 0x04)), + ConditionalField(PacketListField("RXParamSetup", b"", + RXParamSetupAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x05)), + ConditionalField(PacketListField("DevStatusAns", b"", + RXParamSetupAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x06)), + ConditionalField(PacketListField("NewChannel", b"", + NewChannelAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x07)), + ConditionalField(PacketListField("RXTimingSetup", b"", + RXTimingSetupAns, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x08)), + ConditionalField(PacketListField("TxParamSetup", b"", + TxParamSetupAns, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x09)), + ConditionalField(PacketListField("DlChannel", b"", + DlChannelAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0A)), + ConditionalField(PacketListField("Rekey", b"", + RekeyConf, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0B)), + ConditionalField(PacketListField("ADRParamSetup", b"", + ADRParamSetupAns, + length_from=lambda pkt:0), + lambda pkt:(pkt.CID == 0x0C)), + ConditionalField(PacketListField("DeviceTime", b"", + DeviceTimeAns, + length_from=lambda pkt:5), + lambda pkt:(pkt.CID == 0x0D)), + ConditionalField(PacketListField("RejoinParamSetup", b"", + RejoinParamSetupAns, + length_from=lambda pkt:1), + lambda pkt:(pkt.CID == 0x0F))] + + def extract_padding(self, p): + return "", p + +class FOpts(Packet): + name = "FOpts" + fields_desc = [ConditionalField(PacketListField("FOpts_up", b"", + MACCommand_up, # piggybacked MAC Command for uplink + length_from=lambda pkt:pkt.FCtrl[0].FOptsLen), + lambda pkt:(pkt.FCtrl[0].FOptsLen > 0 + and pkt.MType & 0b1 == 0 + and pkt.MType >= 0b010)), + ConditionalField(PacketListField("FOpts_down", b"", + MACCommand_down, # piggybacked MAC Command for downlink + length_from=lambda pkt:pkt.FCtrl[0].FOptsLen), + lambda pkt:(pkt.FCtrl[0].FOptsLen > 0 + and pkt.MType & 0b1 == 1 + and pkt.MType <= 0b101))] + +def FOptsShow(pkt): + try: + if pkt.FCtrl[0].FOptsLen > 0 and pkt.MType & 0b1 == 0 and pkt.MType >= 0b010: + return True + elif pkt.FCtrl[0].FOptsLen > 0 and pkt.MType & 0b1 == 1 and pkt.MType <= 0b101: + return True + return False + except: + return False + +class FHDR(Packet): + name = "FHDR" + fields_desc = [ConditionalField(PacketListField("DevAddr", b"", DevAddrElem, + length_from=lambda pkt:4), + lambda pkt:(pkt.MType >= 0b010 + and pkt.MType <= 0b101)), + ConditionalField(PacketListField("FCtrl", b"", + FCtrl_DownLink, + length_from=lambda pkt:1), + lambda pkt:(pkt.MType & 0b1 == 1 + and pkt.MType <= 0b101)), + ConditionalField(PacketListField("FCtrl", b"", + FCtrl_UpLink, + length_from=lambda pkt:1), + lambda pkt:(pkt.MType & 0b1 == 0 + and pkt.MType >= 0b010)), + ConditionalField(LEShortField("FCnt", 0), + lambda pkt:(pkt.MType >= 0b010 + and pkt.MType <= 0b101)), + ConditionalField(PacketListField("FOpts_up", b"", + MACCommand_up, + length_from=lambda pkt:pkt.FCtrl[0].FOptsLen), + lambda pkt:FOptsShow(pkt)), + ConditionalField(PacketListField("FOpts_down", b"", + MACCommand_down, + length_from=lambda pkt:pkt.FCtrl[0].FOptsLen), + lambda pkt:FOptsShow(pkt))] + + +FPorts = {0: "NwkSKey"} # anything else is AppSKey + + +JoinReqTypes = {0xFF: "Join-request", + 0x00: "Rejoin-request type 0", + 0x01: "Rejoin-request type 1", + 0x02: "Rejoin-request type 2"} + + +class Join_Request(Packet): + name = "Join_Request" + fields_desc = [StrFixedLenField("AppEUI", b"\x00" * 8, 8), + StrFixedLenField("DevEUI", b"\00" * 8, 8), + LEShortField("DevNonce", 0x0000)] + + +class DLsettings(Packet): + name = "DLsettings" + fields_desc = [BitField("OptNeg", 0, 1), + XBitField("RX1DRoffset", 0, 3), + XBitField("RX2_Data_rate", 0, 4)] + + +class Join_Accept(Packet): + name = "Join_Accept" + dcflist = False + fields_desc = [LEX3BytesField("JoinAppNonce", 0), + LEX3BytesField("NetID", 0), + XLEIntField("DevAddr", 0), + DLsettings, + XByteField("RxDelay", 0), + ConditionalField(StrFixedLenField("CFList", b"\x00" * 16 , 16), + lambda pkt:(Join_Accept.dcflist is True))] + + def extract_padding(self, p): + return "", p + + def __init__(self, packet=""): # CFlist calculated with on rest packet len + if len(packet) > 18: + Join_Accept.dcflist = True + return super(Join_Accept, self).__init__(packet) + + +RejoinType = {0: "NetID+DevEUI", + 1: "JoinEUI+DevEUI", + 2: "NetID+DevEUI"} + + +def RejoinReq(Packet): # LoRa 1.1 specs + name = "RejoinReq" + fields_desc = [ByteField("Type", 0), + X3BytesField("NetID", 0), + StrFixedLenField("DevEUI", b"\x00" * 8), + XShortField("RJcount0", 0)] + + +class FRMPayload(Packet): + name = "FRMPayload" + fields_desc = [ConditionalField(StrField("DataPayload", 0, remain=4), # Downlink + lambda pkt:(pkt.MType == 0b101 + or pkt.MType == 0b011)), + ConditionalField(StrField("DataPayload", 0, remain=6), # Uplink + lambda pkt:(pkt.MType == 0b100 + or pkt.MType == 0b010)), + ConditionalField(PacketListField("Join_Request_Field", b"", + Join_Request, + length_from=lambda pkt:18), + lambda pkt:(pkt.MType == 0b000)), + ConditionalField(PacketListField("Join_Accept_Field", b"", + Join_Accept, + count_from=lambda pkt:1), + lambda pkt:(pkt.MType == 0b001 + and LoRa.encrypted is False)), + ConditionalField(StrField("Join_Accept_Encrypted", 0), + lambda pkt:(pkt.MType == 0b001 and LoRa.encrypted is True))] + + +class MACPayload(Packet): + name = "MACPayload" + fields_desc = [FHDR, + ConditionalField(ByteEnumField("FPort", 0, FPorts), + lambda pkt:(pkt.MType >= 0b010 and pkt.MType <= 0b101)), + FRMPayload] + + +MTypes = {0b000: "Join-request", + 0b001: "Join-accept", + 0b010: "Unconfirmed Data Up", + 0b011: "Unconfirmed Data Down", + 0b100: "Confirmed Data Up", + 0b101: "Confirmed Data Down", + 0b110: "Rejoin-request", # Only in LoRa 1.1 specs + 0b111: "Proprietary"} + + +class MHDR(Packet): # same for 1.0 and 1.1 + name = "MHDR" + fields_desc = [BitEnumField("MType", 0b000, 3, MTypes), + BitField("RFU", 0b000, 3), + BitField("Major", 0b00, 2)] + + +class PHYPayload(Packet): + name = "PHYPayload" + fields_desc = [MHDR, + MACPayload, + ConditionalField(XIntField("MIC", 0), + lambda pkt:(pkt.MType != 0b001 + or LoRa.encrypted is False))] + + +class LoRa(Packet): # default frame (unclear specs => taken from https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5677147/) + name = "LoRa" + version = "1.1" # default version to parse + encrypted = True + fields_desc = [XBitField("Preamble", 0, 4), + XBitField("PHDR", 0, 16), + XBitField("PHDR_CRC", 0, 4), + PHYPayload, + ConditionalField(XShortField("CRC", 0), + lambda pkt:(pkt.MType & 0b1 == 0))] diff --git a/lutil/__init__.py b/lutil/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lutil/crypto.py b/lutil/crypto.py new file mode 100644 index 0000000..83f136b --- /dev/null +++ b/lutil/crypto.py @@ -0,0 +1,64 @@ +# LoRa Cryto utils +# Copyright (C) 2020 Sebastien Dudek (@FlUxIuS) at @PentHertz + +from Crypto.Cipher import AES +from Crypto.Hash import CMAC +import binascii + +def JoinAcceptPayload_decrypt(key, hexpkt): + """ + Decrypt Join Accept payloads + In(1): String 128 bits key + In(2): String packet + Out: String decrypted Join accept packet + """ + payload = hexpkt[4:] + cipher = AES.new(key, AES.MODE_ECB) + return cipher.encrypt(payload) # logic right? :D + +def JoinAcceptPayload_encrypt(key, hexpkt): + """ + Encrypts Join Accept Payload + In(1): String 128 bits key + In(2): String packet + Out: String encrypted Join accept payload + """ + payload = hexpkt[4:] + cipher = AES.new(key, AES.MODE_ECB) + return cipher.decrypt(payload) + +def getPHY_CMAC(key, hexpkt, direction=1): + """ + Compute MIC with AES CMAC + In(1): String 128 bits key for CMAC + In(2): hexstring of the packet + In(3): Direction (1: network, 0: end device) + Out: Hexdigest of computed MIC + """ + lowoff = -4 + if direction == 0: + lowoff = -6 # skip the CRC + payload = hexpkt[3:lowoff] + cobj = CMAC.new(key, ciphermod=AES) + toret = cobj.update(payload).hexdigest() + return toret[:8] + +def checkMIC(key, hexpkt, direction=1): + """ + Check MIC in the packet + In(1): String key for CMAC + In(2): String of the packet + In(3): Direction (1: network, 0: end device) + Out: True if key is correct, False otherwise + """ + mic = hexpkt[-4:] + if direction == 0: + mic = hexpkt[-6:-2] # skip the CRC + try: + binascii.unhexlify(mic) + except: + mic = binascii.hexlify(mic) + cmic = getPHY_CMAC(key, hexpkt) + return (mic == cmic) + +#TODO: more helpers