LoRaWAN 1.0.x packet decoder

node v6.17.1
version: 2.1.2
This uses the nice lora-packet library to decode packets from LoRaWAN gateway logs. Just paste the packet into the first line below and hit Shift+Return to decode it.
// Copy the packet from, e.g., "data": "ADFGUkFEshgAdAoAAACyGADXQ5rzpZs=" into // the variable below and hit Shift+Return. Or use the API endpoint to see a // form, or pass the data in the REST URL. // const data = 'ADFGUkFEshgAdAoAAACyGADXQ5rzpZs='; // const nwkSKey = null; // const appSKey = null; const data = '40531E012680664601457090ED25'; // Optional, to validate MIC (brute-forcing to extend 16 bits FCnt to 32 bits) and decrypt payload const nwkSKey = '7A47F143D7CEF033DFA0D4B75E04A316'; const appSKey = 'F1B0B1D3CC529C55C3019A46EF4582EA'; const lorapacket = require('lora-packet@0.7.7'); function decode(data, nwkSKey, appSKey) { data = data.trim(); nwkSKey = nwkSKey ? new Buffer(nwkSKey.trim(), 'hex') : undefined; appSKey = appSKey ? new Buffer(appSKey.trim(), 'hex') : undefined; const enc = data.match(/^[0-9A-F]*$/i) ? 'hex' : 'base64'; try { const packet = lorapacket.fromWire(new Buffer(data, enc)); const isJoinAccept = packet.getMType() === 'Join Accept'; const isJoin = isJoinAccept || packet.getMType() === 'Join Request'; let decoded = packet.toString(); if(isJoinAccept) { decoded = decoded.replace('Join Accept', 'Join Accept -- <strong style="color: #f00">WARNING: The values below have not been decrypted</strong>'); } // For a Join Request, we only need the AppKey, so allow NwkSKey to be empty if(appSKey) { // In LoRaWAN 1.0.x, the value of FCnt only holds the 16 least-significant bits (LSB) of the // actual frame counter. But for a 32 bits frame counter still all 32 bits are used when // calculating the MIC. So, brute-force to find the counter's 16 most significant bits. This // will try 65,536 values... let fCntMsb; const msb = new Buffer(2); let i; for (i = 0; i < 1 << 16; i++) { console.log(`Trying: ${i}`); msb.writeUInt16LE(i, 0); // TODO This needs AppKey, not AppSKey, for Join Accept if (lorapacket.verifyMIC(packet, nwkSKey, appSKey, msb)) { fCntMsb = ('0000' + i.toString(16)).toUpperCase().substr(-4); console.log(`Found MSB: 0x${fCntMsb}`); console.log(`32 bits FCnt: ${i << 16 | packet.getFCnt()}`); break; } } // When no MSB is found, show the expected value for MSB 0x0000 rather than for 0xFFFF: const expected = lorapacket.calculateMIC(packet, nwkSKey, appSKey, fCntMsb ? msb : null); const valid = lorapacket.verifyMIC(packet, nwkSKey, appSKey, fCntMsb ? msb : null); decoded = decoded.replace(/^(.*MIC = .*$)/m, '$1 (from packet)' + (valid ? '' : ' <strong style="color: #f00">INVALID</strong> (tried MSB 0000-' + ('0000' + (i - 1).toString(16)).toUpperCase().substr(-4) + ')') + '\n = ' + expected.toString('hex').toUpperCase() + ' (expected, assuming 32 bits frame counter with MSB ' + (fCntMsb ? fCntMsb : '0000') + ')' ); if(valid) { // The first occurence of "FCnt" is for FHDR and includes "(Big Endian); we want the 2nd occurence // in the summary, which is a bare decimal number decoded = decoded.replace(/^(.*FCnt = [0-9]*$)/m, '$1 (from packet, 16 bits) \n = ' + (i << 16 | packet.getFCnt()) + ' (32 bits, assuming MSB 0x' + ('0000' + i.toString(16)).substr(-4) + ')' ); } if(!isJoin) { const payload = lorapacket.decrypt(packet, appSKey, nwkSKey); // We don't have to align the additional line here, as it will be re-aligned later decoded = decoded.replace(/^(.*FRMPayload) = .+$/m, (match, m1) => `${match} (from packet, encrypted)\n = ${payload.toString('hex').toUpperCase()} (decrypted)`); } } else { // decoded += '\nProvide AppSKey and NwkSKey to validate MIC and decrypt payload'; } // Align the output on the '=' character with as little leading whitespace as possible // (and fix an alignment error in the lora-packet 0.7.3 output): const lines = decoded.split('\n'); const lengths = lines.map(s => s.replace(/^\s*(.*)( = .*)$/, (match, m1, m2) => m1).length); const max = Math.max(...lengths.filter(length => length > 0)); decoded = lines.map(s => s.replace(/^\s*(.*)( = .*)$/, (match, m1, m2) => ' '.repeat(max - m1.length) + m1 + m2)).join('\n'); return `Assuming ${enc}-encoded packet\n${data}\n\n${decoded}`; } catch(e) { return e.message; } } if(data) { decode(data, nwkSKey, appSKey); }
See & star https://github.com/anthonykirby/lora-packet. Or, if you want to install a command line tool utility: - install Node.js and npm; https://docs.npmjs.com/getting-started/installing-node - run: npm install -g lora-packet - run: lora-packet-decode --base64 ADFGUkFEshgAdAoAAACyGADXQ5rzpZs= - ...or: lora-packet-decode --hex 003146524144B21800740A000000B21800D7439AF3A59B For HEX, beware that removing leading zeroes will affect proper decoding!
// Same as above, for HTML form and API endpoint const express = require("@runkit/runkit/express-endpoint/1.0.0"); const app = express(exports); app.get("/", (req, res) => { // exports.endpoint = function(req, res) { let decoded = ""; if(req.query && req.query.data) { decoded = ` <pre>${decode(req.query.data, req.query.nwkskey, req.query.appskey)}</pre> `; } else { decoded = ` <p><strong>Examples</strong></p> <p>Copy the packet from, e.g., <code>"data": "ADFGUkFEshgAdAoAAACyGADXQ5rzpZs="</code> in the gateway's log.</p> <ul> <li><a href="?data=ANwAANB%2B1bNwHm/t9XzurwDIhgMK8sk=&appskey=B6B53F4A168A7A88BDF7EA135CE9CFCA">Join Request with secret AppKey<code>ANwAANB+1bNwHm/t9XzurwDIhgMK8sk=</code></a></li> <li><a href="?data=IIE/R/UI/6JnC24j4B%2BEueJdnEEV8C7qCz3T4gs%2BypLa">Join Accept <code>IIE/R/UI/6JnC24j4B+EueJdnEEV8C7qCz3T4gs+ypL</code></a></li> <li><a href="?data=QCkuASaAAAAByFaF53Iu%2BvzmwQ==">Uplink <code>QCkuASaAAAAByFaF53Iu+vzmwQ==</code></a></li> <li><a href="?data=QK4TBCaAAAABb4ldmIEHFOMmgpU=&nwkskey=99D58493D1205B43EFF938F0F66C339E&appskey=0A501524F8EA5FCBF9BDB5AD7D126F75">Uplink with secret keys<code>QK4TBCaAAAABb4ldmIEHFOMmgpU=</code></a></li> <li><a href="?data=40531E012680664601457090ED25&nwkskey=7A47F143D7CEF033DFA0D4B75E04A316&appskey=F1B0B1D3CC529C55C3019A46EF4582EA">Uplink with secret keys and large uplink counter<code>40531E012680664601457090ED25</code></a></li> <li><a href="?data=60A5280126000200011D8B658839&nwkskey=15641BC99EBBD238E5D9D83D3D5313C5&appskey=B184F94678DD69F3C83C2525CD3938B3">Downlink with secret keys<code>60A5280126000200011D8B658839</code></a></li> </ul> `; } res.setHeader("Content-Type", "text/html"); res.send(` <!doctype html> <html lang=en> <head> <meta charset=utf-8> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>LoRaWAN 1.0.x packet decoder</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> </head> <body> <div class="container-fluid"> <div class="row"> <div class="col-xs-12 col-sm-12 col-md-10 col-md-offset-1"> <h1>LoRaWAN 1.0.x packet decoder</h1> <p class="lead">A frontend towards <a href="https://github.com/anthonykirby/lora-packet">lora-packet</a>.</p> <form> <div class="row"> <div class="col-sm-10"> <label for="data">Base64 or hex-encoded packet ${req.query.data ? '(<a href="?">examples</a>)' : ''}</label> <input id="data" name="data" class="form-control input-lg" type="text" placeholder='Base64 or hex-encoded packet' value="${req.query.data ? req.query.data : ''}"> </div> <div class="hidden-xs col-sm-2"> <label for="submit">&nbsp;</label> <button id="submit" class="btn btn-default btn-lg col-sm-12" type="submit">Decode</button> </div> </div> <br> <div class="row"> <div class="col-sm-6"> <label for="nwkskey">Secret NwkSKey (hex-encoded; optional)</label> <input id="nwkskey" name="nwkskey" class="form-control input" type="text" placeholder='Network Session Key' value="${req.query.nwkskey ? req.query.nwkskey : ''}"> </div> <div class="col-sm-6"> <label for="appskey">Secret AppSKey (hex-encoded; optional)</label> <input id="appskey" name="appskey" class="form-control input" type="text" placeholder='Application (Session) Key' value="${req.query.appskey ? req.query.appskey : ''}"> </div> <div class="col-sm-12"> <p>Specify the secrets if you want to validate the MIC and decrypt the payload. For an OTAA Join Request enter the AppKey in the AppSKey field; for an OTAA Join Accept see the explaination below. Secrets are sent to the server and might be stored in log files of RunKit.</p> </div> </div> </form> <div class="row">&nbsp;</div> <div class="row"> <div class="col-xs-12"> ${decoded} </div> </div> <div class="row"> <div class="col-sm-12"> <p>Beware that LoRaWAN packets are binary data, so do not remove leading zeroes. Also note that erroneous packets (including packets that are just LoRa, not LoRaWAN) are likely to show incorrect results without any warning, so specify the secrets to validate the Message Integrity Code (MIC).</p> <p>If the 4 bytes MIC is valid, then it's safe to assume the packet has not been altered, that it is indeed a LoRaWAN packet, and that the secret NwkSKey is valid too. But even then one cannot tell if the secret AppSKey is valid as well; specifying the wrong AppSKey simply yields different decryption results.</p> <p>In LoRaWAN 1.0.x, the value of FCnt only holds the 16 least-significant bits (LSB) of the actual frame counter. But for a 32 bits frame counter still all 32 bits are used when calculating the MIC. So, a LoRaWAN server needs to guess or try the other 16 bits when validating the MIC. Such server can use its own internal counters for a best guess, and as LoRaWAN defines a maximum allowed gap between the last known value and current value (MAX_FCNT_GAP, being 16,384), the server will probably only try one additional value for the MSB. Above, boldly all possible 65,536 values for MSB are tried until a valid MIC is found.</p> <p>Note that a DevAddr is not unique, and a provider will need to try to validate the MIC using the NwkSKey of all devices it knows for a given DevAddr, until it finds a valid MIC. Only when it finds a valid MIC it knows which device the packet belongs to. Beware that in The Things Network, failing to find a match will NOT be reported in the trace part of the gateway's Traffic page in TTN Console.</p> <p>OTAA Join Requests are not encrypted; specify the AppKey in the field of AppSKey, and leave NwkSKey empty to validate the MIC.</p> <p>OTAA Join Accepts cannot be verified or decrypted above, as <a href="https://runkit.com/avbentem/deciphering-a-lorawan-otaa-join-accept">those need some additional data</a> to validate their MIC and derive the secret session keys.</p> <p>The output above is the standard output of <a href="https://github.com/anthonykirby/lora-packet">lora-packet</a> with some enhancements if the secrets are known, to show if the MIC is valid, brute-forcing the 32 bits frame counter, and to show the decrypted payload.</p> <p>lora-packet can also be installed as a command line utility, and since 0.7.7 can also validate/decrypt then:</p> <ul> <li><a href="https://docs.npmjs.com/getting-started/installing-node">install Node.js and npm</a></li> <li>run: <code>npm install -g lora-packet</code></li> <li>run: <code>lora-packet-decode --base64 ADFGUkFEshgAdAoAAACyGADXQ5rzpZs=</code><br/>or <code>lora-packet-decode --hex 003146524144B21800740A000000B21800D7439AF3A59B</code></li> </ul> <p>The maintainer of this page is not affiliated with lora-packet. See & star <a href="https://github.com/anthonykirby/lora-packet">https://github.com/anthonykirby/lora-packet</a>. </div> </div> </div> </div> </div> </body> </html> `); }); app.get("/:data", (req, res) => { res.setHeader("Content-Type", "text/plain"); res.send(decode(req.params.data) + "\nDecoded using https://github.com/anthonykirby/lora-packet"); }); // RunKit will print the last value; nothing to see here. let dummy = "";

no comments

    sign in to comment