lora-packet
Version:
LoRa packet decoder/encoder
250 lines (214 loc) • 10.9 kB
text/typescript
import LoraPacket, { LorawanVersion } from "./LoraPacket";
import { reverseBuffer } from "./util";
import { AesCmac } from "aes-cmac";
// calculate MIC from payload
function calculateMIC(
payload: LoraPacket,
NwkSKey?: Buffer, //NwkSKey for DataUP/Down; SNwkSIntKey in data 1.1; SNwkSIntKey in Join 1.1
AppKey?: Buffer, //AppSKey for DataUP/Down; FNwkSIntKey in data 1.1; JSIntKey in Join 1.1
FCntMSBytes?: Buffer,
ConfFCntDownTxDrTxCh?: Buffer
): Buffer {
let LWVersion: LorawanVersion = LorawanVersion.V1_0;
if (payload.isJoinRequestMessage()) {
if (AppKey && AppKey.length !== 16) throw new Error("Expected a AppKey with length 16");
if (!payload.MHDR) throw new Error("Expected MHDR to be defined");
if (!payload.AppEUI) throw new Error("Expected AppEUI to be defined");
if (!payload.DevEUI) throw new Error("Expected DevEUI to be defined");
if (!payload.DevNonce) throw new Error("Expected DevNonce to be defined");
if (!payload.MACPayload) throw new Error("Expected DevNonce to be defined");
// const msgLen = payload.MHDR.length + payload.AppEUI.length + payload.DevEUI.length + payload.DevNonce.length;
// CMAC over MHDR | AppEUI | DevEUI | DevNonce
// the seperate fields are not in little-endian format, use the concatenated field
const cmacInput = Buffer.concat([payload.MHDR, payload.MACPayload]);
// CMAC calculation (as RFC4493)
let fullCmac = new AesCmac(AppKey).calculate(cmacInput);
if (!(fullCmac instanceof Buffer)) fullCmac = Buffer.from(fullCmac);
// only first 4 bytes of CMAC are used as MIC
const MIC = fullCmac.slice(0, 4);
return MIC;
} else if (payload.isReJoinRequestMessage()) {
if (payload.RejoinType[0] === 1 && (!AppKey || AppKey.length !== 16))
throw new Error("Expected a JSIntKey with length 16");
if ((payload.RejoinType[0] === 0 || payload.RejoinType[0] === 2) && (!NwkSKey || NwkSKey.length !== 16))
throw new Error("Expected a SNwkSIntKey with length 16");
if (AppKey && AppKey.length !== 16) throw new Error("Expected a AppKey with length 16");
if (!payload.MHDR) throw new Error("Expected MHDR to be defined");
if (!payload.RejoinType) throw new Error("Expected RejoinType to be defined");
if (!payload.NetID && !payload.AppEUI) throw new Error("Expected NetID or JoinEUI to be defined");
if (!payload.DevEUI) throw new Error("Expected DevEUI to be defined");
if (!payload.RJCount0 && !payload.RJCount1) throw new Error("Expected RJCount0 or RJCount1 to be defined");
// const msgLen = payload.MHDR.length + payload.AppEUI.length + payload.DevEUI.length + payload.DevNonce.length;
// CMAC over MHDR | AppEUI | DevEUI | DevNonce
// the seperate fields are not in little-endian format, use the concatenated field
const cmacInput = Buffer.concat([payload.MHDR, payload.MACPayload]);
// CMAC calculation (as RFC4493)
const calcKey = payload.RejoinType[0] === 1 ? AppKey : NwkSKey;
let fullCmac = new AesCmac(calcKey).calculate(cmacInput);
if (!(fullCmac instanceof Buffer)) fullCmac = Buffer.from(fullCmac);
// only first 4 bytes of CMAC are used as MIC
const MIC = fullCmac.slice(0, 4);
return MIC;
} else if (payload.isJoinAcceptMessage()) {
if (AppKey && AppKey.length !== 16) throw new Error("Expected a AppKey with length 16");
if (!payload.MHDR) throw new Error("Expected MHDR to be defined");
if (!payload.AppNonce) throw new Error("Expected AppNonce to be defined");
if (!payload.NetID) throw new Error("Expected NetID to be defined");
if (!payload.DevAddr) throw new Error("Expected DevAddr to be defined");
if (!payload.DLSettings) throw new Error("Expected DLSettings to be defined");
if (!payload.RxDelay) throw new Error("Expected RxDelay to be defined");
if (!payload.CFList) throw new Error("Expected CFList to be defined");
if (!payload.MACPayload) throw new Error("Expected MACPayload to be defined");
if (payload.getDLSettingsOptNeg()) LWVersion = LorawanVersion.V1_1;
let cmacInput: Buffer = Buffer.alloc(0);
let cmacKey: Buffer = AppKey;
if (LWVersion === LorawanVersion.V1_0) {
// const msgLen =
// payload.MHDR.length +
// payload.AppNonce.length +
// payload.NetID.length +
// payload.DevAddr.length +
// payload.DLSettings.length +
// payload.RxDelay.length +
// payload.CFList.length;
// CMAC over MHDR | AppNonce | NetID | DevAddr | DLSettings | RxDelay | CFList
// the seperate fields are not encrypted, use the encrypted concatenated field
cmacInput = Buffer.concat([payload.MHDR, payload.MACPayload]);
} else if (LWVersion === LorawanVersion.V1_1) {
if (!payload.JoinReqType) throw new Error("Expected JoinReqType to be defined");
if (!payload.JoinEUI) throw new Error("Expected JoinEUI to be defined");
if (!payload.DevNonce) throw new Error("Expected DevNonce to be defined");
if (!NwkSKey || NwkSKey.length !== 16) throw new Error("Expected a NwkSKey with length 16");
cmacKey = NwkSKey;
cmacInput = Buffer.concat([
payload.JoinReqType,
reverseBuffer(payload.JoinEUI),
reverseBuffer(payload.DevNonce),
payload.MHDR,
payload.MACPayload,
]);
}
// CMAC calculation (as RFC4493)
let fullCmac = new AesCmac(cmacKey).calculate(cmacInput);
if (!(fullCmac instanceof Buffer)) fullCmac = Buffer.from(fullCmac);
// only first 4 bytes of CMAC are used as MIC
const MIC = fullCmac.slice(0, 4);
return MIC;
} else {
// ConfFCntDownTxDrTxCh = ConfFCntDownTxDrTxCh || Buffer.alloc(2, 0);
if (NwkSKey && NwkSKey.length !== 16) throw new Error("Expected a NwkSKey with length 16");
if (payload.DevAddr && payload.DevAddr.length !== 4) throw new Error("Expected a payload DevAddr with length 4");
if (payload.FCnt && payload.FCnt.length !== 2) throw new Error("Expected a payload FCnt with length 2");
if (!payload.MHDR) throw new Error("Expected MHDR to be defined");
if (!payload.DevAddr) throw new Error("Expected DevAddr to be defined");
if (!payload.FCnt) throw new Error("Expected FCnt to be defined");
if (!payload.MACPayload) throw new Error("Expected MACPayload to be defined");
if (!FCntMSBytes) {
FCntMSBytes = Buffer.from("0000", "hex");
}
if (ConfFCntDownTxDrTxCh) {
if (!AppKey || AppKey?.length !== 16) throw new Error("Expected a FNwkSIntKey with length 16");
LWVersion = LorawanVersion.V1_1;
}
// if (NwkSKey && AppKey) {
// LWVersion = LorawanVersion.V1_1;
// }
let dir;
const isUplinkAndIs1_1 = payload.getDir() === "up" && LWVersion === LorawanVersion.V1_1;
const isDownlinkAndIs1_1 = payload.getDir() === "down" && LWVersion === LorawanVersion.V1_1;
if (payload.getDir() == "up") {
dir = Buffer.alloc(1, 0);
} else if (payload.getDir() == "down") {
dir = Buffer.alloc(1, 1);
if (!ConfFCntDownTxDrTxCh) {
ConfFCntDownTxDrTxCh = Buffer.alloc(4, 0);
} else if (ConfFCntDownTxDrTxCh && ConfFCntDownTxDrTxCh?.length !== 2) {
throw new Error("Expected a ConfFCntDown with length 2");
} else {
ConfFCntDownTxDrTxCh = Buffer.concat([ConfFCntDownTxDrTxCh, Buffer.alloc(2, 0)]);
}
} else {
throw new Error("expecting direction to be either 'up' or 'down'");
}
if (isUplinkAndIs1_1) {
if (!ConfFCntDownTxDrTxCh || ConfFCntDownTxDrTxCh?.length !== 4) {
throw new Error("Expected a ConfFCntDownTxDrTxCh with length 4 Expected ( ConfFCnt | TxDr | TxCh)");
}
if (payload.getFCtrlACK() || (isUplinkAndIs1_1 && payload.getFPort() === 0)) {
ConfFCntDownTxDrTxCh.writeUInt16BE(ConfFCntDownTxDrTxCh.readUInt16LE(0));
} else {
ConfFCntDownTxDrTxCh.writeUInt16BE(0);
}
}
const msgLen = payload.MHDR.length + payload.MACPayload.length;
const B0 = Buffer.concat([
Buffer.from([0x49]), // as spec
isDownlinkAndIs1_1 ? ConfFCntDownTxDrTxCh : Buffer.alloc(4, 0), // LoraWan Spec 1.1, pag. 27
dir, // direction ('Dir')
reverseBuffer(payload.DevAddr),
reverseBuffer(payload.FCnt),
FCntMSBytes, // upper 2 bytes of FCnt (zeroes)
Buffer.alloc(1, 0), // 0x00
Buffer.alloc(1, msgLen), // len(msg)
]);
// CMAC over B0 | MHDR | MACPayload
const cmacInput = Buffer.concat([B0, payload.MHDR, payload.MACPayload]);
// CMAC calculation (as RFC4493)
let key = NwkSKey;
if (isDownlinkAndIs1_1) key = AppKey;
let fullCmac = new AesCmac(key).calculate(cmacInput);
if (!(fullCmac instanceof Buffer)) fullCmac = Buffer.from(fullCmac);
// only first 4 bytes of CMAC are used as MIC
const MIC = fullCmac.slice(0, 4);
if (isUplinkAndIs1_1) {
const B1 = Buffer.concat([
Buffer.from([0x49]), // as spec
ConfFCntDownTxDrTxCh, // LoraWan Spec 1.1, pag. 27
dir, // direction ('Dir')
reverseBuffer(payload.DevAddr),
reverseBuffer(payload.FCnt),
FCntMSBytes, // upper 2 bytes of FCnt (zeroes)
Buffer.alloc(1, 0), // 0x00
Buffer.alloc(1, msgLen), // len(msg)
]);
const cmacSInput = Buffer.concat([B1, payload.MHDR, payload.MACPayload]);
let fullCmacS = new AesCmac(AppKey).calculate(cmacSInput);
if (!(fullCmacS instanceof Buffer)) fullCmacS = Buffer.from(fullCmacS);
// only first 2 bytes of CMAC and CMACS are used as MIC
const MICS = fullCmacS.slice(0, 4);
return Buffer.concat([MICS.slice(0, 2), MIC.slice(0, 2)]);
}
return MIC;
}
}
// verify is just calculate & compare
function verifyMIC(
payload: LoraPacket,
NwkSKey?: Buffer,
AppKey?: Buffer,
FCntMSBytes?: Buffer,
ConfFCntDownTxDrTxCh?: Buffer
): boolean {
if (payload.MIC && payload.MIC.length !== 4) throw new Error("Expected a payload payload.MIC with length 4");
const calculated = calculateMIC(payload, NwkSKey, AppKey, FCntMSBytes, ConfFCntDownTxDrTxCh);
if (!payload.MIC) return false;
return Buffer.compare(payload.MIC, calculated) === 0;
}
// calculate MIC & store
function recalculateMIC(
payload: LoraPacket,
NwkSKey?: Buffer,
AppKey?: Buffer,
FCntMSBytes?: Buffer,
ConfFCntDownTxDrTxCh?: Buffer
): void {
const calculated = calculateMIC(payload, NwkSKey, AppKey, FCntMSBytes, ConfFCntDownTxDrTxCh);
payload.MIC = calculated;
if (!payload.MHDR) throw new Error("Missing MHDR");
if (!payload.MACPayload) throw new Error("Missing MACPayload");
if (!payload.MIC) throw new Error("Missing MIC");
if (!payload.MHDR) throw new Error("Missing MHDR");
payload.PHYPayload = Buffer.concat([payload.MHDR, payload.MACPayload, payload.MIC]);
payload.MACPayloadWithMIC = payload.PHYPayload.slice(payload.MHDR.length, payload.PHYPayload.length);
}
export { calculateMIC, verifyMIC, recalculateMIC };