mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-03-28 17:42:58 +01:00
refactor, fix exception
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { useConfig } from "./ConfigContext";
|
||||
import { decryptMeshcoreGroupMessage } from "../lib/meshcore_decrypt";
|
||||
import { decryptMeshcoreGroupMessage } from "../lib/meshcore";
|
||||
import { getChannelIdFromKey } from "../lib/meshcore";
|
||||
import ChatMessageItem, { ChatMessage } from "./ChatMessageItem";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useConfig } from "./ConfigContext";
|
||||
import { decryptMeshcoreGroupMessage } from "../lib/meshcore_decrypt";
|
||||
import { decryptMeshcoreGroupMessage } from "../lib/meshcore";
|
||||
|
||||
export interface ChatMessage {
|
||||
ingest_timestamp: string;
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
import { createHash } from "crypto";
|
||||
import aesjs from "aes-js";
|
||||
|
||||
// Module-level cache for channel IDs
|
||||
const channelIdCache: Record<string, string> = {};
|
||||
|
||||
// Helper: Convert hex string to Uint8Array
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
if (hex.startsWith("0x")) hex = hex.slice(2);
|
||||
if (hex.length % 2 !== 0) hex = "0" + hex;
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Helper: Convert base64 to Uint8Array
|
||||
function base64ToBytes(b64: string): Uint8Array {
|
||||
return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
// Add a helper to decode base64 or hex
|
||||
function decodeKeyString(key: string): Buffer {
|
||||
export function decodeKeyString(key: string): Uint8Array {
|
||||
// Try base64 first
|
||||
try {
|
||||
const b = Buffer.from(key, 'base64');
|
||||
const b = base64ToBytes(key);
|
||||
if (b.length === 16) return b;
|
||||
} catch {}
|
||||
// Try hex (with or without 0x)
|
||||
@@ -15,7 +32,7 @@ function decodeKeyString(key: string): Buffer {
|
||||
if (hex.startsWith('0x')) hex = hex.slice(2);
|
||||
if (/^[0-9a-fA-F]{32}$/.test(hex)) {
|
||||
try {
|
||||
const b = Buffer.from(hex, 'hex');
|
||||
const b = hexToBytes(hex);
|
||||
if (b.length === 16) return b;
|
||||
} catch {}
|
||||
}
|
||||
@@ -26,12 +43,152 @@ function decodeKeyString(key: string): Buffer {
|
||||
* Returns the channel id for a given base64-encoded key.
|
||||
* Decodes the key, hashes it with SHA-256, and returns the first byte as hex.
|
||||
* Results are cached for performance.
|
||||
* Returns "00" for invalid/empty keys.
|
||||
*/
|
||||
export function getChannelIdFromKey(key: string): string {
|
||||
if (channelIdCache[key]) return channelIdCache[key];
|
||||
const keyBytes = decodeKeyString(key);
|
||||
const hash = createHash('sha256').update(keyBytes).digest();
|
||||
const id = hash[0].toString(16).padStart(2, '0');
|
||||
channelIdCache[key] = id;
|
||||
return id;
|
||||
|
||||
// Handle empty or invalid keys gracefully
|
||||
if (!key || key.trim() === '') {
|
||||
channelIdCache[key] = "00";
|
||||
return "00";
|
||||
}
|
||||
|
||||
try {
|
||||
const keyBytes = decodeKeyString(key);
|
||||
const hash = createHash('sha256').update(Buffer.from(keyBytes)).digest();
|
||||
const id = hash[0].toString(16).padStart(2, '0');
|
||||
channelIdCache[key] = id;
|
||||
return id;
|
||||
} catch (error) {
|
||||
// Return fallback for invalid keys
|
||||
channelIdCache[key] = "00";
|
||||
return "00";
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: HMAC-SHA256, returns Uint8Array
|
||||
async function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
|
||||
if (window.crypto?.subtle) {
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
"raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
|
||||
);
|
||||
const sig = await window.crypto.subtle.sign("HMAC", cryptoKey, data);
|
||||
return new Uint8Array(sig);
|
||||
} else {
|
||||
// Fallback: use a JS polyfill if needed (not implemented here)
|
||||
throw new Error("No WebCrypto support for HMAC-SHA256");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse decrypted MeshCore group message
|
||||
export function parseMeshcoreGroupMessage(decrypted: Uint8Array | string): {
|
||||
timestamp: number;
|
||||
msgType: number;
|
||||
sender: string;
|
||||
text: string;
|
||||
rawText: string;
|
||||
} | null {
|
||||
let buf: Uint8Array;
|
||||
if (typeof decrypted === "string") {
|
||||
buf = new TextEncoder().encode(decrypted);
|
||||
} else {
|
||||
buf = decrypted;
|
||||
}
|
||||
if (buf.length < 6) return null;
|
||||
// 1. Timestamp (4 bytes, little-endian)
|
||||
const timestamp = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24);
|
||||
// 2. MsgType (1 byte)
|
||||
const msgType = buf[4];
|
||||
// 3. Message text (null-terminated)
|
||||
let end = 5;
|
||||
while (end < buf.length && buf[end] !== 0) end++;
|
||||
const rawText = new TextDecoder().decode(buf.slice(5, end));
|
||||
// Try to split sender and text
|
||||
let sender = "";
|
||||
let text = rawText;
|
||||
const sepIdx = rawText.indexOf(": ");
|
||||
if (sepIdx !== -1) {
|
||||
sender = rawText.slice(0, sepIdx);
|
||||
text = rawText.slice(sepIdx + 2);
|
||||
}
|
||||
return { timestamp, msgType, sender, text, rawText };
|
||||
}
|
||||
|
||||
// Main decryption function
|
||||
export async function decryptMeshcoreGroupMessage({
|
||||
encrypted_message, // hex string or Uint8Array
|
||||
mac, // hex string or Uint8Array (2 bytes)
|
||||
channel_hash, // hex string (1 byte)
|
||||
knownKeys, // array of base64 strings
|
||||
parse = false,
|
||||
}: {
|
||||
encrypted_message: string | Uint8Array,
|
||||
mac: string | Uint8Array,
|
||||
channel_hash: string,
|
||||
knownKeys: string[],
|
||||
parse?: boolean,
|
||||
}): Promise<string | ReturnType<typeof parseMeshcoreGroupMessage> | null> {
|
||||
// Normalize inputs
|
||||
const ciphertext = typeof encrypted_message === "string" ? hexToBytes(encrypted_message) : encrypted_message;
|
||||
const macBytes = typeof mac === "string" ? hexToBytes(mac) : mac;
|
||||
const chash = channel_hash.toLowerCase();
|
||||
|
||||
const failures: { key: string, reason: string }[] = [];
|
||||
|
||||
for (const base64Key of knownKeys) {
|
||||
let keyBytes: Uint8Array;
|
||||
try {
|
||||
keyBytes = decodeKeyString(base64Key);
|
||||
} catch (e) {
|
||||
console.warn("Skipping invalid base64 meshcore key:", base64Key, e);
|
||||
failures.push({ key: base64Key, reason: `base64 decode error: ${e}` });
|
||||
continue;
|
||||
}
|
||||
const candidateHash = getChannelIdFromKey(base64Key);
|
||||
if (candidateHash !== chash) {
|
||||
failures.push({ key: base64Key, reason: `channel hash mismatch (expected ${chash}, got ${candidateHash})` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// MAC check
|
||||
let hmac;
|
||||
try {
|
||||
hmac = await hmacSha256(keyBytes, ciphertext);
|
||||
} catch (e) {
|
||||
failures.push({ key: base64Key, reason: `HMAC error: ${e}` });
|
||||
continue;
|
||||
}
|
||||
if (macBytes.length !== 2 || hmac[0] !== macBytes[0] || hmac[1] !== macBytes[1]) {
|
||||
failures.push({ key: base64Key, reason: `MAC mismatch (expected ${Array.from(macBytes).map(b=>b.toString(16).padStart(2,'0')).join('')}, got ${Array.from(hmac.slice(0,2)).map(b=>b.toString(16).padStart(2,'0')).join('')})` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// AES-128-ECB decrypt
|
||||
try {
|
||||
const aesEcb = new aesjs.ModeOfOperation.ecb(keyBytes);
|
||||
const decrypted = aesEcb.decrypt(ciphertext);
|
||||
// Remove trailing nulls/zeros
|
||||
let end = decrypted.length;
|
||||
while (end > 0 && decrypted[end - 1] === 0) end--;
|
||||
const plainBytes = decrypted.slice(0, end);
|
||||
if (parse) {
|
||||
return parseMeshcoreGroupMessage(plainBytes);
|
||||
} else {
|
||||
return new TextDecoder().decode(plainBytes);
|
||||
}
|
||||
} catch (e) {
|
||||
failures.push({ key: base64Key, reason: `AES decryption error: ${e}` });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
console.info("Meshcore decryption failed for message", {
|
||||
channel_hash: chash,
|
||||
mac: Array.from(macBytes).map(b=>b.toString(16).padStart(2,'0')).join(''),
|
||||
knownKeysTried: knownKeys,
|
||||
failures,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
import { getChannelIdFromKey } from "./meshcore";
|
||||
import aesjs from "aes-js";
|
||||
|
||||
// Helper: Convert hex string to Uint8Array
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
if (hex.startsWith("0x")) hex = hex.slice(2);
|
||||
if (hex.length % 2 !== 0) hex = "0" + hex;
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Helper: Convert base64 to Uint8Array
|
||||
function base64ToBytes(b64: string): Uint8Array {
|
||||
return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
// Add a helper to decode base64 or hex
|
||||
function decodeKeyString(key: string): Uint8Array {
|
||||
// Try base64 first
|
||||
try {
|
||||
const b = Uint8Array.from(atob(key), c => c.charCodeAt(0));
|
||||
if (b.length === 16) return b;
|
||||
} catch {}
|
||||
// Try hex (with or without 0x)
|
||||
let hex = key.trim();
|
||||
if (hex.startsWith('0x')) hex = hex.slice(2);
|
||||
if (/^[0-9a-fA-F]{32}$/.test(hex)) {
|
||||
try {
|
||||
const b = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
b[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
||||
}
|
||||
if (b.length === 16) return b;
|
||||
} catch {}
|
||||
}
|
||||
throw new Error('Invalid key format: must be 16 bytes, base64 or hex');
|
||||
}
|
||||
|
||||
// Helper: HMAC-SHA256, returns Uint8Array
|
||||
async function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
|
||||
if (window.crypto?.subtle) {
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
"raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
|
||||
);
|
||||
const sig = await window.crypto.subtle.sign("HMAC", cryptoKey, data);
|
||||
return new Uint8Array(sig);
|
||||
} else {
|
||||
// Fallback: use a JS polyfill if needed (not implemented here)
|
||||
throw new Error("No WebCrypto support for HMAC-SHA256");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse decrypted MeshCore group message
|
||||
export function parseMeshcoreGroupMessage(decrypted: Uint8Array | string): {
|
||||
timestamp: number;
|
||||
msgType: number;
|
||||
sender: string;
|
||||
text: string;
|
||||
rawText: string;
|
||||
} | null {
|
||||
let buf: Uint8Array;
|
||||
if (typeof decrypted === "string") {
|
||||
buf = new TextEncoder().encode(decrypted);
|
||||
} else {
|
||||
buf = decrypted;
|
||||
}
|
||||
if (buf.length < 6) return null;
|
||||
// 1. Timestamp (4 bytes, little-endian)
|
||||
const timestamp = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24);
|
||||
// 2. MsgType (1 byte)
|
||||
const msgType = buf[4];
|
||||
// 3. Message text (null-terminated)
|
||||
let end = 5;
|
||||
while (end < buf.length && buf[end] !== 0) end++;
|
||||
const rawText = new TextDecoder().decode(buf.slice(5, end));
|
||||
// Try to split sender and text
|
||||
let sender = "";
|
||||
let text = rawText;
|
||||
const sepIdx = rawText.indexOf(": ");
|
||||
if (sepIdx !== -1) {
|
||||
sender = rawText.slice(0, sepIdx);
|
||||
text = rawText.slice(sepIdx + 2);
|
||||
}
|
||||
return { timestamp, msgType, sender, text, rawText };
|
||||
}
|
||||
|
||||
// Main decryption function
|
||||
export async function decryptMeshcoreGroupMessage({
|
||||
encrypted_message, // hex string or Uint8Array
|
||||
mac, // hex string or Uint8Array (2 bytes)
|
||||
channel_hash, // hex string (1 byte)
|
||||
knownKeys, // array of base64 strings
|
||||
parse = false,
|
||||
}: {
|
||||
encrypted_message: string | Uint8Array,
|
||||
mac: string | Uint8Array,
|
||||
channel_hash: string,
|
||||
knownKeys: string[],
|
||||
parse?: boolean,
|
||||
}): Promise<string | ReturnType<typeof parseMeshcoreGroupMessage> | null> {
|
||||
// Normalize inputs
|
||||
const ciphertext = typeof encrypted_message === "string" ? hexToBytes(encrypted_message) : encrypted_message;
|
||||
const macBytes = typeof mac === "string" ? hexToBytes(mac) : mac;
|
||||
const chash = channel_hash.toLowerCase();
|
||||
|
||||
const failures: { key: string, reason: string }[] = [];
|
||||
|
||||
for (const base64Key of knownKeys) {
|
||||
let keyBytes: Uint8Array;
|
||||
try {
|
||||
keyBytes = decodeKeyString(base64Key);
|
||||
} catch (e) {
|
||||
console.warn("Skipping invalid base64 meshcore key:", base64Key, e);
|
||||
failures.push({ key: base64Key, reason: `base64 decode error: ${e}` });
|
||||
continue;
|
||||
}
|
||||
const candidateHash = getChannelIdFromKey(base64Key);
|
||||
if (candidateHash !== chash) {
|
||||
failures.push({ key: base64Key, reason: `channel hash mismatch (expected ${chash}, got ${candidateHash})` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// MAC check
|
||||
let hmac;
|
||||
try {
|
||||
hmac = await hmacSha256(keyBytes, ciphertext);
|
||||
} catch (e) {
|
||||
failures.push({ key: base64Key, reason: `HMAC error: ${e}` });
|
||||
continue;
|
||||
}
|
||||
if (macBytes.length !== 2 || hmac[0] !== macBytes[0] || hmac[1] !== macBytes[1]) {
|
||||
failures.push({ key: base64Key, reason: `MAC mismatch (expected ${Array.from(macBytes).map(b=>b.toString(16).padStart(2,'0')).join('')}, got ${Array.from(hmac.slice(0,2)).map(b=>b.toString(16).padStart(2,'0')).join('')})` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// AES-128-ECB decrypt
|
||||
try {
|
||||
const aesEcb = new aesjs.ModeOfOperation.ecb(keyBytes);
|
||||
const decrypted = aesEcb.decrypt(ciphertext);
|
||||
// Remove trailing nulls/zeros
|
||||
let end = decrypted.length;
|
||||
while (end > 0 && decrypted[end - 1] === 0) end--;
|
||||
const plainBytes = decrypted.slice(0, end);
|
||||
if (parse) {
|
||||
return parseMeshcoreGroupMessage(plainBytes);
|
||||
} else {
|
||||
return new TextDecoder().decode(plainBytes);
|
||||
}
|
||||
} catch (e) {
|
||||
failures.push({ key: base64Key, reason: `AES decryption error: ${e}` });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
console.info("Meshcore decryption failed for message", {
|
||||
channel_hash: chash,
|
||||
mac: Array.from(macBytes).map(b=>b.toString(16).padStart(2,'0')).join(''),
|
||||
knownKeysTried: knownKeys,
|
||||
failures,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user