/*
* Shows how to decode a LoRaWAN 1.0.x OTAA Join Accept message, and derive the session keys.
*
* For a not-encrypted Join Request like 00DC0000D07ED5B3701E6FEDF57CEEAF0085CC587FE913
* https://github.com/anthonykirby/lora-packet correctly shows:
*
* Message Type = Join Request
* AppEUI = 70B3D57ED00000DC
* DevEUI = 00AFEE7CF5ED6F1E
* DevNonce = CC85
* MIC = 587FE913
*
* For its response, 204DD85AE608B87FC4889970B7D2042C9E72959B0057AED6094B16003DF12DE145,
* it currently erroneously suggests:
*
* Message Type = Join Accept
* AppNonce = 5AD84D
* NetID = B808E6
* DevAddr = 9988C47F
* MIC = F12DE145
*
* However, the Join Accept payload (including its MIC) is encrypted using the secret
* AppKey (not to be confused with the AppSKey, which is actually derived from the Join
* Accept). When decrypted using B6B53F4A168A7A88BDF7EA135CE9CFCA, the above Join Accept
* would yield:
*
* Message Type = Join Accept
* 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
*/
var reverse = require('buffer-reverse');
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.
// NOTE: this is for EU868 in LoRaWAN 1.0.x; other regions and versions might need a
// different decoding, like for US915 in LoRaWAN 1.1 see page 15 of
// https://lora-alliance.org/sites/default/files/2018-05/lorawan-regional-parameters-v1.1ra.pdf
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(', ') + '; RFU ' + rfu.toString('hex')
+ '\n message MIC = ' + mic.toString('hex')
+ '\nverified MIC = ' + micVerify.toString('hex')
+ '\n NwkSKey = ' + nwkSKey.toString('hex')
+ '\n AppSKey = ' + appSKey.toString('hex');
'<pre>\n' + r + '\n</pre>';