lora-packet
Version:
LoRa packet decoder/encoder
305 lines (260 loc) • 9.96 kB
text/typescript
import LoraPacket from "./LoraPacket";
import { reverseBuffer } from "./util";
import CryptoJS from "crypto-js";
const LORAIV = CryptoJS.enc.Hex.parse("00000000000000000000000000000000");
enum KeyType11 {
FNwkSIntKey = "01",
AppSKey = "02",
SNwkSIntKey = "03",
NwkSEncKey = "04",
}
enum KeyType10 {
NwkSKey = "01",
AppSKey = "02",
}
enum KeyTypeJS {
JSIntKey = "06",
JSEncKey = "05",
}
enum KeyTypeWORSession {
WorSIntKey = "01",
WorSEncKey = "02",
}
function decrypt(payload: LoraPacket, AppSKey?: Buffer, NwkSKey?: Buffer, fCntMSB32?: Buffer): Buffer {
if (!payload.PHYPayload || !payload.FRMPayload) throw new Error("Payload was not defined");
if (!fCntMSB32) fCntMSB32 = Buffer.alloc(2, 0);
const blocks = Math.ceil(payload.FRMPayload.length / 16);
const sequenceS = Buffer.alloc(16 * blocks);
for (let block = 0; block < blocks; block++) {
const ai = _metadataBlockAi(payload, block, fCntMSB32);
ai.copy(sequenceS, block * 16);
}
const key = payload.getFPort() === 0 ? NwkSKey : AppSKey;
if (!key || key.length !== 16) throw new Error("Expected a appropriate key with length 16");
const cipherstream_base64 = CryptoJS.AES.encrypt(
CryptoJS.enc.Hex.parse(sequenceS.toString("hex")),
CryptoJS.enc.Hex.parse(key.toString("hex")),
{
mode: CryptoJS.mode.ECB,
iv: LORAIV,
padding: CryptoJS.pad.NoPadding,
}
);
const cipherstream = Buffer.from(cipherstream_base64.toString(), "base64");
const plaintextPayload = Buffer.alloc(payload.FRMPayload.length);
for (let i = 0; i < payload.FRMPayload.length; i++) {
const Si = cipherstream.readUInt8(i);
plaintextPayload.writeUInt8(Si ^ payload.FRMPayload.readUInt8(i), i);
}
return plaintextPayload;
}
// Check
function decryptJoin(payload: LoraPacket, AppKey: Buffer): Buffer {
if (!payload?.MACPayloadWithMIC) throw new Error("Expected parsed payload to be defined");
if (AppKey.length !== 16) throw new Error("Expected a appropriate key with length 16");
const cipherstream = CryptoJS.AES.decrypt(
payload.MACPayloadWithMIC.toString("base64"),
CryptoJS.enc.Hex.parse(AppKey.toString("hex")),
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding,
}
);
return Buffer.from(cipherstream.toString(), "hex");
}
function decryptFOpts(payload: LoraPacket, NwkSEncKey: Buffer, fCntMSB32?: Buffer): Buffer {
if (!fCntMSB32) fCntMSB32 = Buffer.alloc(2);
if (!payload?.FOpts) throw new Error("Expected FOpts to be defined");
if (!payload?.DevAddr) throw new Error("Expected DevAddr to be defined");
if (NwkSEncKey.length !== 16) throw new Error("Expected a appropriate key with length 16");
if (!payload.FCnt) throw new Error("Expected FCnt to be defined");
const direction: Buffer = Buffer.alloc(1);
let aFCntDown = false;
if (payload.getDir() == "up") {
direction.writeUInt8(0, 0);
} else if (payload.getDir() == "down") {
direction.writeUInt8(1, 0);
if (payload.FPort != null && payload.getFPort() > 0) {
aFCntDown = true;
}
} else {
throw new Error("Decrypt error: expecting direction to be either 'up' or 'down'");
}
// https://lora-alliance.org/wp-content/uploads/2020/11/00001.002.00001.001.cr-fcntdwn-usage-in-fopts-encryption-v2-r1.pdf
const aBuffer = Buffer.concat([
Buffer.alloc(1, 1),
Buffer.alloc(3, 0),
Buffer.alloc(1, aFCntDown ? 2 : 1),
direction,
reverseBuffer(payload.DevAddr),
reverseBuffer(payload.FCnt),
fCntMSB32,
Buffer.alloc(1, 0),
Buffer.alloc(1, 1),
]);
const cipherstream_base64 = CryptoJS.AES.encrypt(
CryptoJS.enc.Hex.parse(aBuffer.toString("hex")),
CryptoJS.enc.Hex.parse(NwkSEncKey.toString("hex")),
{
mode: CryptoJS.mode.ECB,
iv: LORAIV,
padding: CryptoJS.pad.NoPadding,
}
);
const cipherstream = Buffer.from(cipherstream_base64.toString(), "base64");
const plaintextPayload = Buffer.alloc(payload.FOpts.length);
for (let i = 0; i < payload.FOpts.length; i++) {
plaintextPayload[i] = cipherstream[i] ^ payload.FOpts[i];
}
return plaintextPayload;
}
function generateKey(
key: Buffer,
AppNonce: Buffer,
NetIdOrJoinEui: Buffer,
DevNonce: Buffer,
keyType: KeyType11 | KeyType10 | KeyTypeJS | KeyTypeWORSession
): Buffer {
let keyNonceStr: string = keyType;
keyNonceStr += reverseBuffer(AppNonce).toString("hex");
keyNonceStr += reverseBuffer(NetIdOrJoinEui).toString("hex");
keyNonceStr += reverseBuffer(DevNonce).toString("hex");
keyNonceStr = keyNonceStr.padEnd(32, "0");
const keyNonce = Buffer.from(keyNonceStr, "hex");
const nwkSKey_base64 = CryptoJS.AES.encrypt(
CryptoJS.enc.Hex.parse(keyNonce.toString("hex")),
CryptoJS.enc.Hex.parse(key.toString("hex")),
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding,
}
);
return Buffer.from(nwkSKey_base64.toString(), "base64");
}
function generateSessionKeys(
AppKey: Buffer,
NetId: Buffer,
AppNonce: Buffer,
DevNonce: Buffer
): { AppSKey: Buffer; NwkSKey: Buffer } {
return generateSessionKeys10(AppKey, NetId, AppNonce, DevNonce);
}
function generateSessionKeys10(
AppKey: Buffer,
NetId: Buffer,
AppNonce: Buffer,
DevNonce: Buffer
): { AppSKey: Buffer; NwkSKey: Buffer } {
if (AppKey.length !== 16) throw new Error("Expected a AppKey with length 16");
if (NetId.length !== 3) throw new Error("Expected a NetId with length 3");
if (AppNonce.length !== 3) throw new Error("Expected a AppNonce with length 3");
if (DevNonce.length !== 2) throw new Error("Expected a DevNonce with length 2");
return {
AppSKey: generateKey(AppKey, AppNonce, NetId, DevNonce, KeyType10.AppSKey),
NwkSKey: generateKey(AppKey, AppNonce, NetId, DevNonce, KeyType10.NwkSKey),
};
}
function generateSessionKeys11(
AppKey: Buffer,
NwkKey: Buffer,
JoinEUI: Buffer,
AppNonce: Buffer,
DevNonce: Buffer
): { AppSKey: Buffer; FNwkSIntKey: Buffer; SNwkSIntKey: Buffer; NwkSEncKey: Buffer } {
if (AppKey.length !== 16) throw new Error("Expected a AppKey with length 16");
if (NwkKey.length !== 16) throw new Error("Expected a NwkKey with length 16");
if (AppNonce.length !== 3) throw new Error("Expected a AppNonce with length 3");
if (DevNonce.length !== 2) throw new Error("Expected a DevNonce with length 2");
return {
AppSKey: generateKey(AppKey, AppNonce, JoinEUI, DevNonce, KeyType11.AppSKey),
FNwkSIntKey: generateKey(NwkKey, AppNonce, JoinEUI, DevNonce, KeyType11.FNwkSIntKey),
SNwkSIntKey: generateKey(NwkKey, AppNonce, JoinEUI, DevNonce, KeyType11.SNwkSIntKey),
NwkSEncKey: generateKey(NwkKey, AppNonce, JoinEUI, DevNonce, KeyType11.NwkSEncKey),
};
}
function generateJSKeys(NwkKey: Buffer, DevEui: Buffer): { JSIntKey: Buffer; JSEncKey: Buffer } {
if (DevEui.length !== 8) throw new Error("Expected a DevEui with length 8");
if (NwkKey.length !== 16) throw new Error("Expected a NwkKey with length 16");
return {
JSIntKey: generateKey(NwkKey, DevEui, Buffer.alloc(0), Buffer.alloc(0), KeyTypeJS.JSIntKey),
JSEncKey: generateKey(NwkKey, DevEui, Buffer.alloc(0), Buffer.alloc(0), KeyTypeJS.JSEncKey),
};
}
function generateWORKey(NwkSKey: Buffer): { RootWorSKey: Buffer } {
if (NwkSKey.length !== 16) throw new Error("Expected a NwkKey/NwkSEncKey with length 16");
return {
RootWorSKey: generateKey(NwkSKey, Buffer.alloc(0), Buffer.alloc(0), Buffer.alloc(0), KeyTypeWORSession.WorSIntKey),
};
}
function generateWORSessionKeys(RootWorSKey: Buffer, DevAddr: Buffer): { WorSIntKey: Buffer; WorSEncKey: Buffer } {
if (DevAddr.length !== 4) throw new Error("Expected a DevAddr with length 4");
if (RootWorSKey.length !== 16) throw new Error("Expected a RootWorSKey with length 16");
return {
WorSIntKey: generateKey(RootWorSKey, DevAddr, Buffer.alloc(0), Buffer.alloc(0), KeyTypeWORSession.WorSIntKey),
WorSEncKey: generateKey(RootWorSKey, DevAddr, Buffer.alloc(0), Buffer.alloc(0), KeyTypeWORSession.WorSEncKey),
};
}
function encrypt(buffer: Buffer, key: Buffer): Buffer {
// CHECK
const ciphertext = CryptoJS.AES.encrypt(
CryptoJS.enc.Hex.parse(buffer.toString("hex")),
CryptoJS.enc.Hex.parse(key.toString("hex")),
{
mode: CryptoJS.mode.ECB,
iv: LORAIV,
padding: CryptoJS.pad.NoPadding,
}
).ciphertext.toString(CryptoJS.enc.Hex);
return Buffer.from(ciphertext, "hex");
}
function decryptJoinAccept(payload: LoraPacket, appKey: Buffer): Buffer {
const payloadBuffer = payload.PHYPayload || Buffer.alloc(0);
// Check
const mhdr = payloadBuffer.slice(0, 1);
const joinAccept = encrypt(payloadBuffer.slice(1), appKey);
return Buffer.concat([mhdr, joinAccept]);
}
// Encrypt stream mixes in metadata blocks, as Ai =
// 0x01
// 0x00 0x00 0x00 0x00
// direction-uplink/downlink [1]
// DevAddr [4]
// FCnt as 32-bit, lsb first [4]
// 0x00
// counter = i [1]
function _metadataBlockAi(payload: LoraPacket, blockNumber: number, fCntMSB32?: Buffer): Buffer {
if (!fCntMSB32) fCntMSB32 = Buffer.alloc(2);
let direction;
if (payload.getDir() == "up") {
direction = Buffer.alloc(1, 0);
} else if (payload.getDir() == "down") {
direction = Buffer.alloc(1, 1);
} else {
throw new Error("Decrypt error: expecting direction to be either 'up' or 'down'");
}
if (!payload.DevAddr) throw new Error("Decrypt error: DevAddr not defined'");
if (!payload.FCnt) throw new Error("Decrypt error: FCnt not defined'");
const aiBuffer = Buffer.concat([
Buffer.from("0100000000", "hex"),
direction,
reverseBuffer(payload.DevAddr),
reverseBuffer(payload.FCnt),
fCntMSB32,
Buffer.alloc(1, 0),
Buffer.alloc(1, blockNumber + 1),
]);
return aiBuffer;
}
export {
encrypt,
decrypt,
decryptJoin,
decryptFOpts,
decryptJoinAccept,
generateSessionKeys,
generateSessionKeys11,
generateSessionKeys10,
generateWORKey,
generateWORSessionKeys,
generateJSKeys,
};