Release commit

This commit is contained in:
FlUxIuS 2020-01-15 10:24:15 +01:00
commit e8c1bd4127
11 changed files with 1792 additions and 0 deletions

35
LoRa_PHYDecode.py Normal file
View File

@ -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)

131
README.md Normal file
View File

@ -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
<LoRa Preamble=0x1 PHDR=0x631e PHDR_CRC=0x0 MType=Unconfirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=0 |>] FCnt=0 FPort=2 DataPayload='i\x06D\x94\x97\x08\xce!\xd9' MIC=0x4b516899 CRC=0x96e1 |>
...
<LoRa Preamble=0x1 PHDR=0x631e PHDR_CRC=0x0 MType=Unconfirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=0 |>] FCnt=0 FPort=2 DataPayload='penthertz' MIC=0x20a5fcba CRC=0xcdc |>
<LoRa Preamble=0x0 PHDR=0xd30c PHDR_CRC=0x0 MType=Confirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=1 |>] FCnt=0 FOpts_up=[<MACCommand_up CID=LinkCheckReq LinkCheck=[''] |>] FOpts_down=[<MACCommand_down CID=222 |>] FPort=92 DataPayload='' MIC=0x31c753f |>
<LoRa Preamble=0x0 PHDR=0xd30c PHDR_CRC=0x0 MType=Confirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=1 |>] FCnt=0 FOpts_up=[<MACCommand_up CID=LinkCheckReq LinkCheck=[''] |>] FOpts_down=[<MACCommand_down CID=222 |>] 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
<LoRa Join_Request_Field=[''] |>
```
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
<LoRa Preamble=0x0 PHDR=0x0 PHDR_CRC=0x0 MType=Join-request RFU=0 Major=0 Join_Request_Field=[<Join_Request AppEUI='lovecafe' DevEUI='meeetoo' DevNonce=26985 |>] 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)
<Join_Accept JoinAppNonce=0x6fe14a NetID=0x10203 DevAddr=0x68e8cb1 OptNeg=0 RX1DRoffset=0x0 RX2_Data_rate=0x0 RxDelay=0x0 |<Padding load='\xbejsu' |>>
>>> 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 :)!

257
USRPB20xmini_LoRa_receive_UDP.py Executable file
View File

@ -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()

Binary file not shown.

View File

@ -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

BIN
img/completeschema.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

0
layers/__init__.py Normal file
View File

657
layers/loraphy.py Normal file
View File

@ -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))]

0
lutil/__init__.py Normal file
View File

64
lutil/crypto.py Normal file
View File

@ -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