├── lora ├── __init__.py ├── payload.py └── crypto.py ├── pyproject.toml ├── tests ├── fixtures │ ├── 14000122 │ │ ├── payload_encrypted-1.txt │ │ ├── payload_encrypted-2.txt │ │ ├── payload_encrypted-3.txt │ │ ├── payload_encrypted-4.txt │ │ ├── payload_encrypted-5.txt │ │ ├── payload_encrypted-6.txt │ │ ├── payload_encrypted-7.txt │ │ ├── payload_encrypted-8.txt │ │ ├── payload_encrypted-9.txt │ │ ├── key.hex │ │ ├── payload_encrypted-10.txt │ │ ├── payload_encrypted-11.txt │ │ ├── payload_encrypted-12.txt │ │ ├── payload_encrypted-13.txt │ │ ├── payload_encrypted-14.txt │ │ ├── payload_encrypted-15.txt │ │ ├── payload_encrypted-16.txt │ │ ├── payload_encrypted-2.xml │ │ ├── payload_plaintext-1.xml │ │ ├── payload_encrypted-1.xml │ │ ├── payload_encrypted-3.xml │ │ ├── payload_encrypted-4.xml │ │ ├── payload_encrypted-5.xml │ │ ├── payload_encrypted-6.xml │ │ ├── payload_encrypted-7.xml │ │ ├── payload_plaintext-2.xml │ │ ├── payload_plaintext-3.xml │ │ ├── payload_plaintext-4.xml │ │ ├── payload_plaintext-5.xml │ │ ├── payload_plaintext-7.xml │ │ ├── payload_plaintext-8.xml │ │ ├── payload_encrypted-8.xml │ │ ├── payload_encrypted-9.xml │ │ ├── payload_plaintext-6.xml │ │ ├── payload_plaintext-9.xml │ │ ├── payload_plaintext-10.xml │ │ ├── payload_encrypted-10.xml │ │ ├── payload_encrypted-11.xml │ │ ├── payload_encrypted-12.xml │ │ ├── payload_encrypted-13.xml │ │ ├── payload_encrypted-14.xml │ │ ├── payload_plaintext-11.xml │ │ ├── payload_plaintext-12.xml │ │ ├── payload_plaintext-13.xml │ │ ├── payload_plaintext-14.xml │ │ ├── payload_encrypted-15.xml │ │ ├── payload_encrypted-16.xml │ │ ├── payload_plaintext-15.xml │ │ └── payload_plaintext-16.xml │ ├── 000015E4 │ │ ├── key.hex │ │ ├── payload_1.txt │ │ └── payload_1.xml │ └── README.md ├── test_crypto.py ├── import_fixtures.py ├── test_lorapayload.py └── 14000122.txt ├── .isort.cfg ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── tox.ini ├── CHANGELOG.md ├── .gitignore ├── LICENSE ├── setup.py └── README.md /lora/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = "2.0.0" 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-1.txt: -------------------------------------------------------------------------------- 1 | 0b 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-2.txt: -------------------------------------------------------------------------------- 1 | 0a 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-3.txt: -------------------------------------------------------------------------------- 1 | 4321 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-4.txt: -------------------------------------------------------------------------------- 1 | 1234 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-5.txt: -------------------------------------------------------------------------------- 1 | 054321 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-6.txt: -------------------------------------------------------------------------------- 1 | 012345 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-7.txt: -------------------------------------------------------------------------------- 1 | 012345 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-8.txt: -------------------------------------------------------------------------------- 1 | 654321 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-9.txt: -------------------------------------------------------------------------------- 1 | 123456 2 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length = 120 3 | skip = .tox 4 | -------------------------------------------------------------------------------- /tests/fixtures/000015E4/key.hex: -------------------------------------------------------------------------------- 1 | 112233445566778899AABBCCDDEEFFE4 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/key.hex: -------------------------------------------------------------------------------- 1 | C6FB9E3C87AC393B43174EFA8F832195 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-10.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdef4321 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-11.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdef1234 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-12.txt: -------------------------------------------------------------------------------- 1 | 00123456789abcdef54321 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-13.txt: -------------------------------------------------------------------------------- 1 | 00123456789abcdef12345 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-14.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdef654321 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-15.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdef0123456789abcdef 2 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-16.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdef0123456789abcdef 2 | -------------------------------------------------------------------------------- /tests/fixtures/000015E4/payload_1.txt: -------------------------------------------------------------------------------- 1 | 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /tests/fixtures/README.md: -------------------------------------------------------------------------------- 1 | # LoRa Test XMLs 2 | 3 | Each subdirectory is the hex encoded DevAddr of a device transmitting some messages and contains: 4 | 5 | - `key.hex` Hex encoded AppSKey 6 | - One or more `payload_.xml`/`payload_.txt` pairs. 7 | - Each XML file contains the contents of the POST body actility/thingpark makes to the defined HTTP POST receiver. 8 | - Each txt file contains the (hex encoded) plaintext payload originally transmitted. 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | args_are_paths = false 3 | envlist = py27,py34,py35,py36,py37,flake8,isort,black 4 | 5 | [testenv] 6 | changedir=tests 7 | commands = discover 8 | deps = 9 | discover 10 | cryptography==3.3.2 11 | 12 | [testenv:flake8] 13 | basepython = python3.6 14 | deps = flake8 15 | commands = flake8 16 | 17 | [flake8] 18 | ignore = F401,E731 19 | max-line-length = 120 20 | 21 | [testenv:isort] 22 | deps = isort==4.2.15 23 | basepython = python3.6 24 | commands = isort --recursive --diff --check lora/ tests/ 25 | 26 | [testenv:black] 27 | basepython = python3.6 28 | passenv = LC_CTYPE 29 | deps = black==18.9b0 30 | commands = black --check . 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 (2024-09-21) 4 | - Update cryptography dependency to 43.0.1 5 | - Drop support for python 2.7, 3.6 and 3.7 6 | - Add support for python 3.9, 3.10, 3.11 7 | 8 | ## 1.1.9 (2021-05-09) 9 | - Update cryptography dependency to 3.3.2 10 | 11 | ## 1.1.8 (2020-01-08) 12 | - Update cryptography dependency to 3.2 13 | 14 | ## 1.1.7 (2019-01-26) 15 | - Update cryptography dependency to 2.5 16 | 17 | ## 1.1.6 (2018-09-12) 18 | - Adopt black 19 | - Update cryptography dependency to 2.3.1 20 | 21 | ## 1.1.5 (2017-10-13) 22 | - Update cryptography dependency to 2.1.1 23 | 24 | ## 1.1.4 (2016-10-03) 25 | - Update cryptography dependency to 1.5.2 26 | -------------------------------------------------------------------------------- /tests/test_crypto.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lora.crypto import generate_appskey, loramac_decrypt 4 | 5 | 6 | class TestCrypto(unittest.TestCase): 7 | def test_loramac_decrypt(self): 8 | key = "271E403DF4225EEF7E90836494A5B345" 9 | dev_addr = "000015E4" 10 | 11 | payloads = ((0, "73100b90"), (1, "68d388f0"), (2, "0a12e808"), (3, "e3413bee")) 12 | expected = "cafebabe" 13 | 14 | for sequence_counter, payload_hex in payloads: 15 | plaintext_ints = loramac_decrypt(payload_hex, sequence_counter, key, dev_addr) 16 | plaintext_hex = "".join("{:02x}".format(x) for x in plaintext_ints) 17 | 18 | self.assertEquals(plaintext_hex, expected) 19 | 20 | def test_appskey(self): 21 | key = generate_appskey() 22 | self.assertEquals(len(key), 32) 23 | 24 | self.assertNotEquals(key, generate_appskey()) 25 | self.assertNotEquals(generate_appskey(), generate_appskey()) 26 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 84 7 | 1 8 | 4 9 | 90 10 | 95 11 | 5c4e5158 12 | 0059AC01 13 | -27.000000 14 | 8.250000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 8.250000 28 | -27.605556 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 81 7 | 1 8 | 4 9 | 87 10 | 0b 11 | 6b9755a8 12 | 0059AC01 13 | -28.000000 14 | 8.000000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 8.000000 28 | -28.638920 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 82 7 | 1 8 | 4 9 | 88 10 | c8 11 | 21f624d0 12 | 0059AC01 13 | -28.000000 14 | 10.000000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.000000 28 | -28.413927 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 86 7 | 1 8 | 4 9 | 92 10 | a167 11 | b1f629fc 12 | 0059AC01 13 | -27.000000 14 | 8.500000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 8.500000 28 | -27.573822 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 88 7 | 1 8 | 4 9 | 94 10 | 5c74 11 | 141f79e2 12 | 0059AC01 13 | -28.000000 14 | 9.750000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 9.750000 28 | -28.437258 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 90 7 | 1 8 | 4 9 | 96 10 | 72c727 11 | ed898137 12 | 0059AC01 13 | -23.000000 14 | 8.750000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -23.000000 27 | 8.750000 28 | -23.543650 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 92 7 | 1 8 | 4 9 | 98 10 | 58ba24 11 | 426aa579 12 | 0059AC01 13 | -27.000000 14 | 8.000000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 8.000000 28 | -27.638920 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 94 7 | 1 8 | 4 9 | 100 10 | 835db5 11 | e3e0affa 12 | 0059AC01 13 | -29.000000 14 | 9.750000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -29.000000 27 | 9.750000 28 | -29.437258 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 83 7 | 1 8 | 4 9 | 89 10 | 0a 11 | 2426cec1 12 | 0059AC01 13 | -28.000000 14 | 10.250000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.250000 28 | -28.391785 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 85 7 | 1 8 | 4 9 | 91 10 | 4321 11 | 5bcc5f32 12 | 0059AC01 13 | -28.000000 14 | 9.750000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 9.750000 28 | -28.437258 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 87 7 | 1 8 | 4 9 | 93 10 | 1234 11 | 6eee9ca2 12 | 0059AC01 13 | -28.000000 14 | 9.750000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 9.750000 28 | -28.437258 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 89 7 | 1 8 | 4 9 | 95 10 | 054321 11 | 34c15567 12 | 0059AC01 13 | -29.000000 14 | 8.250000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -29.000000 27 | 8.250000 28 | -29.605556 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 93 7 | 1 8 | 4 9 | 99 10 | 012345 11 | 725ae7e7 12 | 0059AC01 13 | -26.000000 14 | 7.750000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -26.000000 27 | 7.750000 28 | -26.673985 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 95 7 | 1 8 | 4 9 | 101 10 | 654321 11 | ff025f1d 12 | 0059AC01 13 | -28.000000 14 | 9.250000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 9.250000 28 | -28.487720 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 96 7 | 1 8 | 4 9 | 102 10 | 30af33 11 | 3569a29a 12 | 0059AC01 13 | -28.000000 14 | 10.000000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.000000 28 | -28.413927 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 98 7 | 1 8 | 4 9 | 104 10 | eb496e 11 | ab990d15 12 | 0059AC01 13 | -28.000000 14 | 10.750000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.750000 28 | -28.350851 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 91 7 | 1 8 | 4 9 | 97 10 | 012345 11 | 24f44449 12 | 0059AC01 13 | -27.000000 14 | 10.250000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 10.250000 28 | -27.391785 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 97 7 | 1 8 | 4 9 | 103 10 | 123456 11 | 4b8d5913 12 | 0059AC01 13 | -28.000000 14 | 10.250000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.250000 28 | -28.391785 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 99 7 | 1 8 | 4 9 | 105 10 | 0123456789abcdef4321 11 | 0fde3254 12 | 0059AC01 13 | -27.000000 14 | 8.250000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 8.250000 28 | -27.605556 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 100 7 | 1 8 | 4 9 | 106 10 | 551edc7c807aa97e0efc 11 | 0bb0327d 12 | 0059AC01 13 | -27.000000 14 | 8.250000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 8.250000 28 | -27.605556 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-11.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 102 7 | 1 8 | 4 9 | 108 10 | 33500e0201fd25456c40 11 | 6d3f06ce 12 | 0059AC01 13 | -28.000000 14 | 10.500000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.500000 28 | -28.370777 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-12.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 104 7 | 1 8 | 4 9 | 110 10 | 67f93000c6f907e477a0d7 11 | 7693f5f0 12 | 0059AC01 13 | -29.000000 14 | 10.500000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -29.000000 27 | 10.500000 28 | -29.370777 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-13.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 106 7 | 1 8 | 4 9 | 112 10 | b8060d0f29499ca3081915 11 | 2569a925 12 | 0059AC01 13 | -27.000000 14 | 9.000000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 9.000000 28 | -27.514969 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-14.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 108 7 | 1 8 | 4 9 | 114 10 | 47aeb5c4712c63e9197fd5 11 | 2b53deee 12 | 0059AC01 13 | -29.000000 14 | 9.000000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -29.000000 27 | 9.000000 28 | -29.514969 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-11.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 101 7 | 1 8 | 4 9 | 107 10 | 0123456789abcdef1234 11 | ba0c4059 12 | 0059AC01 13 | -28.000000 14 | 9.750000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 9.750000 28 | -28.437258 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-12.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 103 7 | 1 8 | 4 9 | 109 10 | 00123456789abcdef54321 11 | 5d55cdf4 12 | 0059AC01 13 | -27.000000 14 | 8.500000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 8.500000 28 | -27.573822 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-13.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 105 7 | 1 8 | 4 9 | 111 10 | 00123456789abcdef12345 11 | 37652449 12 | 0059AC01 13 | -28.000000 14 | 10.750000 15 | 7 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.750000 28 | -28.350851 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-14.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 107 7 | 1 8 | 4 9 | 113 10 | 0123456789abcdef654321 11 | 21980e49 12 | 0059AC01 13 | -27.000000 14 | 9.500000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -27.000000 27 | 9.500000 28 | -27.461836 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-15.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 110 7 | 1 8 | 4 9 | 117 10 | 15c8d5678b86edb5349b89b57da71cb8 11 | 5b23d6c6 12 | 0059AC01 13 | -28.000000 14 | 10.250000 15 | 8 16 | G1 17 | LC2 18 | 1 19 | 08050042 20 | 52.070332 21 | 4.478900 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.250000 28 | -28.391785 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_encrypted-16.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 2 6 | 112 7 | 1 8 | 4 9 | 119 10 | c7d9853a545d9789a5e4285c0859d6fb 11 | 6e59af9f 12 | 0059AC01 13 | -28.000000 14 | 10.000000 15 | 7 16 | G1 17 | LC3 18 | 1 19 | 08050042 20 | 52.070332 21 | 4.478900 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 10.000000 28 | -28.413927 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-15.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 109 7 | 1 8 | 4 9 | 115 10 | 0123456789abcdef0123456789abcdef 11 | 09123c47 12 | 0059AC01 13 | -28.000000 14 | 8.750000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070278 21 | 4.478838 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 8.750000 28 | -28.543650 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /tests/fixtures/14000122/payload_plaintext-16.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0059AC0000100222 5 | 1 6 | 111 7 | 1 8 | 4 9 | 118 10 | 0123456789abcdef0123456789abcdef 11 | 711a20ee 12 | 0059AC01 13 | -28.000000 14 | 11.000000 15 | 7 16 | G1 17 | LC1 18 | 1 19 | 08050042 20 | 52.070332 21 | 4.478900 22 | 23 | 24 | 08050042 25 | 0 26 | -28.000000 27 | 11.000000 28 | -28.331957 29 | 30 | 31 | 100006246 32 | {"alr":{"pro":"SMTC/LoRaMote","ver":"1"}} 33 | 0 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jan Pieter Waagmeester 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/fixtures/000015E4/payload_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | F03D2910000015E4 5 | 27 6 | 2 7 | 1 8 | 222 9 | 11daf7a44d5e2bbe557176e9e6c8dae387dd810a04bae42c24f73d333d088e54fae368814b5764e8 10 | 3d8996c4 11 | 00000065 12 | -36.000000 13 | 10.500000 14 | 9 15 | G1 16 | LC3 17 | 1 18 | 29000097 19 | 52.014877 20 | 4.369840 21 | 22 | 23 | 29000097 24 | -36.000000 25 | 10.500000 26 | 27 | 28 | 100001045 29 | {"alr":{"pro":"LORA/genericA.1","ver":"1"}} 30 | 1:test 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | black: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/setup-python@v4 8 | - uses: actions/checkout@v3.3.0 9 | - run: python -m pip install --upgrade black 10 | - run: black --check . 11 | 12 | flake8: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/setup-python@v4 16 | - uses: actions/checkout@v3.3.0 17 | - run: python -m pip install flake8 18 | - run: flake8 19 | 20 | isort: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/setup-python@v4 24 | with: 25 | python-version: "3.10" 26 | - uses: actions/checkout@v3.3.0 27 | - run: python -m pip install cryptography isort==5.6.4 28 | - run: isort --diff --check lora tests 29 | 30 | tests: 31 | runs-on: ubuntu-latest 32 | strategy: 33 | matrix: 34 | python-version: [3.8, 3.9, "3.10", 3.11] 35 | 36 | steps: 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v4 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | - uses: actions/checkout@v3.3.0 42 | - run: python -m pip install discover cryptography==3.2 43 | - run: discover 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | from setuptools import setup 7 | 8 | from lora import VERSION 9 | 10 | package_name = "python-lora" 11 | 12 | if sys.argv[-1] == "publish": 13 | os.system("python setup.py sdist") 14 | os.system("twine upload -r pypi dist/%s-%s.tar.gz" % (package_name, VERSION)) 15 | sys.exit() 16 | 17 | if sys.argv[-1] == "tag": 18 | os.system("git tag -a v{} -m 'tagging v{}'".format(VERSION, VERSION)) 19 | os.system("git push && git push --tags") 20 | sys.exit() 21 | 22 | 23 | setup( 24 | name="python-lora", 25 | version=VERSION, 26 | description="Decrypt LoRa payloads", 27 | url="https://github.com/jieter/python-lora", 28 | author="Jan Pieter Waagmeester", 29 | author_email="jieter@jieter.nl", 30 | license="MIT", 31 | classifiers=[ 32 | "Intended Audience :: Developers", 33 | "License :: OSI Approved :: MIT License", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3.8", 36 | "Programming Language :: Python :: 3.9", 37 | "Programming Language :: Python :: 3.10", 38 | "Programming Language :: Python :: 3.11", 39 | ], 40 | keywords="LoRa decrypt", 41 | packages=["lora"], 42 | install_requires=["cryptography==43.0.1"], 43 | ) 44 | -------------------------------------------------------------------------------- /tests/import_fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from lora.payload import LoRaPayload 5 | 6 | # Import fixtures from a file containing 7 | # 8 | dev_addr = sys.argv[1] or "14000122" 9 | fixture_filename = "{}.txt".format(dev_addr) 10 | 11 | if not os.exists(fixture_filename): 12 | print("File containing fixtures does not exist ({})".format(fixture_filename)) 13 | sys.exit() 14 | 15 | fixtures = open(fixture_filename) 16 | 17 | filename_fmt = "fixtures/{}/payload_{}-{}.xml" 18 | LORA_XML_HEADER = ( 19 | '' 20 | ) 21 | enc = 1 22 | plain = 1 23 | 24 | for item in fixtures.readlines(): 25 | item = item.strip() 26 | 27 | # skip short lines, not containing xml and comments 28 | if not item.startswith(LORA_XML_HEADER): 29 | continue 30 | 31 | payload = LoRaPayload(item) 32 | 33 | if payload.FPort == "1": 34 | # not encrypted 35 | filename = filename_fmt.format(dev_addr, "plaintext", plain) 36 | plain += 1 37 | else: 38 | filename = filename_fmt.format(dev_addr, "encrypted", enc) 39 | enc += 1 40 | # empty file for expected plaintexts. 41 | open(filename.replace(".xml", ".txt"), "w").write("") 42 | 43 | open(filename, "w").write(item.replace("><", ">\n<")) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-lora 2 | 3 | Python wrapper for LoRa payloads from Thingpark/Actility, allowing decryption of the payload. 4 | 5 | - Depends on [cryptography] 6 | - Based on crypto code in [Lora-net/LoRaMac-node] 7 | - Tested with python 3.8, 3.9, 3.10, 3.11 8 | - Available on [pypi] 9 | 10 | ## Usage 11 | 12 | `pip install python-lora` 13 | 14 | [cryptography] requires [cffi] which in turn requires `libffi-dev` and `python-dev`. 15 | 16 | ```python 17 | from lora.payload import LoRaPayload 18 | 19 | xmlstr = ''' 20 | [...] 21 | 2[...] 22 | [...][...] 23 | ''' 24 | 25 | payload = LoRaPayload(xmlstr) 26 | 27 | key = 'AABBCCDDEEFFAABBCCDDEEFFAABBCCDD' 28 | dev_addr = '00112233' 29 | plaintext = payload.decrypt(key, dev_addr) 30 | ``` 31 | 32 | You can also use `loramac_decrypt` without the XML wrapper to decode a hex-encoded `FRMPayload`: 33 | ```python 34 | >>> from lora.crypto import loramac_decrypt 35 | >>> payload = '11daf7a44d5e2bbe557176e9e6c8da' 36 | >>> sequence_counter = 2 37 | >>> key = 'AABBCCDDEEFFAABBCCDDEEFFAABBCCDD' 38 | >>> dev_addr = '00112233' 39 | >>> loramac_decrypt(payload, sequence_counter, key, dev_addr) 40 | [222, 59, 24, 8, 7, 155, 237, 158, 103, 125, 93, 34, 161, 204, 33] 41 | ``` 42 | 43 | [cryptography]: https://cryptography.io/ 44 | [cffi]: https://cffi.readthedocs.org/en/latest/ 45 | [pypi]: https://pypi.python.org/pypi/python-lora 46 | [Lora-net/LoRaMac-node]: https://github.com/Lora-net/LoRaMac-node/blob/master/src/mac/LoRaMacCrypto.c#L108 47 | -------------------------------------------------------------------------------- /lora/payload.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from xml.etree import ElementTree 4 | 5 | from .crypto import loramac_decrypt 6 | 7 | WKT_POINT_FMT = "SRID=4326;POINT({lng} {lat})" 8 | 9 | 10 | class LoRaPayload(object): 11 | """Wrapper for an actility LoRa Payload""" 12 | 13 | XMLNS = "{http://uri.actility.com/lora}" 14 | 15 | def __init__(self, xmlstr): 16 | self.payload = ElementTree.fromstring(xmlstr) 17 | 18 | if self.payload.tag != self.XMLNS + "DevEUI_uplink": 19 | raise ValueError( 20 | "LoRaPayload expects an XML-string containing a " 21 | "DevEUI_uplink tag as root element as argument" 22 | ) 23 | 24 | def __getattr__(self, name): 25 | """ 26 | Get the (text) contents of an element in the DevEUI_uplink XML, allows 27 | accessing them as properties of the objects: 28 | 29 | >>> payload = LoRaPayload('...') 30 | >>> payload.payload_xml 31 | '11daf7a44d5e2bbe557176e9e6c8da' 32 | """ 33 | try: 34 | return self.payload.find(self.XMLNS + name).text 35 | except AttributeError: 36 | print("Could not find tag with name: {}".format(name)) 37 | 38 | def decrypt(self, key, dev_addr): 39 | """ 40 | Decrypt the actual payload in this LoraPayload. 41 | 42 | key: 16-byte hex-encoded AES key. (i.e. AABBCCDDEEFFAABBCCDDEEFFAABBCCDD) 43 | dev_addr: 4-byte hex-encoded DevAddr (i.e. AABBCCDD) 44 | """ 45 | sequence_counter = int(self.FCntUp) 46 | 47 | return loramac_decrypt(self.payload_hex, sequence_counter, key, dev_addr) 48 | 49 | def Lrr_location(self): 50 | """ 51 | Return the location of the LRR (Wireless base station/Gateway) 52 | """ 53 | return WKT_POINT_FMT.format(lng=float(self.LrrLON), lat=float(self.LrrLAT)) 54 | -------------------------------------------------------------------------------- /tests/test_lorapayload.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import glob 4 | import os 5 | import unittest 6 | 7 | from lora.payload import LoRaPayload 8 | 9 | FIXTURES_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures") 10 | 11 | 12 | def read(filename): 13 | """Read a file and strip spaces""" 14 | with open(filename) as f: 15 | return f.read().strip() 16 | 17 | 18 | def fixtures(): 19 | for device_path in glob.glob(os.path.join(FIXTURES_PATH, "*")): 20 | if device_path.endswith("README.md"): 21 | continue 22 | 23 | # infer dev_addr from fixture path 24 | dev_addr = os.path.split(device_path)[1] 25 | key = read(os.path.join(device_path, "key.hex")) 26 | 27 | # text all the files ending in xml in the path we just discovered 28 | for fixture_filename in glob.glob(os.path.join(device_path, "payload*.xml")): 29 | 30 | fixture = "/".join(fixture_filename.split("/")[-2:]) 31 | if "plaintext" in fixture_filename: 32 | expected = None 33 | else: 34 | expected = read(fixture_filename.replace(".xml", ".txt")) 35 | 36 | yield (dev_addr, key, fixture, read(fixture_filename), expected) 37 | 38 | 39 | class TestLoraPayload(unittest.TestCase): 40 | def test_xmlparsing(self): 41 | xmlfilename = os.path.join(FIXTURES_PATH, "000015E4", "payload_1.xml") 42 | 43 | payload = LoRaPayload(read(xmlfilename)) 44 | self.assertEquals(payload.DevLrrCnt, "1") 45 | self.assertEquals(payload.FCntUp, "2") 46 | 47 | self.assertEquals(payload.Lrr_location(), "SRID=4326;POINT(4.36984 52.014877)") 48 | 49 | def test_decrypting_payload(self): 50 | """Check the decrypted plaintext against a list of expected plaintexts""" 51 | for dev_addr, key, fixture_filename, xml, expected in fixtures(): 52 | payload = LoRaPayload(xml.encode("UTF-8")) 53 | plaintext_ints = payload.decrypt(key, dev_addr) 54 | 55 | decrypted_hex = "".join("{:02x}".format(x) for x in plaintext_ints) 56 | 57 | self.assertEquals( 58 | len(decrypted_hex), 59 | len(payload.payload_hex), 60 | "Decryption should not change length of hex string", 61 | ) 62 | 63 | if expected is None: 64 | # plaintext is in filename, so skip checking the expected outcome 65 | continue 66 | 67 | self.assertEquals( 68 | decrypted_hex, 69 | expected, 70 | "Decrypted payload {} not as expected: \npayload_hex: {}\ndecrypted: {}\nexpected: {}".format( 71 | fixture_filename, payload.payload_hex, decrypted_hex, expected 72 | ), 73 | ) 74 | 75 | 76 | if __name__ == "__main__": 77 | unittest.main() 78 | -------------------------------------------------------------------------------- /lora/crypto.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sys 3 | from binascii import unhexlify 4 | 5 | from cryptography.hazmat.backends import default_backend 6 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 7 | 8 | UP_LINK = 0 9 | DOWN_LINK = 1 10 | 11 | 12 | def to_bytes(s): 13 | """ 14 | PY2/PY3 compatible way to convert to something cryptography understands 15 | """ 16 | if sys.version_info < (3,): 17 | return "".join(map(chr, s)) 18 | else: 19 | return bytes(s) 20 | 21 | 22 | def loramac_decrypt(payload_hex, sequence_counter, key, dev_addr, direction=UP_LINK): 23 | """ 24 | LoraMac decrypt 25 | 26 | Which is actually encrypting a predefined 16-byte block (ref LoraWAN 27 | specification 4.3.3.1) and XORing that with each block of data. 28 | 29 | payload_hex: hex-encoded payload (FRMPayload) 30 | sequence_counter: integer, sequence counter (FCntUp) 31 | key: 16-byte hex-encoded AES key. (i.e. AABBCCDDEEFFAABBCCDDEEFFAABBCCDD) 32 | dev_addr: 4-byte hex-encoded DevAddr (i.e. AABBCCDD) 33 | direction: 0 for uplink packets, 1 for downlink packets 34 | 35 | returns an array of byte values. 36 | 37 | This method is based on `void LoRaMacPayloadEncrypt()` in 38 | https://github.com/Lora-net/LoRaMac-node/blob/master/src/mac/LoRaMacCrypto.c#L108 39 | """ 40 | key = unhexlify(key) 41 | dev_addr = unhexlify(dev_addr) 42 | buffer = bytearray(unhexlify(payload_hex)) 43 | size = len(buffer) 44 | 45 | bufferIndex = 0 46 | # block counter 47 | ctr = 1 48 | 49 | # output buffer, initialize to input buffer size. 50 | encBuffer = [0x00] * size 51 | 52 | cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) 53 | 54 | def aes_encrypt_block(aBlock): 55 | """ 56 | AES encrypt a block. 57 | aes.encrypt expects a string, so we convert the input to string and 58 | the return value to bytes again. 59 | """ 60 | encryptor = cipher.encryptor() 61 | 62 | return bytearray(encryptor.update(to_bytes(aBlock)) + encryptor.finalize()) 63 | 64 | # For the exact definition of this block refer to 65 | # 'chapter 4.3.3.1 Encryption in LoRaWAN' in the LoRaWAN specification 66 | aBlock = bytearray( 67 | [ 68 | 0x01, # 0 always 0x01 69 | 0x00, # 1 always 0x00 70 | 0x00, # 2 always 0x00 71 | 0x00, # 3 always 0x00 72 | 0x00, # 4 always 0x00 73 | direction, # 5 dir, 0 for uplink, 1 for downlink 74 | dev_addr[3], # 6 devaddr, lsb 75 | dev_addr[2], # 7 devaddr 76 | dev_addr[1], # 8 devaddr 77 | dev_addr[0], # 9 devaddr, msb 78 | sequence_counter & 0xFF, # 10 sequence counter (FCntUp) lsb 79 | (sequence_counter >> 8) & 0xFF, # 11 sequence counter 80 | (sequence_counter >> 16) & 0xFF, # 12 sequence counter 81 | (sequence_counter >> 24) & 0xFF, # 13 sequence counter (FCntUp) msb 82 | 0x00, # 14 always 0x01 83 | 0x00, # 15 block counter 84 | ] 85 | ) 86 | 87 | # complete blocks 88 | while size >= 16: 89 | aBlock[15] = ctr & 0xFF 90 | ctr += 1 91 | sBlock = aes_encrypt_block(aBlock) 92 | for i in range(16): 93 | encBuffer[bufferIndex + i] = buffer[bufferIndex + i] ^ sBlock[i] 94 | 95 | size -= 16 96 | bufferIndex += 16 97 | 98 | # partial blocks 99 | if size > 0: 100 | aBlock[15] = ctr & 0xFF 101 | sBlock = aes_encrypt_block(aBlock) 102 | for i in range(size): 103 | encBuffer[bufferIndex + i] = buffer[bufferIndex + i] ^ sBlock[i] 104 | 105 | return encBuffer 106 | 107 | 108 | def generate_appskey(): 109 | """Generate a random secret key""" 110 | return "".join("{:02X}".format(x) for x in random.sample(range(255), 16)) 111 | -------------------------------------------------------------------------------- /tests/14000122.txt: -------------------------------------------------------------------------------- 1 | # XMLs provided by KPN on 2016-03-25 2 | # devAddr: 14000122 3 | # key: C6FB9E3C87AC393B43174EFA8F832195 4 | # transmissions on port 1 are plaintext, port 2 are encrypted. 5 | 6 | # Expected plaintexts: 7 | # 0B 8 | # 0A 9 | # 4321 10 | # 1234 11 | # 54321 12 | # 12345 13 | # 654321 14 | # 123456 15 | # 0123456789ABCDEF4321 16 | # 0123456789ABCDEF1234 17 | # 0123456789ABCDEF54321 18 | # 0123456789ABCDEF12345 19 | # 0123456789ABCDEF654321 20 | # 0123456789ABCDEF0123456789ABCDEF 21 | # 0123456789abcdef0123456789abcdef 22 | 23 | 0059AC000010022218114870b6b9755a80059AC01-28.0000008.0000007G1LC310805004252.0702784.478838080500420-28.0000008.000000-28.638920100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 24 | 0059AC00001002222821488c821f624d00059AC01-28.00000010.0000007G1LC110805004252.0702784.478838080500420-28.00000010.000000-28.413927100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 25 | 0059AC000010022218314890a2426cec10059AC01-28.00000010.2500007G1LC210805004252.0702784.478838080500420-28.00000010.250000-28.391785100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 26 | 0059AC00001002222841490955c4e51580059AC01-27.0000008.2500007G1LC110805004252.0702784.478838080500420-27.0000008.250000-27.605556100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 27 | 0059AC0000100222185149143215bcc5f320059AC01-28.0000009.7500007G1LC210805004252.0702784.478838080500420-28.0000009.750000-28.437258100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 28 | 0059AC00001002222861492a167b1f629fc0059AC01-27.0000008.5000007G1LC310805004252.0702784.478838080500420-27.0000008.500000-27.573822100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 29 | 0059AC0000100222187149312346eee9ca20059AC01-28.0000009.7500007G1LC210805004252.0702784.478838080500420-28.0000009.750000-28.437258100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 30 | 0059AC000010022228814945c74141f79e20059AC01-28.0000009.7500007G1LC110805004252.0702784.478838080500420-28.0000009.750000-28.437258100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 31 | 0059AC0000100222189149505432134c155670059AC01-29.0000008.2500007G1LC310805004252.0702784.478838080500420-29.0000008.250000-29.605556100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 32 | 0059AC0000100222290149672c727ed8981370059AC01-23.0000008.7500007G1LC210805004252.0702784.478838080500420-23.0000008.750000-23.543650100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 33 | 0059AC0000100222191149701234524f444490059AC01-27.00000010.2500007G1LC110805004252.0702784.478838080500420-27.00000010.250000-27.391785100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 34 | 0059AC0000100222292149858ba24426aa5790059AC01-27.0000008.0000007G1LC210805004252.0702784.478838080500420-27.0000008.000000-27.638920100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 35 | 0059AC00001002221931499012345725ae7e70059AC01-26.0000007.7500007G1LC110805004252.0702784.478838080500420-26.0000007.750000-26.673985100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 36 | 0059AC000010022229414100835db5e3e0affa0059AC01-29.0000009.7500007G1LC310805004252.0702784.478838080500420-29.0000009.750000-29.437258100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 37 | 0059AC000010022219514101654321ff025f1d0059AC01-28.0000009.2500007G1LC210805004252.0702784.478838080500420-28.0000009.250000-28.487720100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 38 | 0059AC00001002222961410230af333569a29a0059AC01-28.00000010.0000007G1LC110805004252.0702784.478838080500420-28.00000010.000000-28.413927100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 39 | 0059AC0000100222197141031234564b8d59130059AC01-28.00000010.2500007G1LC110805004252.0702784.478838080500420-28.00000010.250000-28.391785100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 40 | 0059AC000010022229814104eb496eab990d150059AC01-28.00000010.7500007G1LC310805004252.0702784.478838080500420-28.00000010.750000-28.350851100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 41 | 0059AC0000100222199141050123456789abcdef43210fde32540059AC01-27.0000008.2500007G1LC210805004252.0702784.478838080500420-27.0000008.250000-27.605556100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 42 | 0059AC0000100222210014106551edc7c807aa97e0efc0bb0327d0059AC01-27.0000008.2500007G1LC110805004252.0702784.478838080500420-27.0000008.250000-27.605556100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 43 | 0059AC00001002221101141070123456789abcdef1234ba0c40590059AC01-28.0000009.7500007G1LC310805004252.0702784.478838080500420-28.0000009.750000-28.437258100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 44 | 0059AC000010022221021410833500e0201fd25456c406d3f06ce0059AC01-28.00000010.5000007G1LC210805004252.0702784.478838080500420-28.00000010.500000-28.370777100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 45 | 0059AC000010022211031410900123456789abcdef543215d55cdf40059AC01-27.0000008.5000007G1LC110805004252.0702784.478838080500420-27.0000008.500000-27.573822100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 46 | 0059AC000010022221041411067f93000c6f907e477a0d77693f5f00059AC01-29.00000010.5000007G1LC310805004252.0702784.478838080500420-29.00000010.500000-29.370777100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 47 | 0059AC000010022211051411100123456789abcdef12345376524490059AC01-28.00000010.7500007G1LC210805004252.0702784.478838080500420-28.00000010.750000-28.350851100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 48 | 0059AC0000100222210614112b8060d0f29499ca30819152569a9250059AC01-27.0000009.0000007G1LC210805004252.0702784.478838080500420-27.0000009.000000-27.514969100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 49 | 0059AC00001002221107141130123456789abcdef65432121980e490059AC01-27.0000009.5000007G1LC110805004252.0702784.478838080500420-27.0000009.500000-27.461836100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 50 | 0059AC000010022221081411447aeb5c4712c63e9197fd52b53deee0059AC01-29.0000009.0000007G1LC310805004252.0702784.478838080500420-29.0000009.000000-29.514969100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 51 | 0059AC00001002221109141150123456789abcdef0123456789abcdef09123c470059AC01-28.0000008.7500007G1LC110805004252.0702784.478838080500420-28.0000008.750000-28.543650100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 52 | 0059AC000010022221101411715c8d5678b86edb5349b89b57da71cb85b23d6c60059AC01-28.00000010.2500008G1LC210805004252.0703324.478900080500420-28.00000010.250000-28.391785100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 53 | 0059AC00001002221111141180123456789abcdef0123456789abcdef711a20ee0059AC01-28.00000011.0000007G1LC110805004252.0703324.478900080500420-28.00000011.000000-28.331957100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 54 | 0059AC0000100222211214119c7d9853a545d9789a5e4285c0859d6fb6e59af9f0059AC01-28.00000010.0000007G1LC310805004252.0703324.478900080500420-28.00000010.000000-28.413927100006246{"alr":{"pro":"SMTC/LoRaMote","ver":"1"}}0 55 | --------------------------------------------------------------------------------