lora-packet
lora-packet copied to clipboard
Join Accept details cannot be shown without decrypting using the AppKey
lora-packet shows incorrect details for a Join Accept:
-
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 -
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>');
(thank you for the detailed report; I'm scheduling time in September to work on this; apologies for the delay)
(I wish I could downvote for unneeded apologies! ;-) )
Note that the above example is for EU868 in LoRaWAN 1.0.x; other regions and versions might need a different decoding.
Like US915 (which support a whopping 64 channels) does not support CFList in 1.0.x, but does in 1.1; see page 15 of https://lora-alliance.org/sites/default/files/2018-05/lorawan-regional-parameters-v1.1ra.pdf
Can I have code for ABP, I need this because I am using ABP.
@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.)