// 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);
}