question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Join Accept details cannot be shown without decrypting using the AppKey

See original GitHub issue

lora-packet shows incorrect details for a Join Accept:

  1. Without first decrypting the message, the following lines in _initialiseFromWireformat are not quite valid: https://github.com/anthonykirby/lora-packet/blob/53190fa4b9b2920c31187f8e39adb7d72c8cc6b0/lib/packet.js#L138-L149

  2. I guess toString should only print the message type, without any of the erroneous details: https://github.com/anthonykirby/lora-packet/blob/53190fa4b9b2920c31187f8e39adb7d72c8cc6b0/lib/packet.js#L408-L414

Background

For a not-encrypted Join Request like 00DC0000D07ED5B3701E6FEDF57CEEAF0085CC587FE913 lora-packet correctly shows:

Message Type = Join Request
      AppEUI = 70B3D57ED00000DC
      DevEUI = 00AFEE7CF5ED6F1E
    DevNonce = CC85
         MIC = 587FE913

For a matching response, 204DD85AE608B87FC4889970B7D2042C9E72959B0057AED6094B16003DF12DE145, it currently erroneously suggests:

Message Type = Join Accept
    AppNonce = 5AD84D
       NetID = B808E6
     DevAddr = 9988C47F
         MIC = F12DE145

This is wrong as the Join Accept payload (including its MIC) is encrypted using the secret AppKey (not to be confused with the session AppSKey, which is actually derived from the Join Accept). When decrypted using AppKey B6B53F4A168A7A88BDF7EA135CE9CFCA, the above Join Accept would yield:

    AppNonce = E5063A
       NetID = 000013
     DevAddr = 26012E43
  DLSettings = 03
     RXDelay = 01
      CFList = 184F84E85684B85E84886684586E8400
             = decimal 8671000, 8673000, 8675000, 8677000, 8679000
         MIC = 55121DE0

(The Things Network has been assigned a 7-bits “device address prefix” a.k.a. NwkID %0010011. Using that, TTN currently sends NetID 0x000013, and a TTN DevAddr always starts with 0x26 or 0x27.)

When the DevNonce from the Join Request is known as well, then the session keys can be derived:

     NwkSKey = 2C96F7028184BB0BE8AA49275290D4FC
     AppSKey = F3A5C8F0232A38C144029C165865802C

Example to derive the values

The following working example can also be seen at https://runkit.com/avbentem/deciphering-a-lorawan-otaa-join-accept

/*
 * Shows how to decode a LoRaWAN OTAA Join Accept message, and derive the session keys.
 */

var reverse = require('buffer-reverse');
'use strict';
var CryptoJS = require('crypto-js');
var aesCmac = require('node-aes-cmac').aesCmac;

// Secret AppKey as programmed in the device
var appKey = Buffer.from('B6B53F4A168A7A88BDF7EA135CE9CFCA', 'hex');

// DevNonce as generated in Join Request
var devNonce = Buffer.from('CC85', 'hex');

// Full packet: 0x20 MHDR, Join Accept (12 bytes, 16 bytes optional CFList, 4 bytes MIC)
var phyPayload = Buffer.from(
    '204dd85ae608b87fc4889970b7d2042c9e72959b0057aed6094b16003df12de145', 'hex');

// Initialization vector is always zero
var LORA_IV = CryptoJS.enc.Hex.parse('00000000000000000000000000000000');

// Encrypts the given buffer, returning another buffer.
function encrypt(buffer, key) {
    var ciphertext = CryptoJS.AES.encrypt(
        CryptoJS.lib.WordArray.create(buffer),
        CryptoJS.lib.WordArray.create(key),
        {
            mode: CryptoJS.mode.ECB,
            iv: LORA_IV,
            padding: CryptoJS.pad.NoPadding
        }
    ).ciphertext.toString(CryptoJS.enc.Hex);
    return new Buffer(ciphertext, 'hex');
}

// ## Decrypt payload, including MIC
//
// The network server uses an AES decrypt operation in ECB mode to encrypt the join-accept
// message so that the end-device can use an AES encrypt operation to decrypt the message.
// This way an end-device only has to implement AES encrypt but not AES decrypt.
var mhdr = phyPayload.slice(0, 1);
var joinAccept = encrypt(phyPayload.slice(1), appKey);

// ## Decode fields
//
// Size (bytes):     3       3       4         1          1     (16) Optional   4
// Join Accept:  AppNonce  NetID  DevAddr  DLSettings  RxDelay      CFList     MIC
var i = 0;
var appNonce = joinAccept.slice(i, i += 3);
var netID = joinAccept.slice(i, i += 3);
var devAddr = joinAccept.slice(i, i += 4);
var dlSettings = joinAccept.slice(i, i += 1);
var rxDelay = joinAccept.slice(i, i += 1);
if (i + 4 < joinAccept.length) {
    // We need the complete little-endian list (including its RFU byte) for the MIC
    var cfList = joinAccept.slice(i, i += 16);
    // Decode the 5 additional channel frequencies
    var frequencies = [];
    for (var c = 0; c < 5; c++) {
        frequencies.push(cfList.readUIntLE(3 * c, 3));
    }
    var rfu = cfList.slice(15, 15 + 1);
}
var mic = joinAccept.slice(i, i += 4);

// ## Validate MIC
//
// Below, the AppNonce, NetID and all should be added in little-endian format.
// cmac = aes128_cmac(AppKey, MHDR|AppNonce|NetID|DevAddr|DLSettings|RxDelay|CFList)
// MIC = cmac[0..3]
var micVerify = aesCmac(
    appKey,
    Buffer.concat([
        mhdr,
        appNonce,
        netID,
        devAddr,
        dlSettings,
        rxDelay,
        cfList
    ]),
    {returnAsBuffer: true}
).slice(0, 4);

// ## Derive session keys
//
// NwkSKey = aes128_encrypt(AppKey, 0x01|AppNonce|NetID|DevNonce|pad16)
// AppSKey = aes128_encrypt(AppKey, 0x02|AppNonce|NetID|DevNonce|pad16)
var sKey = Buffer.concat([
    appNonce,
    netID,
    reverse(devNonce),
    Buffer.from('00000000000000', 'hex')
]);
var nwkSKey = encrypt(Buffer.concat([Buffer.from('01', 'hex'), sKey]), appKey);
var appSKey = encrypt(Buffer.concat([Buffer.from('02', 'hex'), sKey]), appKey);

var r = '     Payload = ' + phyPayload.toString('hex')
    + '\n        MHDR = ' + mhdr.toString('hex')
    + '\n Join Accept = ' + joinAccept.toString('hex')
    + '\n    AppNonce = ' + (reverse(appNonce)).toString('hex')
    + '\n       NetID = ' + (reverse(netID)).toString('hex')
    + '\n     DevAddr = ' + (reverse(devAddr)).toString('hex')
    + '\n  DLSettings = ' + dlSettings.toString('hex')
    + '\n     RXDelay = ' + rxDelay.toString('hex')
    + '\n      CFList = ' + cfList.toString('hex')
    + '\n             = decimal ' + frequencies.join(', ')
    + '\n message MIC = ' + mic.toString('hex')
    + '\nverified MIC = ' + micVerify.toString('hex')
    + '\n     NwkSKey = ' + nwkSKey.toString('hex')
    + '\n     AppSKey = ' + appSKey.toString('hex');

console.log('<pre>\n' + r + '\n</pre>');

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:1
  • Comments:5 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
anthonykirbycommented, Aug 17, 2017

(thank you for the detailed report; I’m scheduling time in September to work on this; apologies for the delay)

0reactions
avbentemcommented, Jun 11, 2020

@nvdak, this issue does not apply to ABP.

(For ABP, the DevAddr and the secret AppSKey and NwkSKey are fixed, and are simply copied/programmed into the device after registering/activating it on the network. Like for www.thethingsnetwork.org such registration/activation would use the TTN Console website, or the ttnctl command line interface. After copying/programming the fixed details into the device, there are no join messages to be decrypted/decoded at all.)

Read more comments on GitHub >

github_iconTop Results From Across the Web

Generate join accept message (OTAA mode) with MKRWAN ...
I have the APPKEY and the APPEUI. How can I use the MKRWAN 1300 to accept join request and generate the join accept...
Read more >
OTAA Join Debugging - The Things Network
Unfortunately, I can't get my node to join the application. Now I'm looking for ways to debug the join process to get a...
Read more >
Troubleshooting Devices | The Things Stack for LoRaWAN
Check for the errors and solutions listed above; If the Network Server is processing Join Requests and scheduling Join Accepts, check the gateway...
Read more >
OTAA Join Accept message - ChirpStack Application Server
I'm not clear using which key, the join accept payload is encrypted (according to the specification its the AppKey isn't it?)
Read more >
LoRaWAN End Device Activation - LoRa Developer Portal
Never include the AppKey in this QR code, and never show it on the cover of the ... If you are not using...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found