mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-04 03:52:34 +00:00
The web viewer's static/js/meshcore-browser.mjs was an esbuild bundle of @michaelhart/meshcore-decoder at 0.2.7 or older, added without a record of its version or how it was built. Those versions read the path-length byte as a plain byte count, so every packet with 2- or 3-byte hops was misparsed from the path onward: wrong header and type, a path split into single bytes, garbage payload fields, garbled advert names and group text that never decrypted. The bundle is rebuilt from 0.3.0, which takes the hop count from bits 0-5 and the hash size from bits 6-7. Twelve real packets from the bot's database (adverts, group text, direct messages and returned paths at 1, 2 and 3 bytes per hop) decode to the same hop count, path and payload as the bot's own decoder; multibyte adverts get their real names and a 2-byte-hop Public message decrypts, on the tape and in the byte breakdown. scripts/meshcore-decoder-browser/ holds the browser entry the old bundle was built from (recovered from the bundle: it re-exports the library and routes Ed25519's SHA-512 through crypto-js so signatures verify on a plain-HTTP page) and build.sh, which fetches the published package, installs its runtime dependencies with install scripts disabled and bundles it with a pinned esbuild in a temporary directory. It reproduces the committed bundle byte for byte. The unused meshcore-browser.min.mjs is removed, and the multibyte workarounds from the byte breakdown polish (skipping the browser decode and saying so) are taken out again.
119 lines
4.3 KiB
TypeScript
119 lines
4.3 KiB
TypeScript
// Browser entry for meshcore-bot's web viewer, bundled with esbuild into
|
|
// modules/web_viewer/static/js/meshcore-browser.mjs by build.sh next to this file.
|
|
// It re-exports the library and routes Ed25519's SHA-512 through crypto-js,
|
|
// so advert signatures verify on a plain-HTTP LAN page where crypto.subtle
|
|
// doesn't exist.
|
|
|
|
import CryptoJS from 'crypto-js';
|
|
import * as ed25519 from '@noble/ed25519';
|
|
import { MeshCorePacketDecoder } from '../decoder/packet-decoder';
|
|
import { DecryptionOptions } from '../types/crypto';
|
|
import { hexToBytes, bytesToHex } from '../utils/hex';
|
|
|
|
export { MeshCorePacketDecoder };
|
|
export { MeshCorePacketDecoder as MeshCoreDecoder };
|
|
export {
|
|
RouteType,
|
|
PayloadType,
|
|
PayloadVersion,
|
|
DeviceRole,
|
|
AdvertFlags,
|
|
RequestType,
|
|
ControlSubType
|
|
} from '../types/enums';
|
|
export { MeshCoreKeyStore } from '../crypto/key-manager';
|
|
export { ChannelCrypto } from '../crypto/channel-crypto';
|
|
export { hexToBytes, bytesToHex, byteToHex, numberToHex } from '../utils/hex';
|
|
export {
|
|
getRouteTypeName,
|
|
getPayloadTypeName,
|
|
getPayloadVersionName,
|
|
getDeviceRoleName,
|
|
getRequestTypeName,
|
|
getControlSubTypeName
|
|
} from '../utils/enum-names';
|
|
export { ed25519 };
|
|
|
|
function sha512Pure(data: Uint8Array): Uint8Array {
|
|
const wordArray = CryptoJS.lib.WordArray.create(data as unknown as number[]);
|
|
const hash = CryptoJS.SHA512(wordArray);
|
|
const hashBytes = new Uint8Array(64);
|
|
for (let i = 0; i < 16; i++) {
|
|
const word = hash.words[i] || 0;
|
|
hashBytes[i * 4] = (word >>> 24) & 0xff;
|
|
hashBytes[i * 4 + 1] = (word >>> 16) & 0xff;
|
|
hashBytes[i * 4 + 2] = (word >>> 8) & 0xff;
|
|
hashBytes[i * 4 + 3] = word & 0xff;
|
|
}
|
|
return hashBytes;
|
|
}
|
|
|
|
ed25519.etc.sha512Sync = (...messages: Uint8Array[]) => sha512Pure(ed25519.etc.concatBytes(...messages));
|
|
ed25519.etc.sha512Async = async (...messages: Uint8Array[]) => sha512Pure(ed25519.etc.concatBytes(...messages));
|
|
|
|
export class Ed25519SignatureVerifier {
|
|
/**
|
|
* Verify an Ed25519 signature for MeshCore advertisement packets
|
|
* Uses @noble/ed25519 with crypto-js SHA-512 (works in insecure contexts)
|
|
*/
|
|
static async verifyAdvertisementSignature(
|
|
publicKeyHex: string,
|
|
signatureHex: string,
|
|
timestamp: number,
|
|
appDataHex: string
|
|
): Promise<boolean> {
|
|
try {
|
|
const publicKey = hexToBytes(publicKeyHex);
|
|
const signature = hexToBytes(signatureHex);
|
|
const appData = hexToBytes(appDataHex);
|
|
const message = this.constructAdvertSignedMessage(publicKeyHex, timestamp, appData);
|
|
return ed25519.verify(signature, message, publicKey);
|
|
} catch (error) {
|
|
console.error('Ed25519 signature verification failed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static constructAdvertSignedMessage(publicKeyHex: string, timestamp: number, appData: Uint8Array): Uint8Array {
|
|
const publicKey = hexToBytes(publicKeyHex);
|
|
const timestampBytes = new Uint8Array(4);
|
|
timestampBytes[0] = timestamp & 0xff;
|
|
timestampBytes[1] = (timestamp >> 8) & 0xff;
|
|
timestampBytes[2] = (timestamp >> 16) & 0xff;
|
|
timestampBytes[3] = (timestamp >> 24) & 0xff;
|
|
const message = new Uint8Array(32 + 4 + appData.length);
|
|
message.set(publicKey, 0);
|
|
message.set(timestampBytes, 32);
|
|
message.set(appData, 36);
|
|
return message;
|
|
}
|
|
|
|
static getSignedMessageHex(publicKeyHex: string, timestamp: number, appDataHex: string): string {
|
|
const appData = hexToBytes(appDataHex);
|
|
return bytesToHex(this.constructAdvertSignedMessage(publicKeyHex, timestamp, appData));
|
|
}
|
|
}
|
|
|
|
export function decodePacket(hexData: string, options?: DecryptionOptions) {
|
|
return MeshCorePacketDecoder.decode(hexData, options);
|
|
}
|
|
|
|
export function analyzePacket(hexData: string, options?: DecryptionOptions) {
|
|
return MeshCorePacketDecoder.analyzeStructure(hexData, options);
|
|
}
|
|
|
|
export async function decodePacketWithVerification(hexData: string, options?: DecryptionOptions) {
|
|
return MeshCorePacketDecoder.decodeWithVerification(hexData, options);
|
|
}
|
|
|
|
export function isSecureContext(): boolean {
|
|
if (typeof globalThis === 'undefined' || typeof (globalThis as any).window === 'undefined') {
|
|
return true;
|
|
}
|
|
return (globalThis as any).isSecureContext ?? false;
|
|
}
|
|
|
|
export function hasWebCrypto(): boolean {
|
|
return typeof globalThis !== 'undefined' && globalThis.crypto !== undefined && globalThis.crypto.subtle !== undefined;
|
|
}
|