Deciphering an EU868 LoRaWAN 1.0 OTAA Join Accept

node v6.17.1
version: 1.0.5
endpointsharetweet
/* * 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>';
Loading…

no comments

    sign in to comment