Custom CODEC , using Crypto/AES and variables

Hi to all,
I set my first ChirpStack with two test lorawan-gateways and two test lorawan-meters , OTAA activated.

I can receive data form the device, but the payload is encrypted.

If I get the payload, and decrypt and decode I can obtain the correct measurements, the ones I’d like to see in ChirpStack itself !

And … there is a different key for each device, and I put these keys

So my custom decodeUplink() function should be able to access the device variables and the crypto functions, so here my two questions :

1 - import cryptolib
I cannot use require(‘crypto’) inside a custom codec function ? (I’m receiving an error)

2 - internal AES functoins
I have to do a aes-cbc(input.variables.,aesKey).
May I use some chirpstack internal public function to decrypt or I have to use my own ones?

Thanks for any reply and advice. :wink:

I am a little confused by your ask here. Does your device encrypt it’s messages with another layer of encryption before the typical LoRaWAN encryption with the networkSessionKey and applicationSessionKey? As Chirpstack already decrypts the packets to the normal LoRaWAN standards when it reaches your codec, so what you are dealing with in the codec is just the plaintext bits of what the device sends.

If you do have some other encryption layer, then no you cannot decode it in your codec, unless you directly hardcode the keys into the codec (but then it would only work for a single device). You cannot use require() or any other modules in a Chirpstack codec (to my knowledge) and theres no other hidden functions at your disposal besides base JS.

So if it is the case you have another layer of encryption you would have to write your own decryption function in the codec. Although it would likely be easier to just use one of the integrations to pipe the messages to a platform that can decrypt it.

But if you are just wondering about the typical lorawan decryption, the packets are already decrypted by that stage. The codecs job is just to parse those bits into a readable JSON string.

1 Like

Yes, the devices are encrypting the payload AES-128-CBC with a aesKey for each device before sending, then the lorawan packet is encrypted AES-128-CTR as usal.
Chirpstack does its job and return the payload on field “data” that is stil encrypted.
For example, this is a “data” value returned

+jRQhpCTu/b9EfVJ8fiCSpamRkKVrTYJOUapbIoNrSqdw7R08UQklVa/i+I/9MIuAAAA

base64decoded to (hexstring)

fa 34 50 86 90 93 bb f6 fd 11 f5 49 f1 f8 82 4a 96 a6 46 42 95 ad 36 09 39 46 a9 6c 8a 0d ad 2a 9d c3 b4 74 f1 44 24 95 56 bf 8b e2 3f f4 c2 2e 00 00 00

for this device the aesKey is 8B36B98FD649199BDC1827A320CF8483 and after AES-128-CBC decrypt I obtain (hexstring)

01 77 83 25 68 10 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 01 00 00 10 40 02 00 00 d4 00 06 00 00 00 00 00 00 00 00 00

it’s the payload I can really decode
01 version
77832568 timestamp
10 status
0a000000 volume at 01:00
00000000000000 deltaVolumes at 02:00 03:00 04:00 05:00
00000000000000 deltaVolumes at 06:00 07:00 08:00 09:00
00000040010000 deltaVolumes at 10:00 11:00 12:00 13:00
1040020000d400 deltaVolumes at 14:00 15:00 16:00 17:00
06000000000000 deltaVolumes at 18:00 19:00 20:00 21:00
000000 deltaVolumes at 22:00

I’ve written nodJS script to decrypt/decode the payload and I obtain this

{
data: {
frameVersion: 1,
currentDateTime: ‘2025-05-15T06:02:31.000Z’,
statusCode: 16,
status: ‘Empty spool; negative flow; leakage; burst; freeze’,
baseVolume: 10,
deltaVolumes: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 16, 9, 0, 53, 6, 0, 0, 0, 0, 0, 0 ],
volume: 114
}
}

So … I don’t know if there are other devices in the world that encrypt their payload but it

[FEATURE REQUEST ON]
*it would be cool (and not difficult to implement) to have a standard variables in device configuration like *

  • encryptionCipher : [none, aes-128-cbc, aes-128-ecb, aes-128-ctr, aes-256… ]*
  • encryptionNonce : [hex-string or base64]*
  • encryptionKey : [hex-value or base64]*

to receive the payload already decrypted, just to be simply decoded
[FEATURE REQUEST OFF]

So, back to my own problem.
You sated that :

  • I cannot use extenral libraries
  • there are no exported crypto functions (also this feature would be cool, just a crypto object instanced and available to decodeUplink() function)
  • all the decrypt/decode job must be done inside the decodeUplink() function in pure javascript

So, if I want to see a decoded object and the real metrics inside ChirpStack I have to rewrite aes function in pure javascript inside the decodeUplink() function … isn’t it?

Ok … not very comfortable, but neither impossible.
I’m back in the afternoon and tell you how it went.

Meanwhile , if anyone (developers? where are you?) has other info or advice or something … please do not esitate.

Here I am, again

All done, it seems to work too! :slight_smile:

I hand only one problem (and it’s really unconfortable to debug the codec function!).
While in nodeJs everything is working, inside the codec function it gave stange errors (functions not defined), so I put all the subfunctions inside the decodeUplink() and errors disappeared.
So … not very comfortable, but neither impossible.
But … I think the payload decrypting functions should be embeded into Chripstack, or at leasy crypto should be exported, to be used inside the custom codec function.

For the curious, I put the aesKey of my test-device into device->configuration->variables : aesKey and this is the skeleton of my codec function.

function decodeUplink(input) {

  function aesDecrypt(inputBytes, aesKey) {
    ...
  }

  function parseDeltaVolumes(cnt, combined) {
    const r = [];
    for (let i = 0; i < cnt; i++) { r.push(Number(combined & BigInt("0x3FFF"))); combined >>= BigInt(14); }
    return r;
  }

  function parseStatus(code) {
    const p = [];
    if ((code & 0x04) === 0x04) p.push("Low battery");
    if ((code & 0x08) === 0x08) p.push("Hardware error; tamper");
    if ((code & 0x10) === 0x10) p.push("Empty spool; negative flow; leakage; burst; freeze");
    if ((code & 0xE0) === 0x20) p.push("Leakage");
    if ((code & 0xE0) === 0x60) p.push("Negative flow");
    if ((code & 0xE0) === 0x80) p.push("Freeze");
    if ((code & 0xE0) === 0xA0) p.push("Burst");
    if ([0x40, 0xC0, 0xE0].includes(code & 0xE0)) p.push("UNKNOWN ERROR");
    return p.join("|");
  }

  function decodePayload(fPort, data) {
    const payload = data.reduce((s, b) => s + ('00' + b.toString(16)).slice(-2), '');
    const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
    if (fPort==100) {
        const frameVersion = data[0];
        let dt = dataView.getUint32(1, true) ;
        const currentDateTime = new Date(1000 * dt).toISOString();
        dt = 86400 * Math.floor(dt / 86400);
        const volumeDateTime = new Date(1000 *dt).toISOString();
        dt = dt - 86400 + 3600;
        const baseVolumeDateTime = new Date(1000 *dt).toISOString();
        const statusCode = data[5];
        const status = parseStatus(statusCode);
        const baseVolume = dataView.getUint32(6, true);
        const hexStr = toLE(Array.prototype.map.call(data.slice(10, 48), b => b.toString(16).padStart(2, '0')).join(''));
        const deltaVolumes = parseDeltaVolumes(23, BigInt('0x' + hexStr));
        const volume = deltaVolumes.reduce((t, i) => t + i, baseVolume);
        return { payload, frameVersion, currentDateTime, statusCode, status, baseVolumeDateTime, volumeDateTime, baseVolume, deltaVolumes, volume };
    }
    else if (fPort==103) {
        const currentDateTime = new Date(dataView.getUint32(0, true) * 1000).toISOString();
        const statusCode = data[4];
        const status = parseStatus(statusCode);
        return { payload, currentDateTime, statusCode, status };
    }
    else return { payload };
  }

  return { data: decodePayload(input.fPort, aesDecrypt(input.bytes, input.variables.aesKey)) };
}

Thanks to @Liam_Philipp for his reply.

1 Like

Very neat solution, I was unaware that you could pull the device variables in codecs.