Mechanism
LoRaWAN’s confidentiality and integrity derive entirely from a root key, so this layer is about key and nonce management, not cipher strength. In 1.0.x the device and back end share an AppKey; in 1.1 the root is split into NwkKey and AppKey. From the root, the two session keys are derived: the AppSKey encrypts the FRMPayload (AES-128), and the NwkSKey keys the 4-byte MIC (AES-128-CMAC) [loraalliance2020l2][hessel2022survey]. There is no feasible brute force of a strong random AES-128 root key; the real-world exposure is that the key is often not strong, not unique, or not secret, and that the join nonce and session keys are mismanaged [hessel2022survey].
Default / shared / hardcoded AppKey. IOActive’s audit of real LoRaWAN deployments found root keys that were hardcoded in firmware, shared fleet-wide, set to vendor defaults, or otherwise easy to source from public repositories or weak enough to guess [ioactive2020lorawan]. Because every session key descends from that root, recovering it once decrypts all traffic for the device’s lifetime and lets an attacker forge frames with valid MICs — a complete crypto break [ioactive2020lorawan][hessel2022survey]. This is exactly what a candidate-key test demonstrates: given a guessed/leaked AppKey and a captured join, derive the session keys and confirm by recomputing the captured MIC [loracrack][laf]. It is not a brute force of a strong key [loracrack].
DevNonce reuse / join replay (1.0.x). The OTAA JoinRequest carries JoinEUI/AppEUI, DevEUI and a DevNonce in the clear, protected only by a MIC — the key is never transmitted, but the DevNonce sequence is fully observable passively [loraalliance2020l2]. In LoRaWAN 1.0.0–1.0.3 the DevNonce is a random value, and the network must remember and reject previously-seen values; a repeat lets a captured JoinRequest be replayed [na2017replay][loraalliance2020joinsync]. Reuse is not hypothetical: the random nonce is subject to the birthday paradox over a multi-year lifetime, and constrained devices reuse nonces after reboots, counter resets, or poor RNG seeding [hessel2022survey]. LoRaWAN 1.0.4 and 1.1 redefine DevNonce as a monotonic counter that SHALL never be reused for a given JoinEUI, closing this window — at the cost of the join-synchronisation problems the LoRa Alliance documents for mixed fleets [loraalliance2020joinsync][loraalliance2020l2].
Static ABP session keys / frame-counter reset. ABP (Activation By Personalisation) burns static NwkSKey/AppSKey/DevAddr into the device with no per-join rotation. If the frame counter resets on power-cycle, the AES-CTR-style payload encryption repeats keystream (a two-time pad recoverable by XOR) and stale frames become replayable — both reachable without ever recovering the key [hessel2022survey][tarlogic2020lorawan]. OTAA exists precisely to avoid this by deriving fresh session keys at each join.
The IOActive findings (default/hardcoded/weak AppKeys) are reported across audited deployments and firmware, not a single named CVE; treat them as a representative pattern and check the specific device/stack before asserting a given deployment is affected. Loracrack and LAF are PoC/audit tooling (Loracrack last pushed 2022-06, LAF 2023-05); re-confirm they build against your capture format and LoRaWAN version before relying on them.
Procedure
Authorised testing only. Capturing and analysing another network’s traffic, and testing any candidate key against it, requires explicit written permission and must be lawful in your jurisdiction. Do not transmit at this layer; this control is offline analysis over a capture you are authorised to hold. Test AppKey candidates only against your own / in-scope devices.
-
Capture the join and following data frames for the target device, per RFSAM-RES-07 and RFSAM-LORA-LL-01 — de-chirp the CSS waveform and export a LoRaTap PCAP that contains at least one OTAA
JoinRequest/JoinAcceptpair and subsequent uplinks for the sameDevAddr. Confirm in Wireshark:lorawan.mtype == 0 # Join Request: JoinEUI/AppEUI, DevEUI, DevNonce (cleartext) lorawan.mtype == 1 # Join Accept (encrypted)Expected: the join identifiers readable in the clear; the
FRMPayloadof data frames shown as AES-128 ciphertext. -
Establish the LoRaWAN version and activation mode (from the IG step / device profile). The checks below differ for 1.0.x vs 1.0.4/1.1, and for OTAA vs ABP. Record which applies.
-
Audit DevNonce freshness (OTAA, 1.0.x). Group
JoinRequests byDevEUIand extract theDevNoncesequence. Export the join frames and tabulate:# tshark over the LoRaTap PCAP: one row per JoinRequest tshark -r joins.pcap -Y "lorawan.mtype == 0" \ -T fields -e lorawan.dev_eui -e lorawan.dev_nonce | sort | uniq -c | sort -rnExpected: a count per
(DevEUI, DevNonce). Any count > 1 is a reused DevNonce → 1.0.x join-replay exposure [loraalliance2020joinsync]. A small or clustered nonce space (few distinct values, or values biased low) indicates weak RNG even without an exact repeat [hessel2022survey]. -
Test for a weak / shared / default AppKey (OTAA). Assemble candidate keys: vendor defaults, all-zero / sample keys from datasheets or SDKs, keys extracted from firmware, and any fleet-wide value you suspect is reused. Feed the captured join + a data frame and each candidate to the derivation-and-MIC check:
# Loracrack: derive session keys from a candidate AppKey and validate via the MIC python loracrack.py --pcap capture.pcap --appkey <CANDIDATE_APPKEY>Expected: for the correct key, the tool derives
NwkSKey/AppSKeyand the recomputed MIC matches the captured frame’s MIC — confirming the key and that the session is now decryptable [loracrack]. For a wrong key the MIC mismatches. Equivalently, use LAF to recompute and validate the MIC against a key candidate [laf]. A match is a finding: the root key is not secret.A MIC match means you have recovered the session keys for that capture. Decrypt only traffic you are authorised to analyse; do not act on recovered keys beyond the agreed scope.
-
Audit ABP session-key hygiene (ABP). For ABP devices, confirm whether the frame counter (
FCnt) ever resets — capture across a device reboot/power-cycle in your test rig and check for a counter that returns to a low value while theDevAddrand (static) keys are unchanged:lorawan.dev_addr == <TARGET_DEVADDR> # then read the FCnt column over timeExpected: a healthy device’s
FCntincreases monotonically. A reset (a high value followed by a low one) under static keys is the keystream-reuse / replay finding [hessel2022survey][tarlogic2020lorawan]. -
Record the result per device: version + activation mode; DevNonce behaviour (fresh / reused / weak-RNG); AppKey result (strong-random vs a candidate that produced a MIC match); ABP
FCntreset (y/n). Each positive is a key-management failure that collapses some or all of the crypto chain.
Field case
Reported measurement (carried over from the paired link-layer control). The same authorised US915 capture used to profile this network at the link layer (RFSAM-LORA-LL-01) — 51,304 LoRaWAN frames, of which 45,815 (89.3%) were JoinRequests — is the input for the CR-layer key analysis: because the capture was so heavily dominated by JoinRequests, each device’s DevNonce sequence was directly observable in the clear, which is the prerequisite for spotting reuse [loraalliance2020joinsync]. The application payloads stayed AES-128 protected throughout — confidentiality held against the capture; the question this control asks is whether it holds against the key management. This frame/JoinRequest count is the author’s reported field figure, reproduced verbatim from the paired RFSAM-LORA-LL-01 control; it has not been independently re-measured here.
Illustrative walkthrough — substitute the values you capture. To reproduce against your own authorised test network: stand up a couple of OTAA devices and one ABP device on a ChirpStack/RAK gateway, deliberately provision one OTAA device with a default/sample AppKey (so the key-candidate test in step 4 succeeds), and one 1.0.x device that re-joins on reboot (so step 3 shows a DevNonce repeat). Then run steps 3–5 and tabulate:
# per-device key-management findings from an authorised test capture
DevEUI / DevAddr version activation DevNonce AppKey test ABP FCnt reset
[FILL: id] 1.0.3 OTAA [FILL: repeated?] MIC match (2b11ff0d) n/a
[FILL: id] 1.1 OTAA monotonic [FILL: no match] n/a
[FILL: id] 1.0.3 ABP n/a n/a [FILL: y/n]
Reproducible public worked example (no live capture) for the step-4 cell. Rather than depend on a live measurement, the candidate-key/MIC test for the weak OTAA row above is grounded in a documented public test vector shipped with the lora-packet library [lorapacket]. For the data uplink frame 40F17DBE4900020001954378762B11FF0D, feeding the candidate NwkSKey 44024241ed4ce9a68c6a8bc055233fd3 recomputes the MIC as 2b11ff0d, which equals the frame’s trailing MIC bytes — verifyMIC returns true (MIC check=OK) — and feeding the AppSKey ec925802ae430ca77fd3dd73cb2cc588 decrypts the FRMPayload to the ASCII plaintext test. The same library’s vectors also confirm the join side: for the JoinRequest 0039363463336913AA05693574323831330489C65B1304, the candidate AppKey 98929b92c49edba9676d646d3b612456 recomputes the JoinRequest MIC as c65b1304, again matching the frame. These values are self-consistent and reproducible entirely offline (__tests__/mic_test.ts, __tests__/decrypt_test.ts, and the README CLI decoder walkthrough), so the “candidate key → MIC match → decryptable payload” chain of step 4 can be demonstrated end-to-end without any live RF capture [lorapacket].
The expected shape: the deliberately weak OTAA device yields a MIC match under the default key (its session keys are recoverable and traffic decryptable, exactly as the lora-packet vector above shows), the 1.0.x re-joining device shows a repeated DevNonce (join-replay exposure), and the ABP device’s counter resets across reboot (keystream-reuse/replay exposure) — while a correctly provisioned 1.1 OTAA device passes all three. The remaining [FILL: …] cells above are placeholders for the values you capture in your own authorised analysis — they are not measured findings.
Remediation
Developer (device / stack). Provision a unique, cryptographically random AppKey (1.0.x) or NwkKey+AppKey (1.1) per device; never ship a default, sample, all-zero, fleet-wide or firmware-hardcoded root key, since recovering it once breaks every session for the device’s life [ioactive2020lorawan][hessel2022survey]. Generate DevNonce correctly: on 1.0.4/1.1 use the mandated monotonic counter that is never reused for a JoinEUI and persists across reboots; on legacy 1.0.x with a random DevNonce, ensure adequate RNG entropy and persistence so nonces do not repeat after a reset [loraalliance2020l2][loraalliance2020joinsync]. Prefer OTAA over ABP so session keys are derived fresh at each join rather than burned in static.
Integrator (network / join server). Enforce strict server-side DevNonce tracking: remember and reject previously-seen DevNonce values (1.0.x) or enforce monotonicity (1.0.4/1.1), so a replayed JoinRequest is denied [na2017replay][loraalliance2020joinsync]. For ABP deployments, enforce frame-counter validation that survives device reboots — reject a counter that resets rather than accepting it — to neutralise the keystream-reuse and replay paths [hessel2022survey][tarlogic2020lorawan]. Where feasible, migrate to 1.1’s split keys and Join Server, and ensure no 1.0.x elements remain in a network claimed to be 1.1, since backward compatibility reopens the older weaknesses [tarlogic2020lorawan].
Operator (deployment). Treat the root key as the crown jewel: rotate keys where the lifecycle allows, audit that every deployed device carries a unique key (not a shared batch value), and protect the network/join server credentials so the keys cannot be read from an exposed back end [ioactive2020lorawan]. Periodically run this passive join/session-key audit against your own network as a monitoring control — a recurring DevNonce, a successful default-key derivation, or a resetting ABP counter is a provisioning failure to fix, not merely an attacker’s opportunity.