node-red-contrib-meshtastic-advanced 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decrypt.js","sources":["../../../src/nodes/decrypt/decrypt.ts"],"sourcesContent":["/**\n * Meshtastic Decrypt Node\n * Decrypts encrypted Meshtastic packets (both channel PSK and direct message PKC)\n */\n\nimport type { Node, NodeDef, NodeInitializer } from 'node-red';\nimport { Protobuf } from '@meshtastic/protobufs';\nimport {\n decryptChannelMessage,\n decryptDirectMessage,\n deriveSharedSecret,\n parseKey,\n parsePrivateKey,\n parsePublicKey,\n isDefaultKey,\n decodeServiceEnvelope,\n decodeData,\n MeshtasticDecryptError,\n validateNodeNumber,\n validatePacketId,\n ensureBuffer,\n} from '../../lib';\n\ninterface DecryptConfig extends NodeDef {\n name: string;\n mode: 'auto' | 'channel' | 'dm';\n keyFormat: 'base64' | 'hex';\n outputFormat: 'buffer' | 'object';\n}\n\ninterface DecryptCredentials {\n channelKey?: string;\n privateKey?: string;\n publicKeys?: string; // JSON string of nodeNum -> publicKey mapping\n}\n\nconst nodeInit: NodeInitializer = (RED): void => {\n function DecryptNodeConstructor(this: Node, config: DecryptConfig): void {\n RED.nodes.createNode(this, config);\n const node = this;\n\n // Get credentials\n const credentials = RED.nodes.getCredentials(config.id) as DecryptCredentials | undefined;\n\n // Parse channel key if provided\n let channelKey: Buffer | null = null;\n if (credentials?.channelKey) {\n try {\n channelKey = parseKey(credentials.channelKey, config.keyFormat || 'base64');\n\n // Warn if using default key\n if (isDefaultKey(channelKey)) {\n node.warn(\n 'Using default Meshtastic encryption key (AQ==). ' +\n 'This is INSECURE and should only be used for testing. ' +\n 'Please configure a secure channel key.'\n );\n }\n } catch (error) {\n node.error(`Invalid channel key: ${error instanceof Error ? error.message : String(error)}`);\n channelKey = null;\n }\n }\n\n // Parse private key if provided\n let privateKey: Buffer | null = null;\n if (credentials?.privateKey) {\n try {\n privateKey = parsePrivateKey(credentials.privateKey, config.keyFormat || 'base64');\n } catch (error) {\n node.error(`Invalid private key: ${error instanceof Error ? error.message : String(error)}`);\n privateKey = null;\n }\n }\n\n // Parse public keys mapping if provided\n let publicKeysMap: Map<number, Buffer> = new Map();\n if (credentials?.publicKeys) {\n try {\n const publicKeysObj = JSON.parse(credentials.publicKeys);\n for (const [nodeNumStr, keyStr] of Object.entries(publicKeysObj)) {\n const nodeNum = parseInt(nodeNumStr, 10);\n const publicKey = parsePublicKey(keyStr as string, config.keyFormat || 'base64');\n publicKeysMap.set(nodeNum, publicKey);\n }\n } catch (error) {\n node.error(`Invalid public keys mapping: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n node.on('input', (msg: any, send, done) => {\n try {\n // Validate input\n if (!Buffer.isBuffer(msg.payload) && !(msg.payload instanceof Uint8Array)) {\n throw new Error('Input payload must be a Buffer or Uint8Array');\n }\n\n // Decode ServiceEnvelope\n const envelope = decodeServiceEnvelope(ensureBuffer(msg.payload));\n\n if (!envelope.packet) {\n throw new Error('No packet in ServiceEnvelope');\n }\n\n const packet = envelope.packet;\n\n // Check if packet is encrypted\n if (packet.payloadVariant.case !== 'encrypted') {\n node.warn('Packet is not encrypted, passing through without decryption');\n\n const output = {\n ...msg,\n payload: config.outputFormat === 'object' ? envelope : ensureBuffer(msg.payload),\n _meshtastic: {\n encrypted: false,\n from: packet.from,\n to: packet.to,\n channel: packet.channel,\n packetId: packet.id,\n },\n };\n\n send(output);\n done();\n return;\n }\n\n // Extract encrypted data\n const encryptedData = ensureBuffer(packet.payloadVariant.value);\n\n // Validate required fields\n validateNodeNumber(packet.from, 'from');\n validatePacketId(packet.id, 'packetId');\n\n let decryptedData: Buffer;\n let usedMode: 'channel' | 'dm';\n\n // Determine encryption mode\n const mode = config.mode || 'auto';\n\n // Check for PKI encryption flag (if it exists in the packet)\n // Note: pkiEncrypted flag may not be present in all implementations\n const isPkiEncrypted = (packet as any).pkiEncrypted === true;\n\n if (mode === 'auto') {\n // Auto-detect: try PKC if flag is set, otherwise use channel\n if (isPkiEncrypted) {\n usedMode = 'dm';\n } else {\n usedMode = 'channel';\n }\n } else {\n usedMode = mode;\n }\n\n // Decrypt based on mode\n if (usedMode === 'dm') {\n // Direct Message (PKC) decryption\n if (!privateKey) {\n throw new MeshtasticDecryptError('Direct message decryption requires a private key');\n }\n\n // Get sender's public key\n const senderPublicKey = publicKeysMap.get(packet.from);\n if (!senderPublicKey) {\n throw new MeshtasticDecryptError(\n `No public key found for sender node ${packet.from}. ` +\n `Configure the sender's public key in the node settings.`\n );\n }\n\n // Derive shared secret\n const sharedSecret = deriveSharedSecret(privateKey, senderPublicKey);\n\n // Extract nonce and auth tag from packet\n // Note: The exact format depends on Meshtastic implementation\n // This is a simplified version - actual implementation may need adjustment\n const nonceLength = 12;\n const authTagLength = 16;\n\n if (encryptedData.length < nonceLength + authTagLength) {\n throw new MeshtasticDecryptError('Encrypted data too short for DM decryption');\n }\n\n const nonce = encryptedData.subarray(0, nonceLength);\n const ciphertext = encryptedData.subarray(nonceLength, -authTagLength);\n const authTag = encryptedData.subarray(-authTagLength);\n\n // Decrypt\n decryptedData = decryptDirectMessage(ciphertext, sharedSecret, nonce, authTag);\n } else {\n // Channel (PSK) decryption\n if (!channelKey) {\n throw new MeshtasticDecryptError('Channel decryption requires a channel key');\n }\n\n decryptedData = decryptChannelMessage(\n encryptedData,\n channelKey,\n packet.from,\n packet.id\n );\n }\n\n // Decode decrypted data as Data protobuf\n const decodedData = decodeData(decryptedData);\n\n // Update packet with decrypted data\n const decryptedEnvelope: Protobuf.Mqtt.ServiceEnvelope = {\n ...envelope,\n packet: {\n ...packet,\n payloadVariant: {\n case: 'decoded',\n value: decodedData,\n },\n },\n };\n\n // Prepare output\n const output = {\n ...msg,\n payload: config.outputFormat === 'object'\n ? decryptedEnvelope\n : ensureBuffer(Protobuf.Mqtt.ServiceEnvelope.toBinary(decryptedEnvelope)),\n _meshtastic: {\n encrypted: false,\n decryptedWith: usedMode,\n from: packet.from,\n to: packet.to,\n channel: packet.channel,\n packetId: packet.id,\n },\n };\n\n send(output);\n done();\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n node.error(`Decryption failed: ${errorMsg}`, msg);\n done(error instanceof Error ? error : new Error(errorMsg));\n }\n });\n }\n\n RED.nodes.registerType('meshtastic-decrypt', DecryptNodeConstructor, {\n credentials: {\n channelKey: { type: 'password' },\n privateKey: { type: 'password' },\n publicKeys: { type: 'password' },\n },\n });\n};\n\nexport default nodeInit;\n"],"names":["parseKey","isDefaultKey","parsePrivateKey","parsePublicKey","decodeServiceEnvelope","ensureBuffer","output","validateNodeNumber","validatePacketId","MeshtasticDecryptError","deriveSharedSecret","decryptDirectMessage","decryptChannelMessage","decodeData","Protobuf"],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('meshtastic-encode', {
3
+ category: 'meshtastic',
4
+ color: '#3FADD7',
5
+ defaults: {
6
+ name: { value: '' },
7
+ defaultTo: { value: 4294967295 }, // BROADCAST_ADDR
8
+ defaultChannel: { value: 0 },
9
+ defaultHopLimit: { value: 3 },
10
+ outputFormat: { value: 'object' },
11
+ outputType: { value: 'envelope' }
12
+ },
13
+ inputs: 1,
14
+ outputs: 1,
15
+ icon: 'parser-json.svg',
16
+ label: function() {
17
+ return this.name || 'meshtastic-encode';
18
+ },
19
+ paletteLabel: 'encode'
20
+ });
21
+ </script>
22
+
23
+ <script type="text/html" data-template-name="meshtastic-encode">
24
+ <div class="form-row">
25
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
26
+ <input type="text" id="node-input-name" placeholder="Name">
27
+ </div>
28
+
29
+ <div class="form-row">
30
+ <label for="node-input-defaultTo"><i class="fa fa-bullseye"></i> Default To</label>
31
+ <input type="number" id="node-input-defaultTo" placeholder="4294967295 (broadcast)">
32
+ <div class="form-tips">
33
+ Destination node number. 4294967295 (0xFFFFFFFF) for broadcast.
34
+ </div>
35
+ </div>
36
+
37
+ <div class="form-row">
38
+ <label for="node-input-defaultChannel"><i class="fa fa-sitemap"></i> Default Channel</label>
39
+ <input type="number" id="node-input-defaultChannel" min="0" max="7" placeholder="0">
40
+ </div>
41
+
42
+ <div class="form-row">
43
+ <label for="node-input-defaultHopLimit"><i class="fa fa-share-alt"></i> Default Hop Limit</label>
44
+ <input type="number" id="node-input-defaultHopLimit" min="0" max="7" placeholder="3">
45
+ </div>
46
+
47
+ <div class="form-row">
48
+ <label for="node-input-outputType"><i class="fa fa-cube"></i> Output Type</label>
49
+ <select id="node-input-outputType">
50
+ <option value="envelope">ServiceEnvelope (full packet)</option>
51
+ <option value="data">Data only (for custom wrapping)</option>
52
+ </select>
53
+ </div>
54
+
55
+ <div class="form-row">
56
+ <label for="node-input-outputFormat"><i class="fa fa-file-o"></i> Output Format</label>
57
+ <select id="node-input-outputFormat">
58
+ <option value="object">Object</option>
59
+ <option value="buffer">Buffer (binary)</option>
60
+ </select>
61
+ </div>
62
+ </script>
63
+
64
+ <script type="text/html" data-help-name="meshtastic-encode">
65
+ <p>Encodes JSON messages to Meshtastic protobuf format.</p>
66
+
67
+ <h3>Inputs</h3>
68
+ <dl class="message-properties">
69
+ <dt>payload <span class="property-type">object</span></dt>
70
+ <dd>Object containing message to encode</dd>
71
+ <dt>payload.portNum <span class="property-type">string | number</span></dt>
72
+ <dd>Message type (e.g., "TEXT_MESSAGE_APP" or 1)</dd>
73
+ <dt>payload.message <span class="property-type">any</span></dt>
74
+ <dd>Message data (type depends on portNum)</dd>
75
+ <dt class="optional">payload.to <span class="property-type">number</span></dt>
76
+ <dd>Destination node (default: broadcast)</dd>
77
+ <dt class="optional">payload.channel <span class="property-type">number</span></dt>
78
+ <dd>Channel index 0-7 (default: 0)</dd>
79
+ <dt class="optional">payload.from <span class="property-type">number</span></dt>
80
+ <dd>Source node (default: 0, set by encrypt node)</dd>
81
+ <dt class="optional">payload.wantAck <span class="property-type">boolean</span></dt>
82
+ <dd>Request acknowledgment (default: false)</dd>
83
+ <dt class="optional">payload.hopLimit <span class="property-type">number</span></dt>
84
+ <dd>Max hops 0-7 (default: 3)</dd>
85
+ </dl>
86
+
87
+ <h3>Outputs</h3>
88
+ <dl class="message-properties">
89
+ <dt>payload <span class="property-type">Buffer | Object</span></dt>
90
+ <dd>Encoded protobuf (Data or ServiceEnvelope)</dd>
91
+ <dt>_meshtastic <span class="property-type">object</span></dt>
92
+ <dd>Metadata about encoding</dd>
93
+ </dl>
94
+
95
+ <h3>Details</h3>
96
+ <p>This node encodes JSON messages into Meshtastic protobuf format.</p>
97
+
98
+ <h4>Supported Message Types</h4>
99
+ <ul>
100
+ <li><b>TEXT_MESSAGE_APP</b>: Text string</li>
101
+ <li><b>POSITION_APP</b>: Position object with lat/lon/altitude</li>
102
+ <li><b>NODEINFO_APP</b>: User object with id/longName/shortName</li>
103
+ <li><b>TELEMETRY_APP</b>: Telemetry object</li>
104
+ <li><b>ADMIN_APP</b>: Admin command object</li>
105
+ <li>And more...</li>
106
+ </ul>
107
+
108
+ <h4>Output Type</h4>
109
+ <p><b>ServiceEnvelope</b>: Full packet ready for encryption or transmission.</p>
110
+ <p><b>Data only</b>: Just the encoded Data protobuf for custom processing.</p>
111
+
112
+ <h4>Default Values</h4>
113
+ <p>Configure default values for to, channel, and hopLimit. These can be overridden per message.</p>
114
+
115
+ <h3>Example Input</h3>
116
+ <pre>
117
+ {
118
+ portNum: "TEXT_MESSAGE_APP",
119
+ message: "Hello, mesh!",
120
+ to: 123456, // or 4294967295 for broadcast
121
+ channel: 0,
122
+ wantAck: false
123
+ }
124
+ </pre>
125
+
126
+ <h3>Example Input (Position)</h3>
127
+ <pre>
128
+ {
129
+ portNum: "POSITION_APP",
130
+ message: {
131
+ latitudeI: 378524200, // 37.8524200 * 1e7
132
+ longitudeI: -1223964700, // -122.3964700 * 1e7
133
+ altitude: 15,
134
+ time: 1234567890
135
+ }
136
+ }
137
+ </pre>
138
+
139
+ <h3>References</h3>
140
+ <ul>
141
+ <li><a href="https://meshtastic.org/docs/development/reference/protobufs/">Meshtastic Protobufs</a></li>
142
+ </ul>
143
+ </script>
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ require("crypto");
3
+ const decoder = require("../../decoder-BvBAtm2U.js");
4
+ const encoder = require("../../encoder-DU3wTIEA.js");
5
+ const messageTypes = require("../../message-types-FA26fPjF.js");
6
+ const validation = require("../../validation-BegQyBSh.js");
7
+ const nodeInit = (RED) => {
8
+ function EncodeNodeConstructor(config) {
9
+ RED.nodes.createNode(this, config);
10
+ const node = this;
11
+ node.on("input", (msg, send, done) => {
12
+ try {
13
+ if (!msg.payload || typeof msg.payload !== "object") {
14
+ throw new Error("Input payload must be an object");
15
+ }
16
+ const input = msg.payload;
17
+ if (!input.portNum && input.portNum !== 0) {
18
+ throw new decoder.MeshtasticEncodeError("portNum is required in payload");
19
+ }
20
+ const portNum = encoder.parsePortNum(input.portNum);
21
+ const portNumName = decoder.getPortNumName(portNum);
22
+ if (input.message === void 0 && input.message !== null) {
23
+ throw new decoder.MeshtasticEncodeError("message is required in payload");
24
+ }
25
+ const encodedPayload = encoder.encodeMessageByPortNum(portNum, input.message);
26
+ const data = encoder.createDataPayload(portNum, encodedPayload, {
27
+ wantResponse: input.wantResponse,
28
+ requestId: input.requestId,
29
+ emoji: input.emoji
30
+ });
31
+ const outputType = config.outputType || "envelope";
32
+ if (outputType === "data") {
33
+ const output2 = {
34
+ ...msg,
35
+ payload: config.outputFormat === "object" ? data : encoder.encodeData(data),
36
+ _meshtastic: {
37
+ portNum: portNumName,
38
+ portNumValue: portNum,
39
+ encoded: true
40
+ }
41
+ };
42
+ send(output2);
43
+ done();
44
+ return;
45
+ }
46
+ const to = input.to ?? config.defaultTo ?? messageTypes.BROADCAST_ADDR;
47
+ const channel = input.channel ?? config.defaultChannel ?? 0;
48
+ const hopLimit = input.hopLimit ?? config.defaultHopLimit ?? 3;
49
+ validation.validateNodeNumber(to, "to");
50
+ validation.validateChannelIndex(channel, "channel");
51
+ validation.validateHopLimit(hopLimit, "hopLimit");
52
+ const from = input.from ?? 0;
53
+ const packetId = input.packetId;
54
+ const envelope = encoder.createServiceEnvelope(
55
+ data,
56
+ from,
57
+ to,
58
+ channel,
59
+ packetId,
60
+ {
61
+ wantAck: input.wantAck,
62
+ hopLimit,
63
+ priority: input.priority,
64
+ channelId: input.channelId,
65
+ gatewayId: input.gatewayId
66
+ }
67
+ );
68
+ const output = {
69
+ ...msg,
70
+ payload: config.outputFormat === "object" ? envelope : encoder.encodeServiceEnvelope(envelope),
71
+ _meshtastic: {
72
+ portNum: portNumName,
73
+ portNumValue: portNum,
74
+ from,
75
+ to,
76
+ channel,
77
+ hopLimit,
78
+ wantAck: input.wantAck ?? false,
79
+ encoded: true
80
+ }
81
+ };
82
+ send(output);
83
+ done();
84
+ } catch (error) {
85
+ const errorMsg = error instanceof Error ? error.message : String(error);
86
+ node.error(`Encode failed: ${errorMsg}`, msg);
87
+ done(error instanceof Error ? error : new Error(errorMsg));
88
+ }
89
+ });
90
+ }
91
+ RED.nodes.registerType("meshtastic-encode", EncodeNodeConstructor);
92
+ };
93
+ module.exports = nodeInit;
94
+ //# sourceMappingURL=encode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encode.js","sources":["../../../src/nodes/encode/encode.ts"],"sourcesContent":["/**\n * Meshtastic Encode Node\n * Encodes JSON messages to Meshtastic protobuf payloads\n */\n\nimport type { Node, NodeDef, NodeInitializer } from 'node-red';\nimport { Protobuf } from '@meshtastic/protobufs';\nimport {\n encodeMessageByPortNum,\n parsePortNum,\n createDataPayload,\n createServiceEnvelope,\n encodeServiceEnvelope,\n encodeData,\n getPortNumName,\n BROADCAST_ADDR,\n MeshtasticEncodeError,\n validateNodeNumber,\n validateChannelIndex,\n validateHopLimit,\n} from '../../lib';\n\ninterface EncodeConfig extends NodeDef {\n name: string;\n defaultTo: number;\n defaultChannel: number;\n defaultHopLimit: number;\n outputFormat: 'buffer' | 'object';\n outputType: 'data' | 'envelope';\n}\n\nconst nodeInit: NodeInitializer = (RED): void => {\n function EncodeNodeConstructor(this: Node, config: EncodeConfig): void {\n RED.nodes.createNode(this, config);\n const node = this;\n\n node.on('input', (msg: any, send, done) => {\n try {\n // Validate input\n if (!msg.payload || typeof msg.payload !== 'object') {\n throw new Error('Input payload must be an object');\n }\n\n const input = msg.payload;\n\n // Parse port number (required)\n if (!input.portNum && input.portNum !== 0) {\n throw new MeshtasticEncodeError('portNum is required in payload');\n }\n\n const portNum = parsePortNum(input.portNum);\n const portNumName = getPortNumName(portNum);\n\n // Get message data (required)\n if (input.message === undefined && input.message !== null) {\n throw new MeshtasticEncodeError('message is required in payload');\n }\n\n // Encode the message\n const encodedPayload = encodeMessageByPortNum(portNum, input.message);\n\n // Create Data protobuf\n const data = createDataPayload(portNum, encodedPayload, {\n wantResponse: input.wantResponse,\n requestId: input.requestId,\n emoji: input.emoji,\n });\n\n // Check if we should output just Data or full ServiceEnvelope\n const outputType = config.outputType || 'envelope';\n\n if (outputType === 'data') {\n // Output just the Data protobuf\n const output = {\n ...msg,\n payload: config.outputFormat === 'object' ? data : encodeData(data),\n _meshtastic: {\n portNum: portNumName,\n portNumValue: portNum,\n encoded: true,\n },\n };\n\n send(output);\n done();\n return;\n }\n\n // Build full ServiceEnvelope\n // Get addressing info (from, to, channel)\n const to = input.to ?? config.defaultTo ?? BROADCAST_ADDR;\n const channel = input.channel ?? config.defaultChannel ?? 0;\n const hopLimit = input.hopLimit ?? config.defaultHopLimit ?? 3;\n\n // Validate values\n validateNodeNumber(to, 'to');\n validateChannelIndex(channel, 'channel');\n validateHopLimit(hopLimit, 'hopLimit');\n\n // from and packetId will be set during encryption or by the sender\n const from = input.from ?? 0; // 0 = will be set later\n const packetId = input.packetId; // undefined = will be generated\n\n // Create ServiceEnvelope\n const envelope = createServiceEnvelope(\n data,\n from,\n to,\n channel,\n packetId,\n {\n wantAck: input.wantAck,\n hopLimit,\n priority: input.priority,\n channelId: input.channelId,\n gatewayId: input.gatewayId,\n }\n );\n\n // Prepare output\n const output = {\n ...msg,\n payload: config.outputFormat === 'object' ? envelope : encodeServiceEnvelope(envelope),\n _meshtastic: {\n portNum: portNumName,\n portNumValue: portNum,\n from,\n to,\n channel,\n hopLimit,\n wantAck: input.wantAck ?? false,\n encoded: true,\n },\n };\n\n send(output);\n done();\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n node.error(`Encode failed: ${errorMsg}`, msg);\n done(error instanceof Error ? error : new Error(errorMsg));\n }\n });\n }\n\n RED.nodes.registerType('meshtastic-encode', EncodeNodeConstructor);\n};\n\nexport default nodeInit;\n"],"names":["MeshtasticEncodeError","parsePortNum","getPortNumName","encodeMessageByPortNum","createDataPayload","output","encodeData","BROADCAST_ADDR","validateNodeNumber","validateChannelIndex","validateHopLimit","createServiceEnvelope","encodeServiceEnvelope"],"mappings":";;;;;;AA+BA,MAAM,WAA4B,CAAC,QAAc;AAC/C,WAAS,sBAAkC,QAA4B;AACrE,QAAI,MAAM,WAAW,MAAM,MAAM;AACjC,UAAM,OAAO;AAEb,SAAK,GAAG,SAAS,CAAC,KAAU,MAAM,SAAS;AACzC,UAAI;AAEF,YAAI,CAAC,IAAI,WAAW,OAAO,IAAI,YAAY,UAAU;AACnD,gBAAM,IAAI,MAAM,iCAAiC;AAAA,QACnD;AAEA,cAAM,QAAQ,IAAI;AAGlB,YAAI,CAAC,MAAM,WAAW,MAAM,YAAY,GAAG;AACzC,gBAAM,IAAIA,QAAAA,sBAAsB,gCAAgC;AAAA,QAClE;AAEA,cAAM,UAAUC,QAAAA,aAAa,MAAM,OAAO;AAC1C,cAAM,cAAcC,QAAAA,eAAe,OAAO;AAG1C,YAAI,MAAM,YAAY,UAAa,MAAM,YAAY,MAAM;AACzD,gBAAM,IAAIF,QAAAA,sBAAsB,gCAAgC;AAAA,QAClE;AAGA,cAAM,iBAAiBG,QAAAA,uBAAuB,SAAS,MAAM,OAAO;AAGpE,cAAM,OAAOC,QAAAA,kBAAkB,SAAS,gBAAgB;AAAA,UACtD,cAAc,MAAM;AAAA,UACpB,WAAW,MAAM;AAAA,UACjB,OAAO,MAAM;AAAA,QAAA,CACd;AAGD,cAAM,aAAa,OAAO,cAAc;AAExC,YAAI,eAAe,QAAQ;AAEzB,gBAAMC,UAAS;AAAA,YACb,GAAG;AAAA,YACH,SAAS,OAAO,iBAAiB,WAAW,OAAOC,QAAAA,WAAW,IAAI;AAAA,YAClE,aAAa;AAAA,cACX,SAAS;AAAA,cACT,cAAc;AAAA,cACd,SAAS;AAAA,YAAA;AAAA,UACX;AAGF,eAAKD,OAAM;AACX,eAAA;AACA;AAAA,QACF;AAIA,cAAM,KAAK,MAAM,MAAM,OAAO,aAAaE,aAAAA;AAC3C,cAAM,UAAU,MAAM,WAAW,OAAO,kBAAkB;AAC1D,cAAM,WAAW,MAAM,YAAY,OAAO,mBAAmB;AAG7DC,mBAAAA,mBAAmB,IAAI,IAAI;AAC3BC,mBAAAA,qBAAqB,SAAS,SAAS;AACvCC,mBAAAA,iBAAiB,UAAU,UAAU;AAGrC,cAAM,OAAO,MAAM,QAAQ;AAC3B,cAAM,WAAW,MAAM;AAGvB,cAAM,WAAWC,QAAAA;AAAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,YACE,SAAS,MAAM;AAAA,YACf;AAAA,YACA,UAAU,MAAM;AAAA,YAChB,WAAW,MAAM;AAAA,YACjB,WAAW,MAAM;AAAA,UAAA;AAAA,QACnB;AAIF,cAAM,SAAS;AAAA,UACb,GAAG;AAAA,UACH,SAAS,OAAO,iBAAiB,WAAW,WAAWC,QAAAA,sBAAsB,QAAQ;AAAA,UACrF,aAAa;AAAA,YACX,SAAS;AAAA,YACT,cAAc;AAAA,YACd;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,SAAS,MAAM,WAAW;AAAA,YAC1B,SAAS;AAAA,UAAA;AAAA,QACX;AAGF,aAAK,MAAM;AACX,aAAA;AAAA,MACF,SAAS,OAAO;AACd,cAAM,WAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACtE,aAAK,MAAM,kBAAkB,QAAQ,IAAI,GAAG;AAC5C,aAAK,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,QAAQ,CAAC;AAAA,MAC3D;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAI,MAAM,aAAa,qBAAqB,qBAAqB;AACnE;;"}
@@ -0,0 +1,164 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('meshtastic-encrypt', {
3
+ category: 'meshtastic',
4
+ color: '#3FADD7',
5
+ defaults: {
6
+ name: { value: '' },
7
+ mode: { value: 'channel' },
8
+ keyFormat: { value: 'base64' },
9
+ fromNode: { value: 0 },
10
+ outputFormat: { value: 'buffer' }
11
+ },
12
+ credentials: {
13
+ channelKey: { type: 'password' },
14
+ myPrivateKey: { type: 'password' },
15
+ myPublicKey: { type: 'password' },
16
+ recipientPublicKey: { type: 'password' }
17
+ },
18
+ inputs: 1,
19
+ outputs: 1,
20
+ icon: 'lock.svg',
21
+ label: function() {
22
+ return this.name || 'meshtastic-encrypt';
23
+ },
24
+ paletteLabel: 'encrypt'
25
+ });
26
+ </script>
27
+
28
+ <script type="text/html" data-template-name="meshtastic-encrypt">
29
+ <div class="form-row">
30
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
31
+ <input type="text" id="node-input-name" placeholder="Name">
32
+ </div>
33
+
34
+ <div class="form-row">
35
+ <label for="node-input-mode"><i class="fa fa-cog"></i> Mode</label>
36
+ <select id="node-input-mode">
37
+ <option value="channel">Channel (PSK)</option>
38
+ <option value="dm">Direct Message (PKC)</option>
39
+ </select>
40
+ </div>
41
+
42
+ <div class="form-row">
43
+ <label for="node-input-keyFormat"><i class="fa fa-file-code-o"></i> Key Format</label>
44
+ <select id="node-input-keyFormat">
45
+ <option value="base64">Base64</option>
46
+ <option value="hex">Hex</option>
47
+ </select>
48
+ </div>
49
+
50
+ <div class="form-row">
51
+ <label for="node-input-fromNode"><i class="fa fa-user"></i> From Node</label>
52
+ <input type="number" id="node-input-fromNode" placeholder="Source node number">
53
+ <div class="form-tips">
54
+ Source node number (can be overridden via msg._meshtastic.from)
55
+ </div>
56
+ </div>
57
+
58
+ <div class="form-row">
59
+ <label for="node-input-channelKey"><i class="fa fa-key"></i> Channel Key (PSK)</label>
60
+ <input type="password" id="node-input-channelKey" placeholder="Base64 or hex channel key">
61
+ </div>
62
+
63
+ <div class="form-row">
64
+ <label for="node-input-myPrivateKey"><i class="fa fa-lock"></i> My Private Key</label>
65
+ <input type="password" id="node-input-myPrivateKey" placeholder="Your x25519 private key">
66
+ </div>
67
+
68
+ <div class="form-row">
69
+ <label for="node-input-myPublicKey"><i class="fa fa-unlock"></i> My Public Key</label>
70
+ <input type="password" id="node-input-myPublicKey" placeholder="Your x25519 public key">
71
+ </div>
72
+
73
+ <div class="form-row">
74
+ <label for="node-input-recipientPublicKey"><i class="fa fa-user-circle"></i> Recipient Public Key</label>
75
+ <input type="password" id="node-input-recipientPublicKey" placeholder="Recipient's x25519 public key">
76
+ </div>
77
+
78
+ <div class="form-row">
79
+ <label for="node-input-outputFormat"><i class="fa fa-file-o"></i> Output Format</label>
80
+ <select id="node-input-outputFormat">
81
+ <option value="buffer">Buffer (binary)</option>
82
+ <option value="object">Object (ServiceEnvelope)</option>
83
+ </select>
84
+ </div>
85
+ </script>
86
+
87
+ <script type="text/html" data-help-name="meshtastic-encrypt">
88
+ <p>Encrypts Meshtastic payloads using either channel PSK or direct message PKC.</p>
89
+
90
+ <h3>Inputs</h3>
91
+ <dl class="message-properties">
92
+ <dt>payload <span class="property-type">Buffer | Object</span></dt>
93
+ <dd>Data protobuf or ServiceEnvelope to encrypt</dd>
94
+ <dt class="optional">_meshtastic.from <span class="property-type">number</span></dt>
95
+ <dd>Source node number (overrides config)</dd>
96
+ <dt class="optional">_meshtastic.to <span class="property-type">number</span></dt>
97
+ <dd>Destination node number</dd>
98
+ <dt class="optional">_meshtastic.channel <span class="property-type">number</span></dt>
99
+ <dd>Channel index</dd>
100
+ <dt class="optional">_meshtastic.packetId <span class="property-type">number</span></dt>
101
+ <dd>Packet ID (random if not provided)</dd>
102
+ </dl>
103
+
104
+ <h3>Outputs</h3>
105
+ <dl class="message-properties">
106
+ <dt>payload <span class="property-type">Buffer | Object</span></dt>
107
+ <dd>Encrypted ServiceEnvelope</dd>
108
+ <dt>_meshtastic <span class="property-type">object</span></dt>
109
+ <dd>Metadata about encryption</dd>
110
+ </dl>
111
+
112
+ <h3>Details</h3>
113
+ <p>This node encrypts Meshtastic packets using:</p>
114
+ <ul>
115
+ <li><b>Channel encryption (PSK)</b>: AES-128/256-CTR with pre-shared key</li>
116
+ <li><b>Direct message encryption (PKC)</b>: x25519 + AES-256-CCM</li>
117
+ </ul>
118
+
119
+ <h4>Mode</h4>
120
+ <p><b>Channel (PSK)</b>: Encrypt for a channel using a pre-shared key. All nodes with the same key can decrypt.</p>
121
+ <p><b>Direct Message (PKC)</b>: Encrypt for a specific recipient using public key cryptography. Only the recipient can decrypt.</p>
122
+
123
+ <h4>Channel Encryption (PSK)</h4>
124
+ <p>Required:</p>
125
+ <ul>
126
+ <li><b>Channel Key</b>: 16 or 32 bytes in base64/hex</li>
127
+ <li><b>From Node</b>: Source node number (for nonce construction)</li>
128
+ </ul>
129
+
130
+ <h4>Direct Message Encryption (PKC)</h4>
131
+ <p>Required:</p>
132
+ <ul>
133
+ <li><b>My Private Key</b>: Your x25519 private key (32 bytes)</li>
134
+ <li><b>Recipient Public Key</b>: Recipient's x25519 public key (32 bytes)</li>
135
+ <li><b>From Node</b>: Your node number</li>
136
+ </ul>
137
+ <p>Optional:</p>
138
+ <ul>
139
+ <li><b>My Public Key</b>: Your public key (for reference)</li>
140
+ </ul>
141
+
142
+ <h4>From Node Number</h4>
143
+ <p>The source node number is required for encryption (nonce construction). It can be set:</p>
144
+ <ol>
145
+ <li>In the node configuration (From Node field)</li>
146
+ <li>Via <code>msg._meshtastic.from</code> (overrides config)</li>
147
+ </ol>
148
+
149
+ <h4>Packet ID</h4>
150
+ <p>A packet ID is automatically generated if not provided. For channel encryption, it's part of the nonce.</p>
151
+
152
+ <h4>Security</h4>
153
+ <p>All keys are stored encrypted using Node-RED's credentials system.</p>
154
+
155
+ <h3>Example Flow</h3>
156
+ <pre>
157
+ [Inject] → [meshtastic-encode] → [meshtastic-encrypt] → [MQTT Out]
158
+ </pre>
159
+
160
+ <h3>References</h3>
161
+ <ul>
162
+ <li><a href="https://meshtastic.org/docs/overview/encryption/">Meshtastic Encryption</a></li>
163
+ </ul>
164
+ </script>
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ const protobufs = require("@meshtastic/protobufs");
3
+ const crypto = require("crypto");
4
+ const x25519 = require("../../x25519-D0dlDGB0.js");
5
+ const decoder = require("../../decoder-BvBAtm2U.js");
6
+ const encoder = require("../../encoder-DU3wTIEA.js");
7
+ const validation = require("../../validation-BegQyBSh.js");
8
+ const nodeInit = (RED) => {
9
+ function EncryptNodeConstructor(config) {
10
+ RED.nodes.createNode(this, config);
11
+ const node = this;
12
+ const credentials = RED.nodes.getCredentials(config.id);
13
+ let channelKey = null;
14
+ if (credentials == null ? void 0 : credentials.channelKey) {
15
+ try {
16
+ channelKey = x25519.parseKey(credentials.channelKey, config.keyFormat || "base64");
17
+ if (x25519.isDefaultKey(channelKey)) {
18
+ node.warn(
19
+ "Using default Meshtastic encryption key (AQ==). This is INSECURE and should only be used for testing."
20
+ );
21
+ }
22
+ } catch (error) {
23
+ node.error(`Invalid channel key: ${error instanceof Error ? error.message : String(error)}`);
24
+ channelKey = null;
25
+ }
26
+ }
27
+ let myPrivateKey = null;
28
+ let myPublicKey = null;
29
+ let recipientPublicKey = null;
30
+ if (credentials == null ? void 0 : credentials.myPrivateKey) {
31
+ try {
32
+ myPrivateKey = x25519.parsePrivateKey(credentials.myPrivateKey, config.keyFormat || "base64");
33
+ } catch (error) {
34
+ node.error(`Invalid private key: ${error instanceof Error ? error.message : String(error)}`);
35
+ }
36
+ }
37
+ if (credentials == null ? void 0 : credentials.myPublicKey) {
38
+ try {
39
+ myPublicKey = x25519.parsePublicKey(credentials.myPublicKey, config.keyFormat || "base64");
40
+ } catch (error) {
41
+ node.error(`Invalid public key: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+ if (credentials == null ? void 0 : credentials.recipientPublicKey) {
45
+ try {
46
+ recipientPublicKey = x25519.parsePublicKey(credentials.recipientPublicKey, config.keyFormat || "base64");
47
+ } catch (error) {
48
+ node.error(`Invalid recipient public key: ${error instanceof Error ? error.message : String(error)}`);
49
+ }
50
+ }
51
+ node.on("input", (msg, send, done) => {
52
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
53
+ try {
54
+ let data;
55
+ let envelope = null;
56
+ let from;
57
+ let to;
58
+ let channel;
59
+ let packetId;
60
+ if (Buffer.isBuffer(msg.payload) || msg.payload instanceof Uint8Array) {
61
+ try {
62
+ data = decoder.decodeData(decoder.ensureBuffer(msg.payload));
63
+ from = ((_a = msg._meshtastic) == null ? void 0 : _a.from) ?? config.fromNode ?? 0;
64
+ to = ((_b = msg._meshtastic) == null ? void 0 : _b.to) ?? 4294967295;
65
+ channel = ((_c = msg._meshtastic) == null ? void 0 : _c.channel) ?? 0;
66
+ packetId = ((_d = msg._meshtastic) == null ? void 0 : _d.packetId) ?? crypto.randomBytes(4).readUInt32LE(0);
67
+ } catch {
68
+ envelope = decoder.decodeServiceEnvelope(decoder.ensureBuffer(msg.payload));
69
+ if (!envelope.packet || envelope.packet.payloadVariant.case !== "decoded") {
70
+ throw new Error("ServiceEnvelope must have a decoded payload");
71
+ }
72
+ data = envelope.packet.payloadVariant.value;
73
+ from = envelope.packet.from;
74
+ to = envelope.packet.to;
75
+ channel = envelope.packet.channel;
76
+ packetId = envelope.packet.id || crypto.randomBytes(4).readUInt32LE(0);
77
+ }
78
+ } else if (typeof msg.payload === "object") {
79
+ if (msg.payload.packet) {
80
+ envelope = msg.payload;
81
+ if (!envelope.packet || envelope.packet.payloadVariant.case !== "decoded") {
82
+ throw new Error("ServiceEnvelope must have a decoded payload");
83
+ }
84
+ data = envelope.packet.payloadVariant.value;
85
+ from = envelope.packet.from;
86
+ to = envelope.packet.to;
87
+ channel = envelope.packet.channel;
88
+ packetId = envelope.packet.id || crypto.randomBytes(4).readUInt32LE(0);
89
+ } else {
90
+ data = msg.payload;
91
+ from = ((_e = msg._meshtastic) == null ? void 0 : _e.from) ?? config.fromNode ?? 0;
92
+ to = ((_f = msg._meshtastic) == null ? void 0 : _f.to) ?? 4294967295;
93
+ channel = ((_g = msg._meshtastic) == null ? void 0 : _g.channel) ?? 0;
94
+ packetId = ((_h = msg._meshtastic) == null ? void 0 : _h.packetId) ?? crypto.randomBytes(4).readUInt32LE(0);
95
+ }
96
+ } else {
97
+ throw new Error("Input must be a Buffer, Uint8Array, Data, or ServiceEnvelope");
98
+ }
99
+ if (!from || from === 0) {
100
+ throw new decoder.MeshtasticEncryptError("from node number is required (set in config or msg._meshtastic.from)");
101
+ }
102
+ validation.validateNodeNumber(from, "from");
103
+ validation.validatePacketId(packetId, "packetId");
104
+ const dataBytes = decoder.ensureBuffer(encoder.encodeData(data));
105
+ let encryptedBytes;
106
+ let isPkiEncrypted = false;
107
+ if (config.mode === "dm") {
108
+ if (!myPrivateKey || !recipientPublicKey) {
109
+ throw new decoder.MeshtasticEncryptError("DM encryption requires myPrivateKey and recipientPublicKey");
110
+ }
111
+ const sharedSecret = x25519.deriveSharedSecret(myPrivateKey, recipientPublicKey);
112
+ const nonce = crypto.randomBytes(12);
113
+ const { ciphertext, authTag } = x25519.encryptDirectMessage(dataBytes, sharedSecret, nonce);
114
+ encryptedBytes = Buffer.concat([nonce, ciphertext, authTag]);
115
+ isPkiEncrypted = true;
116
+ } else {
117
+ if (!channelKey) {
118
+ throw new decoder.MeshtasticEncryptError("Channel encryption requires a channelKey");
119
+ }
120
+ encryptedBytes = x25519.encryptChannelMessage(dataBytes, channelKey, from, packetId);
121
+ }
122
+ let outputEnvelope;
123
+ if (envelope) {
124
+ outputEnvelope = {
125
+ ...envelope,
126
+ packet: {
127
+ ...envelope.packet,
128
+ from,
129
+ to,
130
+ channel,
131
+ id: packetId,
132
+ payloadVariant: {
133
+ case: "encrypted",
134
+ value: encryptedBytes
135
+ }
136
+ }
137
+ };
138
+ if (isPkiEncrypted) {
139
+ outputEnvelope.packet.pkiEncrypted = true;
140
+ }
141
+ } else {
142
+ outputEnvelope = {
143
+ packet: {
144
+ from,
145
+ to,
146
+ channel,
147
+ id: packetId,
148
+ rxTime: 0,
149
+ rxSnr: 0,
150
+ rxRssi: 0,
151
+ hopLimit: ((_i = msg._meshtastic) == null ? void 0 : _i.hopLimit) ?? 3,
152
+ wantAck: ((_j = msg._meshtastic) == null ? void 0 : _j.wantAck) ?? false,
153
+ priority: protobufs.Protobuf.Mesh.MeshPacket_Priority.UNSET,
154
+ hopStart: ((_k = msg._meshtastic) == null ? void 0 : _k.hopLimit) ?? 3,
155
+ payloadVariant: {
156
+ case: "encrypted",
157
+ value: encryptedBytes
158
+ }
159
+ },
160
+ channelId: "",
161
+ gatewayId: ""
162
+ };
163
+ if (isPkiEncrypted) {
164
+ outputEnvelope.packet.pkiEncrypted = true;
165
+ }
166
+ }
167
+ const output = {
168
+ ...msg,
169
+ payload: config.outputFormat === "object" ? outputEnvelope : encoder.encodeServiceEnvelope(outputEnvelope),
170
+ _meshtastic: {
171
+ encrypted: true,
172
+ encryptedWith: config.mode,
173
+ isPkiEncrypted,
174
+ from,
175
+ to,
176
+ channel,
177
+ packetId
178
+ }
179
+ };
180
+ send(output);
181
+ done();
182
+ } catch (error) {
183
+ const errorMsg = error instanceof Error ? error.message : String(error);
184
+ node.error(`Encryption failed: ${errorMsg}`, msg);
185
+ done(error instanceof Error ? error : new Error(errorMsg));
186
+ }
187
+ });
188
+ }
189
+ RED.nodes.registerType("meshtastic-encrypt", EncryptNodeConstructor, {
190
+ credentials: {
191
+ channelKey: { type: "password" },
192
+ myPrivateKey: { type: "password" },
193
+ myPublicKey: { type: "password" },
194
+ recipientPublicKey: { type: "password" }
195
+ }
196
+ });
197
+ };
198
+ module.exports = nodeInit;
199
+ //# sourceMappingURL=encrypt.js.map