Skip to content

Commit f8e02d7

Browse files
committed
encryption debugging
1 parent 9b29fdd commit f8e02d7

7 files changed

Lines changed: 294 additions & 32 deletions

File tree

backend/src/services/encryption.service.ts

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,34 +66,47 @@ export class EncryptionService {
6666
// Decode base64 key
6767
let keyBuffer = Buffer.from(channel.key, 'base64');
6868

69+
logger.info(`Processing channel ${index} (${channel.name}): raw key length = ${keyBuffer.length}, first byte = 0x${keyBuffer[0]?.toString(16).padStart(2, '0')}`);
70+
6971
// Meshtastic uses special 1-byte PSK shortcuts:
7072
// 0x00 = no encryption
7173
// 0x01 = default key (fixed 16-byte AES-128 key)
7274
// 0x02-0x0A = default key with last byte incremented (simple2-simple10)
7375
if (keyBuffer.length === 1) {
7476
const pskByte = keyBuffer[0];
7577

78+
logger.info(`Detected 1-byte PSK: 0x${pskByte.toString(16).padStart(2, '0')}`);
79+
7680
if (pskByte === 0x00) {
7781
// No encryption - skip this channel
7882
logger.info(`Channel ${index} (${channel.name}) has no encryption (PSK 0x00), skipping`);
7983
return;
8084
} else if (pskByte >= 0x01 && pskByte <= 0x0A) {
8185
// Meshtastic default key: d4 f1 bb 3a 20 29 07 59 f0 bc ff ab cf 4e 69 01
8286
// For PSK 0x02-0x0A, add (pskByte - 1) to the last byte
83-
const defaultKey = Buffer.from([
87+
// NOTE: Meshtastic uses AES-256-CTR, so we need to expand to 32 bytes
88+
// The 16-byte default key is repeated/padded to 32 bytes
89+
const defaultKey16 = Buffer.from([
8490
0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59,
8591
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01
8692
]);
8793

94+
// Expand to 32 bytes by repeating the key
95+
const defaultKey32 = Buffer.alloc(32);
96+
defaultKey16.copy(defaultKey32, 0);
97+
defaultKey16.copy(defaultKey32, 16);
98+
8899
if (pskByte > 0x01) {
89100
// For simple2-simple10, increment the last byte
90-
defaultKey[15] = defaultKey[15] + (pskByte - 0x01);
91-
logger.info(`Mapped 1-byte PSK 0x${pskByte.toString(16).padStart(2, '0')} to Meshtastic simple${pskByte} key for channel ${index}: ${channel.name}`);
101+
defaultKey32[15] = defaultKey32[15] + (pskByte - 0x01);
102+
defaultKey32[31] = defaultKey32[31] + (pskByte - 0x01);
103+
logger.info(`Mapped 1-byte PSK 0x${pskByte.toString(16).padStart(2, '0')} to Meshtastic simple${pskByte} key (32 bytes) for channel ${index}: ${channel.name}`);
92104
} else {
93-
logger.info(`Mapped 1-byte PSK 0x01 to Meshtastic default key for channel ${index}: ${channel.name}`);
105+
logger.info(`Mapped 1-byte PSK 0x01 to Meshtastic default key (32 bytes) for channel ${index}: ${channel.name}`);
94106
}
95107

96-
keyBuffer = defaultKey;
108+
keyBuffer = defaultKey32;
109+
logger.info(`After expansion: keyBuffer length = ${keyBuffer.length}`);
97110
} else {
98111
// Unknown 1-byte PSK, pad with zeros
99112
const paddedKey = Buffer.alloc(16, 0);
@@ -102,11 +115,28 @@ export class EncryptionService {
102115
logger.warn(`Unknown 1-byte PSK 0x${pskByte.toString(16).padStart(2, '0')} for channel ${index}: ${channel.name}, padding with zeros`);
103116
}
104117
} else if (keyBuffer.length < 16) {
105-
// For keys shorter than 16 bytes (but not 1 byte), pad with zeros
106-
const paddedKey = Buffer.alloc(16, 0);
118+
// For keys shorter than 16 bytes (but not 1 byte), expand to 32 bytes for AES-256
119+
const expandedKey = Buffer.alloc(32, 0);
120+
keyBuffer.copy(expandedKey);
121+
// Repeat the key pattern
122+
for (let i = keyBuffer.length; i < 32; i++) {
123+
expandedKey[i] = keyBuffer[i % keyBuffer.length];
124+
}
125+
keyBuffer = expandedKey;
126+
logger.info(`Expanded encryption key for channel ${index}: ${channel.name} (${Buffer.from(channel.key, 'base64').length} -> 32 bytes)`);
127+
} else if (keyBuffer.length === 16) {
128+
// Expand 16-byte key to 32 bytes by repeating
129+
const expandedKey = Buffer.alloc(32);
130+
keyBuffer.copy(expandedKey, 0);
131+
keyBuffer.copy(expandedKey, 16);
132+
keyBuffer = expandedKey;
133+
logger.info(`Expanded 16-byte key to 32 bytes for channel ${index}: ${channel.name}`);
134+
} else if (keyBuffer.length > 16 && keyBuffer.length < 32) {
135+
// Pad to 32 bytes
136+
const paddedKey = Buffer.alloc(32, 0);
107137
keyBuffer.copy(paddedKey);
108138
keyBuffer = paddedKey;
109-
logger.info(`Padded encryption key for channel ${index}: ${channel.name} (${Buffer.from(channel.key, 'base64').length} -> 16 bytes)`);
139+
logger.info(`Padded encryption key for channel ${index}: ${channel.name} (${Buffer.from(channel.key, 'base64').length} -> 32 bytes)`);
110140
}
111141

112142
logger.info(`Loaded encryption key for channel ${index}: ${channel.name} (${keyBuffer.length} bytes, base64: ${channel.key})`);
@@ -165,34 +195,55 @@ export class EncryptionService {
165195
return null;
166196
}
167197

198+
// Log the full encrypted payload for debugging
199+
logger.info(`=== DECRYPTION DEBUG ===`);
200+
logger.info(`Encrypted payload length: ${encryptedPayload.length} bytes`);
201+
logger.info(`Full encrypted payload (hex): ${encryptedPayload.toString('hex')}`);
202+
logger.info(`Packet ID: ${packetId}`);
203+
logger.info(`Channel index: ${channelIndex}`);
204+
logger.info(`Key (hex): ${key.toString('hex')}`);
205+
168206
// Extract nonce (first 8 bytes) and pad to 16 bytes for CTR mode
169207
const nonce = Buffer.alloc(16, 0);
170208
encryptedPayload.copy(nonce, 0, 0, 8);
171209

172210
// Extract ciphertext (everything after the 8-byte nonce)
173211
const ciphertext = encryptedPayload.slice(8);
174212

175-
logger.debug(`Decrypting with nonce: ${nonce.toString('hex')}, channel: ${channelIndex}, ciphertext length: ${ciphertext.length}`);
213+
logger.info(`Nonce (8 bytes): ${encryptedPayload.slice(0, 8).toString('hex')}`);
214+
logger.info(`Nonce padded (16 bytes): ${nonce.toString('hex')}`);
215+
logger.info(`Ciphertext length: ${ciphertext.length} bytes`);
216+
logger.info(`Ciphertext (first 32 bytes): ${ciphertext.slice(0, 32).toString('hex')}`);
176217

177218
// Determine the algorithm based on key length
178-
// Meshtastic uses AES-128-CTR (16-byte key)
219+
// Meshtastic uses AES-256-CTR (32-byte key)
179220
let algorithm: string;
180-
if (key.length === 16) {
181-
algorithm = 'aes-128-ctr';
182-
} else if (key.length === 32) {
221+
if (key.length === 32) {
222+
algorithm = 'aes-256-ctr';
223+
} else if (key.length === 16) {
224+
// Expand 16-byte key to 32 bytes for AES-256
225+
logger.info(`Expanding 16-byte key to 32 bytes for AES-256-CTR`);
226+
const expandedKey = Buffer.alloc(32);
227+
key.copy(expandedKey, 0);
228+
key.copy(expandedKey, 16);
229+
key = expandedKey;
183230
algorithm = 'aes-256-ctr';
184231
} else {
185232
logger.warn(`Unexpected key length: ${key.length} bytes, expected 16 or 32`);
186-
// Try to use the key as-is with AES-128
187-
algorithm = 'aes-128-ctr';
188-
if (key.length < 16) {
189-
// Pad key to 16 bytes if too short
190-
const paddedKey = Buffer.alloc(16, 0);
233+
// Try to expand/pad to 32 bytes for AES-256
234+
algorithm = 'aes-256-ctr';
235+
if (key.length < 32) {
236+
// Pad key to 32 bytes
237+
const paddedKey = Buffer.alloc(32, 0);
191238
key.copy(paddedKey);
239+
// If key is 16 bytes or less, repeat it
240+
if (key.length <= 16) {
241+
key.copy(paddedKey, 16, 0, Math.min(key.length, 16));
242+
}
192243
key = paddedKey;
193-
} else if (key.length > 16 && key.length < 32) {
194-
// Truncate to 16 bytes
195-
key = key.slice(0, 16);
244+
} else {
245+
// Truncate to 32 bytes
246+
key = key.slice(0, 32);
196247
}
197248
}
198249

@@ -205,7 +256,11 @@ export class EncryptionService {
205256
decipher.final()
206257
]);
207258

208-
logger.debug(`Successfully decrypted payload (${ciphertext.length} -> ${decrypted.length} bytes)`);
259+
logger.info(`Decrypted length: ${decrypted.length} bytes`);
260+
logger.info(`Decrypted (first 64 bytes): ${decrypted.slice(0, Math.min(64, decrypted.length)).toString('hex')}`);
261+
logger.info(`Decrypted as ASCII: ${decrypted.slice(0, Math.min(64, decrypted.length)).toString('ascii').replace(/[^\x20-\x7E]/g, '.')}`);
262+
logger.info(`=== END DECRYPTION DEBUG ===`);
263+
209264
return decrypted;
210265
} catch (error) {
211266
logger.error('Failed to decrypt message:', error);

backend/src/services/mqtt-monitor.service.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export interface MessageStatistics {
5151
messagesByChannel: Record<number, number>;
5252
encryptedMessages: number;
5353
unencryptedMessages: number;
54+
decryptionFailures: number;
55+
decryptionFailurePercentage: number;
5456
averageMessageSize: number;
5557
messagesPerMinute: number;
5658
topNodes: Array<{ nodeId: string; shortName?: string; longName?: string; count: number }>;
@@ -207,6 +209,7 @@ export class MQTTMonitorService extends EventEmitter {
207209
const messagesByChannel: Record<number, number> = {};
208210
const nodeMessageCounts: Record<string, number> = {};
209211
let encryptedCount = 0;
212+
let decryptionFailureCount = 0;
210213
let totalSize = 0;
211214

212215
// Initialize message type counts
@@ -231,6 +234,10 @@ export class MQTTMonitorService extends EventEmitter {
231234
if (msg.parsed.encrypted) {
232235
encryptedCount++;
233236
}
237+
238+
if (msg.parsed.decryptionFailed) {
239+
decryptionFailureCount++;
240+
}
234241
}
235242

236243
totalSize += msg.size;
@@ -242,18 +249,28 @@ export class MQTTMonitorService extends EventEmitter {
242249
.slice(0, 10)
243250
.map(([nodeId]) => nodeId);
244251

252+
logger.debug(`Top node IDs from MQTT monitor: ${topNodeIds.join(', ')}`);
253+
logger.debug(`Node message counts: ${JSON.stringify(nodeMessageCounts)}`);
254+
245255
// Fetch node names from database
246256
const topNodes = await this.fetchNodeNames(topNodeIds, nodeMessageCounts);
247257

258+
logger.debug(`Top nodes after database lookup: ${JSON.stringify(topNodes)}`);
259+
248260
const timeRangeMinutes = this.getTimeRangeMinutes(timeRange);
249261
const messagesPerMinute = recentMessages.length / timeRangeMinutes;
262+
const decryptionFailurePercentage = encryptedCount > 0
263+
? (decryptionFailureCount / encryptedCount) * 100
264+
: 0;
250265

251266
return {
252267
totalMessages: recentMessages.length,
253268
messagesByType,
254269
messagesByChannel,
255270
encryptedMessages: encryptedCount,
256271
unencryptedMessages: recentMessages.length - encryptedCount,
272+
decryptionFailures: decryptionFailureCount,
273+
decryptionFailurePercentage,
257274
averageMessageSize: recentMessages.length > 0 ? totalSize / recentMessages.length : 0,
258275
messagesPerMinute,
259276
topNodes,
@@ -268,11 +285,19 @@ export class MQTTMonitorService extends EventEmitter {
268285
nodeIds: string[],
269286
counts: Record<string, number>
270287
): Promise<Array<{ nodeId: string; shortName?: string; longName?: string; count: number }>> {
288+
logger.info(`[MQTT Monitor] Fetching node names for ${nodeIds.length} IDs: ${nodeIds.join(', ')}`);
289+
271290
try {
272-
const { PrismaClient } = await import('@prisma/client');
273-
const prisma = new PrismaClient();
291+
// Use the shared Prisma client from database connection
292+
const { getDatabase } = await import('../database/connection');
293+
const db = getDatabase();
274294

275-
const nodes = await prisma.node.findMany({
295+
// First, let's check if there are ANY nodes in the database
296+
const totalNodes = await db.node.count();
297+
logger.info(`[MQTT Monitor] Total nodes in database: ${totalNodes}`);
298+
299+
// Try to find nodes with these specific IDs
300+
const nodes = await db.node.findMany({
276301
where: {
277302
nodeId: {
278303
in: nodeIds
@@ -285,12 +310,32 @@ export class MQTTMonitorService extends EventEmitter {
285310
}
286311
});
287312

288-
await prisma.$disconnect();
313+
logger.info(`[MQTT Monitor] Found ${nodes.length} nodes matching IDs`);
314+
315+
if (nodes.length > 0) {
316+
logger.info(`[MQTT Monitor] Sample matched node: ${JSON.stringify(nodes[0])}`);
317+
}
318+
319+
// Also try to get a few nodes with shortNames to see what's in the database
320+
const nodesWithShortNames = await db.node.findMany({
321+
where: {
322+
shortName: {
323+
not: null
324+
}
325+
},
326+
select: {
327+
nodeId: true,
328+
shortName: true
329+
},
330+
take: 5
331+
});
332+
333+
logger.info(`[MQTT Monitor] Sample nodes with shortNames: ${JSON.stringify(nodesWithShortNames)}`);
289334

290335
// Map nodes with their counts, filtering out nodes without shortName
291336
const result = nodeIds
292337
.map(nodeId => {
293-
const node = nodes.find(n => n.nodeId === nodeId);
338+
const node = nodes.find((n: any) => n.nodeId === nodeId);
294339
return {
295340
nodeId,
296341
shortName: node?.shortName || undefined,
@@ -300,9 +345,10 @@ export class MQTTMonitorService extends EventEmitter {
300345
})
301346
.filter(node => node.shortName); // Only include nodes with a shortName
302347

348+
logger.info(`[MQTT Monitor] Filtered to ${result.length} nodes with shortName`);
303349
return result;
304350
} catch (error) {
305-
logger.error('Failed to fetch node names:', error);
351+
logger.error('[MQTT Monitor] Failed to fetch node names:', error);
306352
// Return empty array if database query fails
307353
return [];
308354
}

backend/src/services/mqtt.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,16 @@ export class MQTTService extends EventEmitter {
222222
}
223223
} else {
224224
// Decryption or parsing failed - emit a failure indicator
225-
// Extract channel name from topic for better error reporting
225+
// Extract channel name and node ID from topic for better error reporting
226226
const topicParts = topic.split('/');
227227
const eIndex = topicParts.indexOf('e');
228228
const channelName = eIndex !== -1 && eIndex + 1 < topicParts.length ? topicParts[eIndex + 1] : 'unknown';
229+
// Node ID is typically the last part of the topic (e.g., !9e75f7d4)
230+
const nodeId = topicParts[topicParts.length - 1] || 'unknown';
229231

230232
const monitorPayload = JSON.stringify({
231-
from: 'unknown',
233+
from: nodeId,
234+
sender: nodeId,
232235
type: 'ENCRYPTED',
233236
encrypted: true,
234237
decryptionFailed: true,

backend/src/services/protobuf-decoder.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ export class ProtobufDecoderService {
272272
throw new Error('Protobuf root not initialized');
273273
}
274274

275+
logger.debug(`Attempting to decode decrypted payload as Data message (${decrypted.length} bytes)`);
276+
logger.debug(`First 32 bytes of decrypted data: ${decrypted.slice(0, 32).toString('hex')}`);
277+
275278
const Data = this.root.lookupType('Data');
276279
const dataMessage = Data.decode(decrypted);
277280
const decoded = Data.toObject(dataMessage, {
@@ -287,7 +290,8 @@ export class ProtobufDecoderService {
287290

288291
logger.debug(`Successfully decrypted and decoded packet from channel "${channelName}"`);
289292
} catch (error) {
290-
logger.warn(`Failed to decode decrypted payload from channel "${channelName}" - wrong encryption key, skipping packet`);
293+
logger.warn(`Failed to decode decrypted payload from channel "${channelName}" - wrong encryption key or invalid protobuf`);
294+
logger.debug(`Decode error details: ${error}`);
291295
// If decryption succeeded but protobuf parsing failed, the key is wrong
292296
// Don't process this packet
293297
return null;

config/app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ seo:
8484
encryption:
8585
channels:
8686
- name: "LongFast"
87-
key: "AQ==" # PSK 0x01 = Meshtastic default key (d4 f1 bb 3a 20 29 07 59 f0 bc ff ab cf 4e 69 01)
87+
key: "d4f1bb3a20290759f0bcffabcf4e6901" # PSK 0x01 = Meshtastic default key (d4 f1 bb 3a 20 29 07 59 f0 bc ff ab cf 4e 69 01). d4f1bb3a20290759f0bcffabcf4e6901
8888
default: true
8989
- name: "Primary"
9090
key: "1PG7OiApB3XvvX7g8kYzDYQD+CW+3Oi+Qs/LoIWh/gg=" # Custom 32-byte key (AES-256)

0 commit comments

Comments
 (0)