PK P}X - deciphering-a-lorawan-otaa-join-accept-1.0.4/PK P}X 1 deciphering-a-lorawan-otaa-join-accept-1.0.4/lib/PK P}X7 Z deciphering-a-lorawan-otaa-join-accept-1.0.4/lib/deciphering-a-lorawan-otaa-join-accept.js/* * 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'); '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. // 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'); '
\n' + r + '\n'; ; PK P}Xʃ>a a 9 deciphering-a-lorawan-otaa-join-accept-1.0.4/package.json{ "name": "@runkit/avbentem_deciphering-a-lorawan-otaa-join-accept", "version": "1.0.4", "main": "./lib/deciphering-a-lorawan-otaa-join-accept", "dependencies": { "buffer-reverse": "1.0.1", "crypto-js": "3.1.8", "node-aes-cmac": "0.1.1" }, "scripts": { "install": "node scripts/install.js" } }PK P}X]ZQ Q @ deciphering-a-lorawan-otaa-join-accept-1.0.4/npm-shrinkwrap.json{ "name": "@runkit/avbentem_deciphering-a-lorawan-otaa-join-accept", "version": "1.0.4", "dependencies": { "buffer-reverse": { "version": "1.0.1", "from": "https://registry.npmjs.org/buffer-reverse/-/buffer-reverse-1.0.1.tgz" }, "crypto-js": { "version": "3.1.8", "from": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz" }, "node-aes-cmac": { "version": "0.1.1", "from": "https://registry.npmjs.org/node-aes-cmac/-/node-aes-cmac-0.1.1.tgz" } } }PK P}X 5 deciphering-a-lorawan-otaa-join-accept-1.0.4/scripts/PK P}X CZ Z ? deciphering-a-lorawan-otaa-join-accept-1.0.4/scripts/install.jsvar fs = require('fs'); var path = require('path'); var json = require("../package.json"); var match = json.name.match(/^@runkit\/(.*)_(.*)$/); // check if our module name actually makes sense, otherwise abort if (!match) throw new Error("Unknown package structure!"); // check if we're actually inside a node_modules/@runkit folder, otherwise don't create symlinks if (path.basename(path.dirname(process.cwd())) !== "@runkit" || path.basename(path.dirname(path.dirname(process.cwd()))) !== "node_modules") { console.log("Not installing as notebook: " + process.cwd()); return; } console.log("creating notebook symlinks"); var userPath = path.join("..", match[1]); var repoPath = path.join(userPath, match[2]); mkdir(userPath); mkdir(repoPath); var branchMatch = json.version.match(/^0\.0\.0\-(.*)$/); if (branchMatch) { var branchPath = path.join(repoPath, "branches"); mkdir(branchPath); var versionPath = path.join(branchPath, branchMatch[1]); var relativePath = path.join("..", "..", "..", path.basename(process.cwd())); unlink(versionPath); fs.symlinkSync(relativePath, versionPath); } else { var relativePath = path.join("..", "..", path.basename(process.cwd())); var latestPath = path.join(repoPath, "latest"); var versionPath = path.join(repoPath, json.version); unlink(latestPath); fs.symlinkSync(relativePath, latestPath); unlink(versionPath); fs.symlinkSync(relativePath, versionPath); } function mkdir(p) { try { fs.mkdirSync(p); } catch (e) {} } function unlink(p) { try { fs.unlinkSync(p); } catch (e) {} }PK P}X - deciphering-a-lorawan-otaa-join-accept-1.0.4/PK P}X 1 K deciphering-a-lorawan-otaa-join-accept-1.0.4/lib/PK P}X7 Z deciphering-a-lorawan-otaa-join-accept-1.0.4/lib/deciphering-a-lorawan-otaa-join-accept.jsPK P}Xʃ>a a 9 deciphering-a-lorawan-otaa-join-accept-1.0.4/package.jsonPK P}X]ZQ Q @ { deciphering-a-lorawan-otaa-join-accept-1.0.4/npm-shrinkwrap.jsonPK P}X 5 * deciphering-a-lorawan-otaa-join-accept-1.0.4/scripts/PK P}X CZ Z ? } deciphering-a-lorawan-otaa-join-accept-1.0.4/scripts/install.jsPK 4$