Update CLAUDE.mds and fold in meshcore cracker lib

This commit is contained in:
Jack Kingsman
2026-01-07 18:42:50 -08:00
parent 9d29ef059b
commit 6d3cafbe4f
45 changed files with 388910 additions and 4 deletions

View File

@@ -15,7 +15,7 @@ A web interface for MeshCore mesh radio networks. The backend connects to a Mesh
│ │ StatusBar│ │ Sidebar │ │MessageList│ │ MessageInput │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ RawPacketList + CrackerPanel (WebGPU key bruteforcing) │ │
│ │ CrackerPanel (global collapsible, WebGPU cracking) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ useWebSocket ←──── Real-time updates │

View File

@@ -11,6 +11,7 @@ This document provides context for AI assistants and developers working on the R
- **Sonner** - Toast notifications
- **shadcn/ui components** - Sheet, Tabs, Button (in `components/ui/`)
- **meshcore-cracker** - WebGPU-accelerated channel key bruteforcing
- **nosleep.js** - Prevents device sleep during cracking
## Directory Structure
@@ -448,6 +449,22 @@ npm run build
# Then run backend: uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
```
## URL Hash Navigation
Deep linking to conversations via URL hash:
- `#channel/RoomName` - Opens a channel (leading `#` stripped from name for cleaner URLs)
- `#contact/ContactName` - Opens a DM
- `#raw` - Opens the raw packet feed
```typescript
// Parse hash on initial load
const hashConv = parseHashConversation();
// Update hash when conversation changes (uses replaceState to avoid history pollution)
window.history.replaceState(null, '', newHash);
```
## CrackerPanel
The `CrackerPanel` component provides WebGPU-accelerated brute-forcing of channel keys for undecrypted GROUP_TEXT packets.
@@ -460,6 +477,8 @@ The `CrackerPanel` component provides WebGPU-accelerated brute-forcing of channe
- **Auto-channel creation**: Cracked channels are automatically added to the channel list
- **Configurable max length**: Adjustable while running (default: 6)
- **Retry failed**: Option to retry failed packets at increasing lengths
- **NoSleep integration**: Prevents device sleep during cracking via `nosleep.js`
- **Global collapsible panel**: Toggle from sidebar, runs in background when hidden
### Key Implementation Patterns
@@ -475,6 +494,12 @@ const maxLengthRef = useRef(6);
Progress reporting shows rate in Mkeys/s or Gkeys/s depending on speed.
## Sidebar Features
- **Sort toggle**: Default is 'recent' (most recent message first), can toggle to alphabetical
- **Mark all as read**: Button appears when there are unread messages, clears all unread counts
- **Cracker toggle**: Shows/hides the global cracker panel with running status indicator
## Toast Notifications
The app uses Sonner for toast notifications via a custom wrapper at `components/ui/sonner.tsx`:

View File

@@ -0,0 +1,220 @@
# MeshCore Cracker
Standalone library for cracking MeshCore GroupText packets using WebGPU-accelerated brute force.
## Features
- WebGPU-accelerated brute force (100M+ keys/second on modern GPUs)
- Dictionary attack support with external wordlist
- Configurable timestamp and UTF-8 filters
- Progress callbacks with ETA
- Resume support for interrupted searches
- Clean ESM API
## Installation
```bash
npm install meshcore-cracker
```
## Usage
### Basic Usage
```typescript
import { GroupTextCracker } from 'meshcore-cracker';
const cracker = new GroupTextCracker();
const result = await cracker.crack(packetHex, {
maxLength: 6,
});
if (result.found) {
console.log(`Room: #${result.roomName}`);
console.log(`Key: ${result.key}`);
console.log(`Message: ${result.decryptedMessage}`);
}
cracker.destroy();
```
### With Progress Callback
```typescript
const result = await cracker.crack(packetHex, {
maxLength: 8,
useTimestampFilter: true,
useUtf8Filter: true,
}, (progress) => {
console.log(`Progress: ${progress.percent.toFixed(1)}%`);
console.log(`Rate: ${(progress.rateKeysPerSec / 1e6).toFixed(2)} Mkeys/s`);
console.log(`ETA: ${progress.etaSeconds.toFixed(0)}s`);
console.log(`Phase: ${progress.phase}`);
});
```
### With Dictionary Attack
```typescript
const cracker = new GroupTextCracker();
// Load wordlist from URL
await cracker.loadWordlist('/words_alpha.txt');
// Or set wordlist directly
cracker.setWordlist(['test', 'hello', 'world']);
const result = await cracker.crack(packetHex, { maxLength: 6 });
```
### Aborting and Resuming
```typescript
const cracker = new GroupTextCracker();
// Start cracking (in background)
const crackPromise = cracker.crack(packetHex, { maxLength: 8 }, (progress) => {
// Abort after 10 seconds
if (progress.elapsedSeconds > 10) {
cracker.abort();
}
});
const result = await crackPromise;
if (result.aborted && result.resumeFrom) {
// Resume later from where we left off
const resumed = await cracker.crack(packetHex, {
maxLength: 8,
startFrom: result.resumeFrom,
});
}
```
## API Reference
### GroupTextCracker
Main class for cracking GroupText packets.
#### Methods
##### `crack(packetHex, options?, onProgress?): Promise<CrackResult>`
Crack a GroupText packet to find the room name and decrypt the message.
**Parameters:**
- `packetHex: string` - The packet data as a hex string
- `options?: CrackOptions` - Cracking options
- `onProgress?: ProgressCallback` - Optional progress callback
**Returns:** `Promise<CrackResult>`
##### `loadWordlist(url: string): Promise<void>`
Load a wordlist from a URL for dictionary attacks.
##### `setWordlist(words: string[]): void`
Set the wordlist directly from an array.
##### `abort(): void`
Abort the current cracking operation.
##### `isGpuAvailable(): boolean`
Check if WebGPU is available.
##### `destroy(): void`
Clean up GPU resources.
### CrackOptions
```typescript
interface CrackOptions {
maxLength?: number; // Max room name length (default: 8)
useTimestampFilter?: boolean; // Filter old timestamps (default: true)
useUtf8Filter?: boolean; // Filter invalid UTF-8 (default: true)
startFrom?: string; // Resume from position
}
```
### CrackResult
```typescript
interface CrackResult {
found: boolean;
roomName?: string; // Room name without '#'
key?: string; // Encryption key (hex)
decryptedMessage?: string; // Decrypted message
aborted?: boolean; // Was operation aborted
resumeFrom?: string; // Position for resume
error?: string; // Error message
}
```
### ProgressReport
```typescript
interface ProgressReport {
checked: number; // Candidates checked
total: number; // Total candidates
percent: number; // Progress 0-100
rateKeysPerSec: number; // Current rate
etaSeconds: number; // Estimated time remaining
elapsedSeconds: number; // Time elapsed
currentLength: number; // Current room name length
currentPosition: string; // Current position
phase: 'public-key' | 'wordlist' | 'bruteforce';
}
```
## Utility Functions
For advanced usage, the library also exports utility functions:
```typescript
import {
deriveKeyFromRoomName, // Derive key from room name
getChannelHash, // Get channel hash from key
verifyMac, // Verify MAC
isTimestampValid, // Check timestamp validity
isValidUtf8, // Check UTF-8 validity
indexToRoomName, // Convert index to room name
roomNameToIndex, // Convert room name to index
countNamesForLength, // Count names for a length
isWebGpuSupported, // Check WebGPU support
PUBLIC_ROOM_NAME, // "[[public room]]"
PUBLIC_KEY, // Public room key
} from 'meshcore-cracker';
```
## Browser Requirements
- WebGPU support (Chrome 113+, Edge 113+, or other Chromium-based browsers)
- Falls back gracefully with an error if WebGPU is not available
## Performance
Typical performance on modern hardware:
- **GPU (RTX 3080)**: ~500M keys/second
- **GPU (integrated)**: ~50M keys/second
Search space by room name length:
| Length | Candidates | Time @ 100M/s |
|--------|------------|---------------|
| 1 | 36 | instant |
| 2 | 1,296 | instant |
| 3 | 47,952 | instant |
| 4 | 1,774,224 | <1s |
| 5 | 65,646,288 | <1s |
| 6 | 2,428,912,656 | ~24s |
| 7 | 89,869,768,272 | ~15min |
| 8 | 3,325,181,426,064 | ~9h |
## License
MIT

View File

@@ -0,0 +1,15 @@
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
format: 'esm',
outfile: 'dist/index.js',
sourcemap: true,
target: 'es2020',
platform: 'browser',
external: [],
minify: false,
});
console.log('Build complete: dist/index.js');

View File

@@ -0,0 +1,65 @@
export declare const CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
export declare const CHARS_LEN: number;
export declare const CHARS_WITH_DASH: string;
export declare const PUBLIC_ROOM_NAME = "[[public room]]";
export declare const PUBLIC_KEY = "8b3387e9c5cdea6ac9e5edbaa115cd72";
/**
* Convert room name to (length, index) for resuming/skipping.
* Index encoding: LSB-first (first character = least significant digit).
*/
export declare function roomNameToIndex(name: string): {
length: number;
index: number;
} | null;
/**
* Convert (length, index) to room name.
* Index encoding: LSB-first (first character = least significant digit).
*/
export declare function indexToRoomName(length: number, idx: number): string | null;
/**
* Derive 128-bit key from room name using SHA256.
* Room names are prefixed with '#' before hashing.
*/
export declare function deriveKeyFromRoomName(roomName: string): string;
/**
* Compute channel hash (first byte of SHA256(key)).
*/
export declare function getChannelHash(keyHex: string): string;
/**
* Verify MAC using HMAC-SHA256 with 32-byte padded key.
*/
export declare function verifyMac(ciphertext: string, cipherMac: string, keyHex: string): boolean;
/**
* Count valid room names for a given length.
* Accounts for dash rules (no start/end dash, no consecutive dashes).
*/
export declare function countNamesForLength(len: number): number;
/**
* Check if timestamp is within last month.
*/
export declare function isTimestampValid(timestamp: number, now?: number): boolean;
/**
* Check for valid UTF-8 (no replacement characters).
*/
export declare function isValidUtf8(text: string): boolean;
/**
* Room name generator - iterates through all valid room names.
*/
export declare class RoomNameGenerator {
private length;
private indices;
private done;
private currentInLength;
private totalForLength;
current(): string;
getLength(): number;
getCurrentInLength(): number;
getTotalForLength(): number;
getRemainingInLength(): number;
isDone(): boolean;
next(): boolean;
private isValid;
nextValid(): boolean;
skipTo(targetLength: number, targetIndex: number): void;
}
//# sourceMappingURL=core.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,KAAK,yCAAyC,CAAC;AAC5D,eAAO,MAAM,SAAS,QAAe,CAAC;AACtC,eAAO,MAAM,eAAe,QAAc,CAAC;AAG3C,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAClD,eAAO,MAAM,UAAU,qCAAqC,CAAC;AAE7D;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA+BtF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B1E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKxF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAwBvD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAIzE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAAa;IAEnC,OAAO,IAAI,MAAM;IAIjB,SAAS,IAAI,MAAM;IAInB,kBAAkB,IAAI,MAAM;IAI5B,iBAAiB,IAAI,MAAM;IAI3B,oBAAoB,IAAI,MAAM;IAI9B,MAAM,IAAI,OAAO;IAIjB,IAAI,IAAI,OAAO;IA6Cf,OAAO,CAAC,OAAO;IAcf,SAAS,IAAI,OAAO;IAWpB,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;CAiBxD"}

View File

@@ -0,0 +1,240 @@
// Core logic for MeshCore packet cracker - pure functions
import SHA256 from 'crypto-js/sha256';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import Hex from 'crypto-js/enc-hex';
// Room name character set
export const CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789';
export const CHARS_LEN = CHARS.length; // 36
export const CHARS_WITH_DASH = CHARS + '-';
// Public room special case
export const PUBLIC_ROOM_NAME = '[[public room]]';
export const PUBLIC_KEY = '8b3387e9c5cdea6ac9e5edbaa115cd72';
/**
* Convert room name to (length, index) for resuming/skipping.
* Index encoding: LSB-first (first character = least significant digit).
*/
export function roomNameToIndex(name) {
if (!name || name.length === 0) {
return null;
}
const length = name.length;
let index = 0;
let multiplier = 1;
// Process from left to right (first char is LSB, matching indexToRoomName)
for (let i = 0; i < length; i++) {
const c = name[i];
const charIdx = CHARS_WITH_DASH.indexOf(c);
if (charIdx === -1) {
return null;
} // Invalid character
const isFirst = i === 0;
const isLast = i === length - 1;
const charCount = isFirst || isLast ? 36 : 37;
// Dash not allowed at start/end
if ((isFirst || isLast) && charIdx === 36) {
return null;
}
index += charIdx * multiplier;
multiplier *= charCount;
}
return { length, index };
}
/**
* Convert (length, index) to room name.
* Index encoding: LSB-first (first character = least significant digit).
*/
export function indexToRoomName(length, idx) {
if (length <= 0) {
return null;
}
let result = '';
let remaining = idx;
let prevWasDash = false;
for (let i = 0; i < length; i++) {
const isFirst = i === 0;
const isLast = i === length - 1;
const charCount = isFirst || isLast ? 36 : 37;
const charIdx = remaining % charCount;
remaining = Math.floor(remaining / charCount);
const isDash = charIdx === 36;
if (isDash && prevWasDash) {
return null;
} // Invalid: consecutive dashes
prevWasDash = isDash;
result += CHARS_WITH_DASH[charIdx];
}
return result;
}
/**
* Derive 128-bit key from room name using SHA256.
* Room names are prefixed with '#' before hashing.
*/
export function deriveKeyFromRoomName(roomName) {
if (roomName === PUBLIC_ROOM_NAME) {
return PUBLIC_KEY;
}
const hash = SHA256(roomName);
return hash.toString(Hex).substring(0, 32);
}
/**
* Compute channel hash (first byte of SHA256(key)).
*/
export function getChannelHash(keyHex) {
const hash = SHA256(Hex.parse(keyHex));
return hash.toString(Hex).substring(0, 2);
}
/**
* Verify MAC using HMAC-SHA256 with 32-byte padded key.
*/
export function verifyMac(ciphertext, cipherMac, keyHex) {
const paddedKey = keyHex.padEnd(64, '0');
const hmac = HmacSHA256(Hex.parse(ciphertext), Hex.parse(paddedKey));
const computed = hmac.toString(Hex).substring(0, 4).toLowerCase();
return computed === cipherMac.toLowerCase();
}
/**
* Count valid room names for a given length.
* Accounts for dash rules (no start/end dash, no consecutive dashes).
*/
export function countNamesForLength(len) {
if (len === 1) {
return CHARS_LEN;
}
if (len === 2) {
return CHARS_LEN * CHARS_LEN;
}
// For length >= 3: first and last are CHARS (36), middle follows no-consecutive-dash rule
// Middle length = len - 2
// Use DP: count sequences of length k with no consecutive dashes
// endsWithNonDash[k], endsWithDash[k]
let endsNonDash = CHARS_LEN; // length 1 middle
let endsDash = 1;
for (let i = 2; i <= len - 2; i++) {
const newEndsNonDash = (endsNonDash + endsDash) * CHARS_LEN;
const newEndsDash = endsNonDash; // dash can only follow non-dash
endsNonDash = newEndsNonDash;
endsDash = newEndsDash;
}
const middleCount = len > 2 ? endsNonDash + endsDash : 1;
return CHARS_LEN * middleCount * CHARS_LEN;
}
/**
* Check if timestamp is within last month.
*/
export function isTimestampValid(timestamp, now) {
const ONE_MONTH_SECONDS = 30 * 24 * 60 * 60;
const currentTime = now ?? Math.floor(Date.now() / 1000);
return timestamp <= currentTime && timestamp >= currentTime - ONE_MONTH_SECONDS;
}
/**
* Check for valid UTF-8 (no replacement characters).
*/
export function isValidUtf8(text) {
return !text.includes('\uFFFD');
}
/**
* Room name generator - iterates through all valid room names.
*/
export class RoomNameGenerator {
constructor() {
this.length = 1;
this.indices = [0];
this.done = false;
this.currentInLength = 0;
this.totalForLength = CHARS_LEN;
}
current() {
return this.indices.map((i) => (i === CHARS_LEN ? '-' : CHARS[i])).join('');
}
getLength() {
return this.length;
}
getCurrentInLength() {
return this.currentInLength;
}
getTotalForLength() {
return this.totalForLength;
}
getRemainingInLength() {
return this.totalForLength - this.currentInLength;
}
isDone() {
return this.done;
}
next() {
if (this.done) {
return false;
}
this.currentInLength++;
// Increment with carry, respecting dash rules
let pos = this.length - 1;
while (pos >= 0) {
const isFirst = pos === 0;
const isLast = pos === this.length - 1;
const maxVal = isFirst || isLast ? CHARS_LEN - 1 : CHARS_LEN; // CHARS_LEN = dash index
if (this.indices[pos] < maxVal) {
this.indices[pos]++;
// Check dash rule: no consecutive dashes
if (this.indices[pos] === CHARS_LEN && pos > 0 && this.indices[pos - 1] === CHARS_LEN) {
// Would create consecutive dashes, continue incrementing
continue;
}
// Reset all positions after this one
for (let i = pos + 1; i < this.length; i++) {
this.indices[i] = 0;
}
// Validate: check no consecutive dashes in reset portion
if (this.isValid()) {
return true;
}
continue;
}
pos--;
}
// Overflow - increase length
this.length++;
this.indices = new Array(this.length).fill(0);
this.currentInLength = 0;
this.totalForLength = countNamesForLength(this.length);
return true;
}
isValid() {
for (let i = 0; i < this.length; i++) {
const isDash = this.indices[i] === CHARS_LEN;
if (isDash && (i === 0 || i === this.length - 1)) {
return false;
}
if (isDash && i > 0 && this.indices[i - 1] === CHARS_LEN) {
return false;
}
}
return true;
}
// Skip invalid combinations efficiently
nextValid() {
do {
if (!this.next()) {
return false;
}
} while (!this.isValid());
return true;
}
// Skip to a specific (length, index) position
// Index encoding: first char is LSB (consistent with indexToRoomName)
skipTo(targetLength, targetIndex) {
this.length = targetLength;
this.indices = new Array(targetLength).fill(0);
this.totalForLength = countNamesForLength(targetLength);
// Convert index to indices array (LSB first = position 0)
let remaining = targetIndex;
for (let i = 0; i < targetLength; i++) {
const isFirst = i === 0;
const isLast = i === targetLength - 1;
const charCount = isFirst || isLast ? CHARS_LEN : CHARS_LEN + 1;
this.indices[i] = remaining % charCount;
remaining = Math.floor(remaining / charCount);
}
this.currentInLength = targetIndex;
}
}
//# sourceMappingURL=core.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,61 @@
/**
* GroupTextCracker - Standalone MeshCore GroupText packet cracker
*
* Cracks encrypted GroupText packets by trying room names until the
* correct encryption key is found.
*/
import type { CrackOptions, CrackResult, ProgressCallback, DecodedPacket } from './types';
/**
* Main cracker class for MeshCore GroupText packets.
*/
export declare class GroupTextCracker {
private gpuInstance;
private wordlist;
private abortFlag;
private useTimestampFilter;
private useUtf8Filter;
/**
* Load a wordlist from a URL for dictionary attacks.
* The wordlist should be a text file with one word per line.
*
* @param url - URL to fetch the wordlist from
*/
loadWordlist(url: string): Promise<void>;
/**
* Set the wordlist directly from an array of words.
*
* @param words - Array of room names to try
*/
setWordlist(words: string[]): void;
/**
* Abort the current cracking operation.
* The crack() method will return with aborted: true.
*/
abort(): void;
/**
* Check if WebGPU is available in the current environment.
*/
isGpuAvailable(): boolean;
/**
* Decode a packet and extract the information needed for cracking.
*
* @param packetHex - The packet data as a hex string
* @returns Decoded packet info or null if not a GroupText packet
*/
decodePacket(packetHex: string): Promise<DecodedPacket | null>;
/**
* Crack a GroupText packet to find the room name and decrypt the message.
*
* @param packetHex - The packet data as a hex string
* @param options - Cracking options
* @param onProgress - Optional callback for progress updates
* @returns The cracking result
*/
crack(packetHex: string, options?: CrackOptions, onProgress?: ProgressCallback): Promise<CrackResult>;
/**
* Clean up GPU resources.
* Call this when you're done using the cracker.
*/
destroy(): void;
}
//# sourceMappingURL=cracker.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"cracker.d.ts","sourceRoot":"","sources":["../src/cracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAkB,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAe1G;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,aAAa,CAAQ;IAE7B;;;;;OAKG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9C;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAMlC;;;OAGG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,cAAc,IAAI,OAAO;IAIzB;;;;;OAKG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IA8BpE;;;;;;;OAOG;IACG,KAAK,CACT,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,YAAY,EACtB,UAAU,CAAC,EAAE,gBAAgB,GAC5B,OAAO,CAAC,WAAW,CAAC;IA0PvB;;;OAGG;IACH,OAAO,IAAI,IAAI;CAMhB"}

View File

@@ -0,0 +1,327 @@
/**
* GroupTextCracker - Standalone MeshCore GroupText packet cracker
*
* Cracks encrypted GroupText packets by trying room names until the
* correct encryption key is found.
*/
import { MeshCorePacketDecoder, ChannelCrypto } from '@michaelhart/meshcore-decoder';
import { GpuBruteForce, isWebGpuSupported } from './gpu-bruteforce';
import { PUBLIC_ROOM_NAME, PUBLIC_KEY, indexToRoomName, roomNameToIndex, deriveKeyFromRoomName, getChannelHash, verifyMac, countNamesForLength, isTimestampValid, isValidUtf8, } from './core';
// Valid room name characters (for wordlist filtering)
const VALID_CHARS = /^[a-z0-9-]+$/;
const NO_DASH_AT_ENDS = /^[a-z0-9].*[a-z0-9]$|^[a-z0-9]$/;
const NO_CONSECUTIVE_DASHES = /--/;
function isValidRoomName(name) {
if (!name || name.length === 0)
return false;
if (!VALID_CHARS.test(name))
return false;
if (name.length > 1 && !NO_DASH_AT_ENDS.test(name))
return false;
if (NO_CONSECUTIVE_DASHES.test(name))
return false;
return true;
}
/**
* Main cracker class for MeshCore GroupText packets.
*/
export class GroupTextCracker {
constructor() {
this.gpuInstance = null;
this.wordlist = [];
this.abortFlag = false;
this.useTimestampFilter = true;
this.useUtf8Filter = true;
}
/**
* Load a wordlist from a URL for dictionary attacks.
* The wordlist should be a text file with one word per line.
*
* @param url - URL to fetch the wordlist from
*/
async loadWordlist(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load wordlist: ${response.status} ${response.statusText}`);
}
const text = await response.text();
const allWords = text
.split('\n')
.map((w) => w.trim().toLowerCase())
.filter((w) => w.length > 0);
// Filter to valid room names only
this.wordlist = allWords.filter(isValidRoomName);
}
/**
* Set the wordlist directly from an array of words.
*
* @param words - Array of room names to try
*/
setWordlist(words) {
this.wordlist = words
.map((w) => w.trim().toLowerCase())
.filter(isValidRoomName);
}
/**
* Abort the current cracking operation.
* The crack() method will return with aborted: true.
*/
abort() {
this.abortFlag = true;
}
/**
* Check if WebGPU is available in the current environment.
*/
isGpuAvailable() {
return isWebGpuSupported();
}
/**
* Decode a packet and extract the information needed for cracking.
*
* @param packetHex - The packet data as a hex string
* @returns Decoded packet info or null if not a GroupText packet
*/
async decodePacket(packetHex) {
const cleanHex = packetHex.trim().replace(/\s+/g, '').replace(/^0x/i, '');
if (!cleanHex || !/^[0-9a-fA-F]+$/.test(cleanHex)) {
return null;
}
try {
const decoded = await MeshCorePacketDecoder.decodeWithVerification(cleanHex, {});
const payload = decoded.payload?.decoded;
if (!payload?.channelHash || !payload?.ciphertext || !payload?.cipherMac) {
return null;
}
return {
channelHash: payload.channelHash,
ciphertext: payload.ciphertext,
cipherMac: payload.cipherMac,
isGroupText: true,
};
}
catch {
return null;
}
}
/**
* Crack a GroupText packet to find the room name and decrypt the message.
*
* @param packetHex - The packet data as a hex string
* @param options - Cracking options
* @param onProgress - Optional callback for progress updates
* @returns The cracking result
*/
async crack(packetHex, options, onProgress) {
this.abortFlag = false;
this.useTimestampFilter = options?.useTimestampFilter ?? true;
this.useUtf8Filter = options?.useUtf8Filter ?? true;
const maxLength = options?.maxLength ?? 8;
// Decode packet
const decoded = await this.decodePacket(packetHex);
if (!decoded) {
return { found: false, error: 'Invalid packet or not a GroupText packet' };
}
const { channelHash, ciphertext, cipherMac } = decoded;
const targetHashByte = parseInt(channelHash, 16);
// Initialize GPU if not already done
if (!this.gpuInstance) {
this.gpuInstance = new GpuBruteForce();
const gpuOk = await this.gpuInstance.init();
if (!gpuOk) {
return { found: false, error: 'WebGPU not available' };
}
}
const startTime = performance.now();
let totalChecked = 0;
let lastProgressUpdate = performance.now();
// Determine starting position
let startFromLength = 1;
let startFromOffset = 0;
if (options?.startFrom) {
const pos = roomNameToIndex(options.startFrom);
if (pos) {
startFromLength = pos.length;
startFromOffset = pos.index + 1; // Start after the given position
if (startFromOffset >= countNamesForLength(startFromLength)) {
startFromLength++;
startFromOffset = 0;
}
}
}
// Calculate total candidates for progress
let totalCandidates = 0;
for (let l = startFromLength; l <= maxLength; l++) {
totalCandidates += countNamesForLength(l);
}
totalCandidates -= startFromOffset;
// Helper to report progress
const reportProgress = (phase, currentLength, currentPosition) => {
if (!onProgress)
return;
const now = performance.now();
const elapsed = (now - startTime) / 1000;
const rate = elapsed > 0 ? Math.round(totalChecked / elapsed) : 0;
const remaining = totalCandidates - totalChecked;
const eta = rate > 0 ? remaining / rate : 0;
onProgress({
checked: totalChecked,
total: totalCandidates,
percent: totalCandidates > 0 ? Math.min(100, (totalChecked / totalCandidates) * 100) : 0,
rateKeysPerSec: rate,
etaSeconds: eta,
elapsedSeconds: elapsed,
currentLength,
currentPosition,
phase,
});
};
// Helper to verify MAC and filters
const verifyMacAndFilters = (key) => {
if (!verifyMac(ciphertext, cipherMac, key)) {
return { valid: false };
}
const result = ChannelCrypto.decryptGroupTextMessage(ciphertext, cipherMac, key);
if (!result.success || !result.data) {
return { valid: false };
}
if (this.useTimestampFilter && !isTimestampValid(result.data.timestamp)) {
return { valid: false };
}
if (this.useUtf8Filter && !isValidUtf8(result.data.message)) {
return { valid: false };
}
return { valid: true, message: result.data.message };
};
// Phase 1: Try public key
if (startFromLength === 1 && startFromOffset === 0) {
reportProgress('public-key', 0, PUBLIC_ROOM_NAME);
const publicChannelHash = getChannelHash(PUBLIC_KEY);
if (channelHash === publicChannelHash) {
const result = verifyMacAndFilters(PUBLIC_KEY);
if (result.valid) {
return {
found: true,
roomName: PUBLIC_ROOM_NAME,
key: PUBLIC_KEY,
decryptedMessage: result.message,
};
}
}
}
// Phase 2: Dictionary attack
if (this.wordlist.length > 0 && startFromLength === 1 && startFromOffset === 0) {
for (let i = 0; i < this.wordlist.length; i++) {
if (this.abortFlag) {
return {
found: false,
aborted: true,
resumeFrom: this.wordlist[i],
};
}
const word = this.wordlist[i];
const key = deriveKeyFromRoomName('#' + word);
const wordChannelHash = getChannelHash(key);
if (parseInt(wordChannelHash, 16) === targetHashByte) {
const result = verifyMacAndFilters(key);
if (result.valid) {
return {
found: true,
roomName: word,
key,
decryptedMessage: result.message,
};
}
}
// Progress update
const now = performance.now();
if (now - lastProgressUpdate >= 200) {
reportProgress('wordlist', word.length, word);
lastProgressUpdate = now;
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
// Phase 3: GPU brute force
const INITIAL_BATCH_SIZE = 32768;
const TARGET_DISPATCH_MS = 1000;
let currentBatchSize = INITIAL_BATCH_SIZE;
let batchSizeTuned = false;
for (let length = startFromLength; length <= maxLength; length++) {
if (this.abortFlag) {
const resumePos = indexToRoomName(length, 0);
return {
found: false,
aborted: true,
resumeFrom: resumePos || undefined,
};
}
const totalForLength = countNamesForLength(length);
let offset = length === startFromLength ? startFromOffset : 0;
while (offset < totalForLength) {
if (this.abortFlag) {
const resumePos = indexToRoomName(length, offset);
return {
found: false,
aborted: true,
resumeFrom: resumePos || undefined,
};
}
const batchSize = Math.min(currentBatchSize, totalForLength - offset);
const dispatchStart = performance.now();
const matches = await this.gpuInstance.runBatch(targetHashByte, length, offset, batchSize, ciphertext, cipherMac);
const dispatchTime = performance.now() - dispatchStart;
totalChecked += batchSize;
// Auto-tune batch size
if (!batchSizeTuned && batchSize >= INITIAL_BATCH_SIZE && dispatchTime > 0) {
const scaleFactor = TARGET_DISPATCH_MS / dispatchTime;
const optimalBatchSize = Math.round(batchSize * scaleFactor);
const rounded = Math.pow(2, Math.round(Math.log2(Math.max(INITIAL_BATCH_SIZE, optimalBatchSize))));
currentBatchSize = Math.max(INITIAL_BATCH_SIZE, rounded);
batchSizeTuned = true;
}
// Check matches
for (const matchIdx of matches) {
const roomName = indexToRoomName(length, matchIdx);
if (!roomName)
continue;
const key = deriveKeyFromRoomName('#' + roomName);
const result = verifyMacAndFilters(key);
if (result.valid) {
return {
found: true,
roomName,
key,
decryptedMessage: result.message,
};
}
}
offset += batchSize;
// Progress update
const now = performance.now();
if (now - lastProgressUpdate >= 200) {
const currentPos = indexToRoomName(length, Math.min(offset, totalForLength - 1)) || '';
reportProgress('bruteforce', length, currentPos);
lastProgressUpdate = now;
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
// Not found
const lastPos = indexToRoomName(maxLength, countNamesForLength(maxLength) - 1);
return {
found: false,
resumeFrom: lastPos || undefined,
};
}
/**
* Clean up GPU resources.
* Call this when you're done using the cracker.
*/
destroy() {
if (this.gpuInstance) {
this.gpuInstance.destroy();
this.gpuInstance = null;
}
}
}
//# sourceMappingURL=cracker.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
export interface GpuBruteForceResult {
found: boolean;
roomName?: string;
key?: string;
candidateIndices?: number[];
}
export declare class GpuBruteForce {
private device;
private pipeline;
private bindGroupLayout;
private paramsBuffer;
private matchCountBuffer;
private matchIndicesBuffer;
private ciphertextBuffer;
private ciphertextBufferSize;
private matchCountReadBuffers;
private matchIndicesReadBuffers;
private currentReadBufferIndex;
private bindGroup;
private bindGroupDirty;
private static readonly ZERO_DATA;
private shaderCode;
init(): Promise<boolean>;
isAvailable(): boolean;
indexToRoomName(idx: number, length: number): string | null;
countNamesForLength(len: number): number;
runBatch(targetChannelHash: number, nameLength: number, batchOffset: number, batchSize: number, ciphertextHex?: string, targetMacHex?: string): Promise<number[]>;
destroy(): void;
}
/**
* Check if WebGPU is supported in the current browser.
*/
export declare function isWebGpuSupported(): boolean;
//# sourceMappingURL=gpu-bruteforce.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"gpu-bruteforce.d.ts","sourceRoot":"","sources":["../src/gpu-bruteforce.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,eAAe,CAAmC;IAG1D,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,kBAAkB,CAA0B;IACpD,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,oBAAoB,CAAa;IAGzC,OAAO,CAAC,qBAAqB,CAAsD;IACnF,OAAO,CAAC,uBAAuB,CAAsD;IACrF,OAAO,CAAC,sBAAsB,CAAa;IAG3C,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,cAAc,CAAiB;IAGvC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAGzD,OAAO,CAAC,UAAU,CAkYlB;IAEM,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IA8E9B,WAAW,IAAI,OAAO;IAKtB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAK3D,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAIlC,QAAQ,CACZ,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE,MAAM,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,EAAE,CAAC;IAkJpB,OAAO,IAAI,IAAI;CA+BhB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C"}

View File

@@ -0,0 +1,645 @@
// WebGPU-accelerated brute force key cracking for MeshCore packets
import { indexToRoomName, countNamesForLength } from './core';
export class GpuBruteForce {
constructor() {
this.device = null;
this.pipeline = null;
this.bindGroupLayout = null;
// Persistent buffers for reuse between batches
this.paramsBuffer = null;
this.matchCountBuffer = null;
this.matchIndicesBuffer = null;
this.ciphertextBuffer = null;
this.ciphertextBufferSize = 0;
// Double-buffered staging buffers for overlapping GPU/CPU work
this.matchCountReadBuffers = [null, null];
this.matchIndicesReadBuffers = [null, null];
this.currentReadBufferIndex = 0;
// Cached bind group (recreated only when ciphertext buffer changes)
this.bindGroup = null;
this.bindGroupDirty = true;
// Shader for SHA256 computation
this.shaderCode = `
// SHA256 round constants
const K: array<u32, 64> = array<u32, 64>(
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u
);
// Character lookup table (a-z = 0-25, 0-9 = 26-35, dash = 36)
const CHARS: array<u32, 37> = array<u32, 37>(
0x61u, 0x62u, 0x63u, 0x64u, 0x65u, 0x66u, 0x67u, 0x68u, 0x69u, 0x6au, // a-j
0x6bu, 0x6cu, 0x6du, 0x6eu, 0x6fu, 0x70u, 0x71u, 0x72u, 0x73u, 0x74u, // k-t
0x75u, 0x76u, 0x77u, 0x78u, 0x79u, 0x7au, // u-z
0x30u, 0x31u, 0x32u, 0x33u, 0x34u, 0x35u, 0x36u, 0x37u, 0x38u, 0x39u, // 0-9
0x2du // dash
);
struct Params {
target_channel_hash: u32,
batch_offset: u32,
name_length: u32,
batch_size: u32,
target_mac: u32, // First 2 bytes of target MAC (in high 16 bits)
ciphertext_words: u32, // Number of 32-bit words in ciphertext
ciphertext_len_bits: u32, // Length of ciphertext in bits
verify_mac: u32, // 1 to verify MAC, 0 to skip
}
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var<storage, read_write> match_count: atomic<u32>;
@group(0) @binding(2) var<storage, read_write> match_indices: array<u32>;
@group(0) @binding(3) var<storage, read> ciphertext: array<u32>; // Ciphertext data
fn rotr(x: u32, n: u32) -> u32 {
return (x >> n) | (x << (32u - n));
}
fn ch(x: u32, y: u32, z: u32) -> u32 {
return (x & y) ^ (~x & z);
}
fn maj(x: u32, y: u32, z: u32) -> u32 {
return (x & y) ^ (x & z) ^ (y & z);
}
fn sigma0(x: u32) -> u32 {
return rotr(x, 2u) ^ rotr(x, 13u) ^ rotr(x, 22u);
}
fn sigma1(x: u32) -> u32 {
return rotr(x, 6u) ^ rotr(x, 11u) ^ rotr(x, 25u);
}
fn gamma0(x: u32) -> u32 {
return rotr(x, 7u) ^ rotr(x, 18u) ^ (x >> 3u);
}
fn gamma1(x: u32) -> u32 {
return rotr(x, 17u) ^ rotr(x, 19u) ^ (x >> 10u);
}
// Convert index to room name bytes, returns the hash as a u32 for the first byte check
fn index_to_room_name(idx: u32, length: u32, msg: ptr<function, array<u32, 16>>) -> bool {
// Message starts with '#' (0x23)
var byte_pos = 0u;
var word_idx = 0u;
var current_word = 0x23000000u; // '#' in big-endian position 0
byte_pos = 1u;
var remaining = idx;
var prev_was_dash = false;
// Generate room name from index
for (var i = 0u; i < length; i++) {
let char_count = select(37u, 36u, i == 0u || i == length - 1u); // no dash at start/end
var char_idx = remaining % char_count;
remaining = remaining / char_count;
// Check for consecutive dashes (invalid)
let is_dash = char_idx == 36u && i > 0u && i < length - 1u;
if (is_dash && prev_was_dash) {
return false; // Invalid: consecutive dashes
}
prev_was_dash = is_dash;
// Map char index to actual character
let c = CHARS[char_idx];
// Pack byte into current word (big-endian)
let shift = (3u - byte_pos % 4u) * 8u;
if (byte_pos % 4u == 0u && byte_pos > 0u) {
(*msg)[word_idx] = current_word;
word_idx = word_idx + 1u;
current_word = 0u;
}
current_word = current_word | (c << shift);
byte_pos = byte_pos + 1u;
}
// Add padding: 0x80 followed by zeros, then length in bits
let msg_len_bits = (length + 1u) * 8u; // +1 for '#'
// Add 0x80 padding byte
let shift = (3u - byte_pos % 4u) * 8u;
if (byte_pos % 4u == 0u) {
(*msg)[word_idx] = current_word;
word_idx = word_idx + 1u;
current_word = 0x80000000u;
} else {
current_word = current_word | (0x80u << shift);
}
byte_pos = byte_pos + 1u;
// Store current word
if (byte_pos % 4u == 0u || word_idx < 14u) {
(*msg)[word_idx] = current_word;
word_idx = word_idx + 1u;
}
// Zero-fill until word 14
for (var i = word_idx; i < 14u; i++) {
(*msg)[i] = 0u;
}
// Length in bits (64-bit, but we only use lower 32 bits for short messages)
(*msg)[14u] = 0u;
(*msg)[15u] = msg_len_bits;
return true;
}
fn sha256_block(msg: ptr<function, array<u32, 16>>) -> array<u32, 8> {
// Initialize hash values
var h: array<u32, 8> = array<u32, 8>(
0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au,
0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u
);
// Message schedule
var w: array<u32, 64>;
for (var i = 0u; i < 16u; i++) {
w[i] = (*msg)[i];
}
for (var i = 16u; i < 64u; i++) {
w[i] = gamma1(w[i-2u]) + w[i-7u] + gamma0(w[i-15u]) + w[i-16u];
}
// Compression
var a = h[0]; var b = h[1]; var c = h[2]; var d = h[3];
var e = h[4]; var f = h[5]; var g = h[6]; var hh = h[7];
for (var i = 0u; i < 64u; i++) {
let t1 = hh + sigma1(e) + ch(e, f, g) + K[i] + w[i];
let t2 = sigma0(a) + maj(a, b, c);
hh = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
h[0] = h[0] + a; h[1] = h[1] + b; h[2] = h[2] + c; h[3] = h[3] + d;
h[4] = h[4] + e; h[5] = h[5] + f; h[6] = h[6] + g; h[7] = h[7] + hh;
return h;
}
// Compute SHA256 of the key (16 bytes) to get channel hash
fn sha256_key(key: array<u32, 4>) -> u32 {
var msg: array<u32, 16>;
// Key bytes (16 bytes = 4 words)
msg[0] = key[0];
msg[1] = key[1];
msg[2] = key[2];
msg[3] = key[3];
// Padding: 0x80 followed by zeros
msg[4] = 0x80000000u;
for (var i = 5u; i < 14u; i++) {
msg[i] = 0u;
}
// Length: 128 bits
msg[14] = 0u;
msg[15] = 128u;
let hash = sha256_block(&msg);
// Return first byte of hash (big-endian)
return hash[0] >> 24u;
}
// HMAC-SHA256 for MAC verification
// Key is 16 bytes (4 words), padded to 32 bytes with zeros for MeshCore
// Returns first 2 bytes of HMAC (as u32 in high 16 bits)
fn hmac_sha256_mac(key: array<u32, 4>, ciphertext_len: u32) -> u32 {
// HMAC: H((K' ^ opad) || H((K' ^ ipad) || message))
// K' is 64 bytes (32 bytes key + 32 bytes zero padding for MeshCore, then padded to 64)
// ipad = 0x36 repeated, opad = 0x5c repeated
// Build padded key (64 bytes = 16 words)
// MeshCore uses 32-byte secret: 16-byte key + 16 zero bytes
var k_pad: array<u32, 16>;
k_pad[0] = key[0];
k_pad[1] = key[1];
k_pad[2] = key[2];
k_pad[3] = key[3];
for (var i = 4u; i < 16u; i++) {
k_pad[i] = 0u;
}
// Inner hash: SHA256((K' ^ ipad) || message)
// First block: K' ^ ipad (64 bytes)
var inner_block: array<u32, 16>;
for (var i = 0u; i < 16u; i++) {
inner_block[i] = k_pad[i] ^ 0x36363636u;
}
// Initialize hash state with first block
var h: array<u32, 8> = sha256_block(&inner_block);
// Process ciphertext blocks (continuing from h state)
let ciphertext_words = params.ciphertext_words;
var word_idx = 0u;
// Process full 64-byte blocks of ciphertext
while (word_idx + 16u <= ciphertext_words) {
var block: array<u32, 16>;
for (var i = 0u; i < 16u; i++) {
block[i] = ciphertext[word_idx + i];
}
h = sha256_block_continue(&block, h);
word_idx = word_idx + 16u;
}
// Final block with remaining ciphertext + padding
var final_block: array<u32, 16>;
var remaining = ciphertext_words - word_idx;
for (var i = 0u; i < 16u; i++) {
if (i < remaining) {
final_block[i] = ciphertext[word_idx + i];
} else if (i == remaining) {
// Add 0x80 padding
final_block[i] = 0x80000000u;
} else {
final_block[i] = 0u;
}
}
// Add length (64 bytes of ipad + ciphertext length)
let total_bits = 512u + params.ciphertext_len_bits;
if (remaining < 14u) {
final_block[14] = 0u;
final_block[15] = total_bits;
h = sha256_block_continue(&final_block, h);
} else {
// Need extra block for length
h = sha256_block_continue(&final_block, h);
var len_block: array<u32, 16>;
for (var i = 0u; i < 14u; i++) {
len_block[i] = 0u;
}
len_block[14] = 0u;
len_block[15] = total_bits;
h = sha256_block_continue(&len_block, h);
}
let inner_hash = h;
// Outer hash: SHA256((K' ^ opad) || inner_hash)
var outer_block: array<u32, 16>;
for (var i = 0u; i < 16u; i++) {
outer_block[i] = k_pad[i] ^ 0x5c5c5c5cu;
}
h = sha256_block(&outer_block);
// Second block: inner_hash (32 bytes) + padding
var hash_block: array<u32, 16>;
for (var i = 0u; i < 8u; i++) {
hash_block[i] = inner_hash[i];
}
hash_block[8] = 0x80000000u;
for (var i = 9u; i < 14u; i++) {
hash_block[i] = 0u;
}
hash_block[14] = 0u;
hash_block[15] = 512u + 256u; // 64 bytes opad + 32 bytes inner hash
h = sha256_block_continue(&hash_block, h);
// Return first 2 bytes (high 16 bits of first word)
return h[0] & 0xFFFF0000u;
}
// SHA256 block computation continuing from existing state
fn sha256_block_continue(msg: ptr<function, array<u32, 16>>, h_in: array<u32, 8>) -> array<u32, 8> {
var h = h_in;
// Message schedule
var w: array<u32, 64>;
for (var i = 0u; i < 16u; i++) {
w[i] = (*msg)[i];
}
for (var i = 16u; i < 64u; i++) {
w[i] = gamma1(w[i-2u]) + w[i-7u] + gamma0(w[i-15u]) + w[i-16u];
}
// Compression
var a = h[0]; var b = h[1]; var c = h[2]; var d = h[3];
var e = h[4]; var f = h[5]; var g = h[6]; var hh = h[7];
for (var i = 0u; i < 64u; i++) {
let t1 = hh + sigma1(e) + ch(e, f, g) + K[i] + w[i];
let t2 = sigma0(a) + maj(a, b, c);
hh = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
h[0] = h[0] + a; h[1] = h[1] + b; h[2] = h[2] + c; h[3] = h[3] + d;
h[4] = h[4] + e; h[5] = h[5] + f; h[6] = h[6] + g; h[7] = h[7] + hh;
return h;
}
// Process a single candidate and record match if found
fn process_candidate(name_idx: u32) {
// Generate message for this room name
var msg: array<u32, 16>;
let valid = index_to_room_name(name_idx, params.name_length, &msg);
if (!valid) {
return;
}
// Compute SHA256("#roomname") - this gives us the key
let key_hash = sha256_block(&msg);
// Take first 16 bytes (4 words) as the key
var key: array<u32, 4>;
key[0] = key_hash[0];
key[1] = key_hash[1];
key[2] = key_hash[2];
key[3] = key_hash[3];
// Compute SHA256(key) to get channel hash
let channel_hash = sha256_key(key);
// Check if channel hash matches target
if (channel_hash != params.target_channel_hash) {
return;
}
// Channel hash matches - verify MAC if enabled
if (params.verify_mac == 1u) {
let computed_mac = hmac_sha256_mac(key, params.ciphertext_len_bits);
if (computed_mac != params.target_mac) {
return;
}
}
// Found a match - record the index
let match_idx = atomicAdd(&match_count, 1u);
if (match_idx < 1024u) { // Limit stored matches
match_indices[match_idx] = name_idx;
}
}
// Each thread processes 16 candidates to amortize thread overhead
const CANDIDATES_PER_THREAD: u32 = 16u;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let base_idx = global_id.x * CANDIDATES_PER_THREAD;
for (var i = 0u; i < CANDIDATES_PER_THREAD; i++) {
let idx = base_idx + i;
if (idx >= params.batch_size) {
return;
}
let name_idx = params.batch_offset + idx;
process_candidate(name_idx);
}
}
`;
}
async init() {
if (!navigator.gpu) {
console.warn('WebGPU not supported');
return false;
}
try {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
console.warn('No GPU adapter found');
return false;
}
this.device = await adapter.requestDevice();
// Create bind group layout
this.bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
],
});
// Create persistent buffers
this.paramsBuffer = this.device.createBuffer({
size: 32, // 8 u32s
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.matchCountBuffer = this.device.createBuffer({
size: 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
this.matchIndicesBuffer = this.device.createBuffer({
size: 1024 * 4, // Max 1024 matches per batch
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
// Double-buffered staging buffers
for (let i = 0; i < 2; i++) {
this.matchCountReadBuffers[i] = this.device.createBuffer({
size: 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
this.matchIndicesReadBuffers[i] = this.device.createBuffer({
size: 1024 * 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
}
// Create pipeline
const shaderModule = this.device.createShaderModule({
code: this.shaderCode,
});
const pipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [this.bindGroupLayout],
});
this.pipeline = this.device.createComputePipeline({
layout: pipelineLayout,
compute: {
module: shaderModule,
entryPoint: 'main',
},
});
return true;
}
catch (e) {
console.error('WebGPU initialization failed:', e);
return false;
}
}
isAvailable() {
return this.device !== null && this.pipeline !== null;
}
// Convert room name index to actual room name string (delegates to core)
indexToRoomName(idx, length) {
return indexToRoomName(length, idx);
}
// Count valid names for a given length (delegates to core)
countNamesForLength(len) {
return countNamesForLength(len);
}
async runBatch(targetChannelHash, nameLength, batchOffset, batchSize, ciphertextHex, targetMacHex) {
if (!this.device ||
!this.pipeline ||
!this.bindGroupLayout ||
!this.paramsBuffer ||
!this.matchCountBuffer ||
!this.matchIndicesBuffer ||
!this.matchCountReadBuffers[0] ||
!this.matchCountReadBuffers[1] ||
!this.matchIndicesReadBuffers[0] ||
!this.matchIndicesReadBuffers[1]) {
throw new Error('GPU not initialized');
}
// Swap to alternate staging buffer set (double-buffering)
const readBufferIdx = this.currentReadBufferIndex;
this.currentReadBufferIndex = 1 - this.currentReadBufferIndex;
const matchCountReadBuffer = this.matchCountReadBuffers[readBufferIdx];
const matchIndicesReadBuffer = this.matchIndicesReadBuffers[readBufferIdx];
// Parse ciphertext if provided
const verifyMac = ciphertextHex && targetMacHex ? 1 : 0;
let ciphertextWords;
let ciphertextLenBits = 0;
let targetMac = 0;
if (verifyMac) {
// Convert hex to bytes then to big-endian u32 words
const ciphertextBytes = new Uint8Array(ciphertextHex.length / 2);
for (let i = 0; i < ciphertextBytes.length; i++) {
ciphertextBytes[i] = parseInt(ciphertextHex.substr(i * 2, 2), 16);
}
ciphertextLenBits = ciphertextBytes.length * 8;
// Pad to 4-byte boundary and convert to big-endian u32
const paddedLen = Math.ceil(ciphertextBytes.length / 4) * 4;
const padded = new Uint8Array(paddedLen);
padded.set(ciphertextBytes);
ciphertextWords = new Uint32Array(paddedLen / 4);
for (let i = 0; i < ciphertextWords.length; i++) {
ciphertextWords[i] =
(padded[i * 4] << 24) |
(padded[i * 4 + 1] << 16) |
(padded[i * 4 + 2] << 8) |
padded[i * 4 + 3];
}
// Parse target MAC (2 bytes in high 16 bits)
const macByte0 = parseInt(targetMacHex.substr(0, 2), 16);
const macByte1 = parseInt(targetMacHex.substr(2, 2), 16);
targetMac = (macByte0 << 24) | (macByte1 << 16);
}
else {
ciphertextWords = new Uint32Array([0]); // Dummy
}
// Resize ciphertext buffer if needed (marks bind group as dirty)
const requiredCiphertextSize = Math.max(ciphertextWords.length * 4, 4);
if (!this.ciphertextBuffer || this.ciphertextBufferSize < requiredCiphertextSize) {
if (this.ciphertextBuffer) {
this.ciphertextBuffer.destroy();
}
this.ciphertextBuffer = this.device.createBuffer({
size: requiredCiphertextSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
this.ciphertextBufferSize = requiredCiphertextSize;
this.bindGroupDirty = true;
}
// Write params
const paramsData = new Uint32Array([
targetChannelHash,
batchOffset,
nameLength,
batchSize,
targetMac,
ciphertextWords.length,
ciphertextLenBits,
verifyMac,
]);
this.device.queue.writeBuffer(this.paramsBuffer, 0, paramsData);
// Write ciphertext
this.device.queue.writeBuffer(this.ciphertextBuffer, 0, ciphertextWords);
// Reset match count (reuse static zero buffer)
this.device.queue.writeBuffer(this.matchCountBuffer, 0, GpuBruteForce.ZERO_DATA);
// Recreate bind group only if needed
if (this.bindGroupDirty || !this.bindGroup) {
this.bindGroup = this.device.createBindGroup({
layout: this.bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this.paramsBuffer } },
{ binding: 1, resource: { buffer: this.matchCountBuffer } },
{ binding: 2, resource: { buffer: this.matchIndicesBuffer } },
{ binding: 3, resource: { buffer: this.ciphertextBuffer } },
],
});
this.bindGroupDirty = false;
}
// Create command encoder
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.pipeline);
passEncoder.setBindGroup(0, this.bindGroup);
// Each workgroup has 256 threads, each processing 16 candidates
const CANDIDATES_PER_THREAD = 16;
passEncoder.dispatchWorkgroups(Math.ceil(batchSize / (256 * CANDIDATES_PER_THREAD)));
passEncoder.end();
// Copy results to current staging buffers
commandEncoder.copyBufferToBuffer(this.matchCountBuffer, 0, matchCountReadBuffer, 0, 4);
commandEncoder.copyBufferToBuffer(this.matchIndicesBuffer, 0, matchIndicesReadBuffer, 0, 1024 * 4);
// Submit
this.device.queue.submit([commandEncoder.finish()]);
// Read results from current staging buffers
await matchCountReadBuffer.mapAsync(GPUMapMode.READ);
const matchCount = new Uint32Array(matchCountReadBuffer.getMappedRange())[0];
matchCountReadBuffer.unmap();
const matches = [];
if (matchCount > 0) {
await matchIndicesReadBuffer.mapAsync(GPUMapMode.READ);
const indices = new Uint32Array(matchIndicesReadBuffer.getMappedRange());
for (let i = 0; i < Math.min(matchCount, 1024); i++) {
matches.push(indices[i]);
}
matchIndicesReadBuffer.unmap();
}
return matches;
}
destroy() {
// Clean up persistent buffers
this.paramsBuffer?.destroy();
this.matchCountBuffer?.destroy();
this.matchIndicesBuffer?.destroy();
this.ciphertextBuffer?.destroy();
// Clean up double-buffered staging buffers
this.matchCountReadBuffers[0]?.destroy();
this.matchCountReadBuffers[1]?.destroy();
this.matchIndicesReadBuffers[0]?.destroy();
this.matchIndicesReadBuffers[1]?.destroy();
this.paramsBuffer = null;
this.matchCountBuffer = null;
this.matchIndicesBuffer = null;
this.ciphertextBuffer = null;
this.ciphertextBufferSize = 0;
this.matchCountReadBuffers = [null, null];
this.matchIndicesReadBuffers = [null, null];
this.currentReadBufferIndex = 0;
this.bindGroup = null;
this.bindGroupDirty = true;
if (this.device) {
this.device.destroy();
this.device = null;
}
this.pipeline = null;
this.bindGroupLayout = null;
}
}
// Reusable zero buffer for resetting match count
GpuBruteForce.ZERO_DATA = new Uint32Array([0]);
/**
* Check if WebGPU is supported in the current browser.
*/
export function isWebGpuSupported() {
return typeof navigator !== 'undefined' && 'gpu' in navigator;
}
//# sourceMappingURL=gpu-bruteforce.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
/**
* MeshCore Cracker - Standalone library for cracking MeshCore GroupText packets
*
* @example
* ```typescript
* import { GroupTextCracker } from 'meshcore-cracker';
*
* const cracker = new GroupTextCracker();
*
* // Optional: load wordlist for dictionary attack
* await cracker.loadWordlist('/words_alpha.txt');
*
* const result = await cracker.crack(packetHex, {
* maxLength: 6,
* useTimestampFilter: true,
* useUtf8Filter: true,
* }, (progress) => {
* console.log(`${progress.percent.toFixed(1)}% - ETA: ${progress.etaSeconds}s`);
* });
*
* if (result.found) {
* console.log(`Room: #${result.roomName}`);
* console.log(`Message: ${result.decryptedMessage}`);
* }
*
* cracker.destroy();
* ```
*/
export { GroupTextCracker } from './cracker';
export type { CrackOptions, CrackResult, ProgressReport, ProgressCallback, DecodedPacket, } from './types';
export { deriveKeyFromRoomName, getChannelHash, verifyMac, isTimestampValid, isValidUtf8, indexToRoomName, roomNameToIndex, countNamesForLength, PUBLIC_ROOM_NAME, PUBLIC_KEY, } from './core';
export { isWebGpuSupported } from './gpu-bruteforce';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG7C,YAAY,EACV,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,qBAAqB,EACrB,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,GACX,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}

11476
frontend/lib/meshcore-cracker/dist/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,87 @@
/**
* Options for configuring the cracking process.
*/
export interface CrackOptions {
/**
* Maximum room name length to search (default: 8).
* Longer names exponentially increase search time.
*/
maxLength?: number;
/**
* Filter results by timestamp validity (default: true).
* When enabled, rejects results where the decrypted timestamp
* is more than 30 days old.
*/
useTimestampFilter?: boolean;
/**
* Filter results by UTF-8 validity (default: true).
* When enabled, rejects results containing invalid UTF-8 sequences.
*/
useUtf8Filter?: boolean;
/**
* Resume cracking from a specific room name position.
* Useful for resuming interrupted searches.
*/
startFrom?: string;
}
/**
* Progress information reported during cracking.
*/
export interface ProgressReport {
/** Total candidates checked so far */
checked: number;
/** Total candidates to check */
total: number;
/** Progress percentage (0-100) */
percent: number;
/** Current cracking rate in keys/second */
rateKeysPerSec: number;
/** Estimated time remaining in seconds */
etaSeconds: number;
/** Time elapsed since start in seconds */
elapsedSeconds: number;
/** Current room name length being tested */
currentLength: number;
/** Current room name position being tested */
currentPosition: string;
/** Current phase of cracking */
phase: 'public-key' | 'wordlist' | 'bruteforce';
}
/**
* Callback function for progress updates.
* Called approximately 5 times per second during cracking.
*/
export type ProgressCallback = (report: ProgressReport) => void;
/**
* Result of a cracking operation.
*/
export interface CrackResult {
/** Whether a matching room name was found */
found: boolean;
/** The room name (without '#' prefix) if found */
roomName?: string;
/** The derived encryption key (hex) if found */
key?: string;
/** The decrypted message content if found */
decryptedMessage?: string;
/** Whether the operation was aborted */
aborted?: boolean;
/** Position to resume from if aborted or failed */
resumeFrom?: string;
/** Error message if an error occurred */
error?: string;
}
/**
* Decoded packet information extracted from a MeshCore GroupText packet.
*/
export interface DecodedPacket {
/** Channel hash (1 byte, hex) */
channelHash: string;
/** Encrypted ciphertext (hex) */
ciphertext: string;
/** MAC for verification (2 bytes, hex) */
cipherMac: string;
/** Whether this is a GroupText packet */
isGroupText: boolean;
}
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAEhB,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IAEd,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAEhB,2CAA2C;IAC3C,cAAc,EAAE,MAAM,CAAC;IAEvB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IAEnB,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC;IAEvB,4CAA4C;IAC5C,aAAa,EAAE,MAAM,CAAC;IAEtB,8CAA8C;IAC9C,eAAe,EAAE,MAAM,CAAC;IAExB,gCAAgC;IAChC,KAAK,EAAE,YAAY,GAAG,UAAU,GAAG,YAAY,CAAC;CACjD;AAED;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,KAAK,EAAE,OAAO,CAAC;IAEf,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,6CAA6C;IAC7C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,wCAAwC;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IAEpB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;IAEnB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAElB,yCAAyC;IACzC,WAAW,EAAE,OAAO,CAAC;CACtB"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}

1
frontend/lib/meshcore-cracker/node_modules/.bin/esbuild generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../esbuild/bin/esbuild

View File

@@ -0,0 +1,3 @@
# esbuild
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.

Binary file not shown.

View File

@@ -0,0 +1,20 @@
{
"name": "@esbuild/linux-x64",
"version": "0.24.2",
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=18"
},
"os": [
"linux"
],
"cpu": [
"x64"
]
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Evan Wallace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,3 @@
# esbuild
This is a JavaScript bundler and minifier. See https://github.com/evanw/esbuild and the [JavaScript API documentation](https://esbuild.github.io/api/) for details.

Binary file not shown.

View File

@@ -0,0 +1,287 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// lib/npm/node-platform.ts
var fs = require("fs");
var os = require("os");
var path = require("path");
var ESBUILD_BINARY_PATH = process.env.ESBUILD_BINARY_PATH || ESBUILD_BINARY_PATH;
var isValidBinaryPath = (x) => !!x && x !== "/usr/bin/esbuild";
var knownWindowsPackages = {
"win32 arm64 LE": "@esbuild/win32-arm64",
"win32 ia32 LE": "@esbuild/win32-ia32",
"win32 x64 LE": "@esbuild/win32-x64"
};
var knownUnixlikePackages = {
"aix ppc64 BE": "@esbuild/aix-ppc64",
"android arm64 LE": "@esbuild/android-arm64",
"darwin arm64 LE": "@esbuild/darwin-arm64",
"darwin x64 LE": "@esbuild/darwin-x64",
"freebsd arm64 LE": "@esbuild/freebsd-arm64",
"freebsd x64 LE": "@esbuild/freebsd-x64",
"linux arm LE": "@esbuild/linux-arm",
"linux arm64 LE": "@esbuild/linux-arm64",
"linux ia32 LE": "@esbuild/linux-ia32",
"linux mips64el LE": "@esbuild/linux-mips64el",
"linux ppc64 LE": "@esbuild/linux-ppc64",
"linux riscv64 LE": "@esbuild/linux-riscv64",
"linux s390x BE": "@esbuild/linux-s390x",
"linux x64 LE": "@esbuild/linux-x64",
"linux loong64 LE": "@esbuild/linux-loong64",
"netbsd arm64 LE": "@esbuild/netbsd-arm64",
"netbsd x64 LE": "@esbuild/netbsd-x64",
"openbsd arm64 LE": "@esbuild/openbsd-arm64",
"openbsd x64 LE": "@esbuild/openbsd-x64",
"sunos x64 LE": "@esbuild/sunos-x64"
};
var knownWebAssemblyFallbackPackages = {
"android arm LE": "@esbuild/android-arm",
"android x64 LE": "@esbuild/android-x64"
};
function pkgAndSubpathForCurrentPlatform() {
let pkg;
let subpath;
let isWASM = false;
let platformKey = `${process.platform} ${os.arch()} ${os.endianness()}`;
if (platformKey in knownWindowsPackages) {
pkg = knownWindowsPackages[platformKey];
subpath = "esbuild.exe";
} else if (platformKey in knownUnixlikePackages) {
pkg = knownUnixlikePackages[platformKey];
subpath = "bin/esbuild";
} else if (platformKey in knownWebAssemblyFallbackPackages) {
pkg = knownWebAssemblyFallbackPackages[platformKey];
subpath = "bin/esbuild";
isWASM = true;
} else {
throw new Error(`Unsupported platform: ${platformKey}`);
}
return { pkg, subpath, isWASM };
}
function downloadedBinPath(pkg, subpath) {
const esbuildLibDir = path.dirname(require.resolve("esbuild"));
return path.join(esbuildLibDir, `downloaded-${pkg.replace("/", "-")}-${path.basename(subpath)}`);
}
// lib/npm/node-install.ts
var fs2 = require("fs");
var os2 = require("os");
var path2 = require("path");
var zlib = require("zlib");
var https = require("https");
var child_process = require("child_process");
var versionFromPackageJSON = require(path2.join(__dirname, "package.json")).version;
var toPath = path2.join(__dirname, "bin", "esbuild");
var isToPathJS = true;
function validateBinaryVersion(...command) {
command.push("--version");
let stdout;
try {
stdout = child_process.execFileSync(command.shift(), command, {
// Without this, this install script strangely crashes with the error
// "EACCES: permission denied, write" but only on Ubuntu Linux when node is
// installed from the Snap Store. This is not a problem when you download
// the official version of node. The problem appears to be that stderr
// (i.e. file descriptor 2) isn't writable?
//
// More info:
// - https://snapcraft.io/ (what the Snap Store is)
// - https://nodejs.org/dist/ (download the official version of node)
// - https://github.com/evanw/esbuild/issues/1711#issuecomment-1027554035
//
stdio: "pipe"
}).toString().trim();
} catch (err) {
if (os2.platform() === "darwin" && /_SecTrustEvaluateWithError/.test(err + "")) {
let os3 = "this version of macOS";
try {
os3 = "macOS " + child_process.execFileSync("sw_vers", ["-productVersion"]).toString().trim();
} catch {
}
throw new Error(`The "esbuild" package cannot be installed because ${os3} is too outdated.
The Go compiler (which esbuild relies on) no longer supports ${os3},
which means the "esbuild" binary executable can't be run. You can either:
* Update your version of macOS to one that the Go compiler supports
* Use the "esbuild-wasm" package instead of the "esbuild" package
* Build esbuild yourself using an older version of the Go compiler
`);
}
throw err;
}
if (stdout !== versionFromPackageJSON) {
throw new Error(`Expected ${JSON.stringify(versionFromPackageJSON)} but got ${JSON.stringify(stdout)}`);
}
}
function isYarn() {
const { npm_config_user_agent } = process.env;
if (npm_config_user_agent) {
return /\byarn\//.test(npm_config_user_agent);
}
return false;
}
function fetch(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location)
return fetch(res.headers.location).then(resolve, reject);
if (res.statusCode !== 200)
return reject(new Error(`Server responded with ${res.statusCode}`));
let chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => resolve(Buffer.concat(chunks)));
}).on("error", reject);
});
}
function extractFileFromTarGzip(buffer, subpath) {
try {
buffer = zlib.unzipSync(buffer);
} catch (err) {
throw new Error(`Invalid gzip data in archive: ${err && err.message || err}`);
}
let str = (i, n) => String.fromCharCode(...buffer.subarray(i, i + n)).replace(/\0.*$/, "");
let offset = 0;
subpath = `package/${subpath}`;
while (offset < buffer.length) {
let name = str(offset, 100);
let size = parseInt(str(offset + 124, 12), 8);
offset += 512;
if (!isNaN(size)) {
if (name === subpath) return buffer.subarray(offset, offset + size);
offset += size + 511 & ~511;
}
}
throw new Error(`Could not find ${JSON.stringify(subpath)} in archive`);
}
function installUsingNPM(pkg, subpath, binPath) {
const env = { ...process.env, npm_config_global: void 0 };
const esbuildLibDir = path2.dirname(require.resolve("esbuild"));
const installDir = path2.join(esbuildLibDir, "npm-install");
fs2.mkdirSync(installDir);
try {
fs2.writeFileSync(path2.join(installDir, "package.json"), "{}");
child_process.execSync(
`npm install --loglevel=error --prefer-offline --no-audit --progress=false ${pkg}@${versionFromPackageJSON}`,
{ cwd: installDir, stdio: "pipe", env }
);
const installedBinPath = path2.join(installDir, "node_modules", pkg, subpath);
fs2.renameSync(installedBinPath, binPath);
} finally {
try {
removeRecursive(installDir);
} catch {
}
}
}
function removeRecursive(dir) {
for (const entry of fs2.readdirSync(dir)) {
const entryPath = path2.join(dir, entry);
let stats;
try {
stats = fs2.lstatSync(entryPath);
} catch {
continue;
}
if (stats.isDirectory()) removeRecursive(entryPath);
else fs2.unlinkSync(entryPath);
}
fs2.rmdirSync(dir);
}
function applyManualBinaryPathOverride(overridePath) {
const pathString = JSON.stringify(overridePath);
fs2.writeFileSync(toPath, `#!/usr/bin/env node
require('child_process').execFileSync(${pathString}, process.argv.slice(2), { stdio: 'inherit' });
`);
const libMain = path2.join(__dirname, "lib", "main.js");
const code = fs2.readFileSync(libMain, "utf8");
fs2.writeFileSync(libMain, `var ESBUILD_BINARY_PATH = ${pathString};
${code}`);
}
function maybeOptimizePackage(binPath) {
if (os2.platform() !== "win32" && !isYarn()) {
const tempPath = path2.join(__dirname, "bin-esbuild");
try {
fs2.linkSync(binPath, tempPath);
fs2.renameSync(tempPath, toPath);
isToPathJS = false;
fs2.unlinkSync(tempPath);
} catch {
}
}
}
async function downloadDirectlyFromNPM(pkg, subpath, binPath) {
const url = `https://registry.npmjs.org/${pkg}/-/${pkg.replace("@esbuild/", "")}-${versionFromPackageJSON}.tgz`;
console.error(`[esbuild] Trying to download ${JSON.stringify(url)}`);
try {
fs2.writeFileSync(binPath, extractFileFromTarGzip(await fetch(url), subpath));
fs2.chmodSync(binPath, 493);
} catch (e) {
console.error(`[esbuild] Failed to download ${JSON.stringify(url)}: ${e && e.message || e}`);
throw e;
}
}
async function checkAndPreparePackage() {
if (isValidBinaryPath(ESBUILD_BINARY_PATH)) {
if (!fs2.existsSync(ESBUILD_BINARY_PATH)) {
console.warn(`[esbuild] Ignoring bad configuration: ESBUILD_BINARY_PATH=${ESBUILD_BINARY_PATH}`);
} else {
applyManualBinaryPathOverride(ESBUILD_BINARY_PATH);
return;
}
}
const { pkg, subpath } = pkgAndSubpathForCurrentPlatform();
let binPath;
try {
binPath = require.resolve(`${pkg}/${subpath}`);
} catch (e) {
console.error(`[esbuild] Failed to find package "${pkg}" on the file system
This can happen if you use the "--no-optional" flag. The "optionalDependencies"
package.json feature is used by esbuild to install the correct binary executable
for your current platform. This install script will now attempt to work around
this. If that fails, you need to remove the "--no-optional" flag to use esbuild.
`);
binPath = downloadedBinPath(pkg, subpath);
try {
console.error(`[esbuild] Trying to install package "${pkg}" using npm`);
installUsingNPM(pkg, subpath, binPath);
} catch (e2) {
console.error(`[esbuild] Failed to install package "${pkg}" using npm: ${e2 && e2.message || e2}`);
try {
await downloadDirectlyFromNPM(pkg, subpath, binPath);
} catch (e3) {
throw new Error(`Failed to install package "${pkg}"`);
}
}
}
maybeOptimizePackage(binPath);
}
checkAndPreparePackage().then(() => {
if (isToPathJS) {
validateBinaryVersion(process.execPath, toPath);
} else {
validateBinaryVersion(toPath);
}
});

View File

@@ -0,0 +1,705 @@
export type Platform = 'browser' | 'node' | 'neutral'
export type Format = 'iife' | 'cjs' | 'esm'
export type Loader = 'base64' | 'binary' | 'copy' | 'css' | 'dataurl' | 'default' | 'empty' | 'file' | 'js' | 'json' | 'jsx' | 'local-css' | 'text' | 'ts' | 'tsx'
export type LogLevel = 'verbose' | 'debug' | 'info' | 'warning' | 'error' | 'silent'
export type Charset = 'ascii' | 'utf8'
export type Drop = 'console' | 'debugger'
interface CommonOptions {
/** Documentation: https://esbuild.github.io/api/#sourcemap */
sourcemap?: boolean | 'linked' | 'inline' | 'external' | 'both'
/** Documentation: https://esbuild.github.io/api/#legal-comments */
legalComments?: 'none' | 'inline' | 'eof' | 'linked' | 'external'
/** Documentation: https://esbuild.github.io/api/#source-root */
sourceRoot?: string
/** Documentation: https://esbuild.github.io/api/#sources-content */
sourcesContent?: boolean
/** Documentation: https://esbuild.github.io/api/#format */
format?: Format
/** Documentation: https://esbuild.github.io/api/#global-name */
globalName?: string
/** Documentation: https://esbuild.github.io/api/#target */
target?: string | string[]
/** Documentation: https://esbuild.github.io/api/#supported */
supported?: Record<string, boolean>
/** Documentation: https://esbuild.github.io/api/#platform */
platform?: Platform
/** Documentation: https://esbuild.github.io/api/#mangle-props */
mangleProps?: RegExp
/** Documentation: https://esbuild.github.io/api/#mangle-props */
reserveProps?: RegExp
/** Documentation: https://esbuild.github.io/api/#mangle-props */
mangleQuoted?: boolean
/** Documentation: https://esbuild.github.io/api/#mangle-props */
mangleCache?: Record<string, string | false>
/** Documentation: https://esbuild.github.io/api/#drop */
drop?: Drop[]
/** Documentation: https://esbuild.github.io/api/#drop-labels */
dropLabels?: string[]
/** Documentation: https://esbuild.github.io/api/#minify */
minify?: boolean
/** Documentation: https://esbuild.github.io/api/#minify */
minifyWhitespace?: boolean
/** Documentation: https://esbuild.github.io/api/#minify */
minifyIdentifiers?: boolean
/** Documentation: https://esbuild.github.io/api/#minify */
minifySyntax?: boolean
/** Documentation: https://esbuild.github.io/api/#line-limit */
lineLimit?: number
/** Documentation: https://esbuild.github.io/api/#charset */
charset?: Charset
/** Documentation: https://esbuild.github.io/api/#tree-shaking */
treeShaking?: boolean
/** Documentation: https://esbuild.github.io/api/#ignore-annotations */
ignoreAnnotations?: boolean
/** Documentation: https://esbuild.github.io/api/#jsx */
jsx?: 'transform' | 'preserve' | 'automatic'
/** Documentation: https://esbuild.github.io/api/#jsx-factory */
jsxFactory?: string
/** Documentation: https://esbuild.github.io/api/#jsx-fragment */
jsxFragment?: string
/** Documentation: https://esbuild.github.io/api/#jsx-import-source */
jsxImportSource?: string
/** Documentation: https://esbuild.github.io/api/#jsx-development */
jsxDev?: boolean
/** Documentation: https://esbuild.github.io/api/#jsx-side-effects */
jsxSideEffects?: boolean
/** Documentation: https://esbuild.github.io/api/#define */
define?: { [key: string]: string }
/** Documentation: https://esbuild.github.io/api/#pure */
pure?: string[]
/** Documentation: https://esbuild.github.io/api/#keep-names */
keepNames?: boolean
/** Documentation: https://esbuild.github.io/api/#color */
color?: boolean
/** Documentation: https://esbuild.github.io/api/#log-level */
logLevel?: LogLevel
/** Documentation: https://esbuild.github.io/api/#log-limit */
logLimit?: number
/** Documentation: https://esbuild.github.io/api/#log-override */
logOverride?: Record<string, LogLevel>
/** Documentation: https://esbuild.github.io/api/#tsconfig-raw */
tsconfigRaw?: string | TsconfigRaw
}
export interface TsconfigRaw {
compilerOptions?: {
alwaysStrict?: boolean
baseUrl?: string
experimentalDecorators?: boolean
importsNotUsedAsValues?: 'remove' | 'preserve' | 'error'
jsx?: 'preserve' | 'react-native' | 'react' | 'react-jsx' | 'react-jsxdev'
jsxFactory?: string
jsxFragmentFactory?: string
jsxImportSource?: string
paths?: Record<string, string[]>
preserveValueImports?: boolean
strict?: boolean
target?: string
useDefineForClassFields?: boolean
verbatimModuleSyntax?: boolean
}
}
export interface BuildOptions extends CommonOptions {
/** Documentation: https://esbuild.github.io/api/#bundle */
bundle?: boolean
/** Documentation: https://esbuild.github.io/api/#splitting */
splitting?: boolean
/** Documentation: https://esbuild.github.io/api/#preserve-symlinks */
preserveSymlinks?: boolean
/** Documentation: https://esbuild.github.io/api/#outfile */
outfile?: string
/** Documentation: https://esbuild.github.io/api/#metafile */
metafile?: boolean
/** Documentation: https://esbuild.github.io/api/#outdir */
outdir?: string
/** Documentation: https://esbuild.github.io/api/#outbase */
outbase?: string
/** Documentation: https://esbuild.github.io/api/#external */
external?: string[]
/** Documentation: https://esbuild.github.io/api/#packages */
packages?: 'bundle' | 'external'
/** Documentation: https://esbuild.github.io/api/#alias */
alias?: Record<string, string>
/** Documentation: https://esbuild.github.io/api/#loader */
loader?: { [ext: string]: Loader }
/** Documentation: https://esbuild.github.io/api/#resolve-extensions */
resolveExtensions?: string[]
/** Documentation: https://esbuild.github.io/api/#main-fields */
mainFields?: string[]
/** Documentation: https://esbuild.github.io/api/#conditions */
conditions?: string[]
/** Documentation: https://esbuild.github.io/api/#write */
write?: boolean
/** Documentation: https://esbuild.github.io/api/#allow-overwrite */
allowOverwrite?: boolean
/** Documentation: https://esbuild.github.io/api/#tsconfig */
tsconfig?: string
/** Documentation: https://esbuild.github.io/api/#out-extension */
outExtension?: { [ext: string]: string }
/** Documentation: https://esbuild.github.io/api/#public-path */
publicPath?: string
/** Documentation: https://esbuild.github.io/api/#entry-names */
entryNames?: string
/** Documentation: https://esbuild.github.io/api/#chunk-names */
chunkNames?: string
/** Documentation: https://esbuild.github.io/api/#asset-names */
assetNames?: string
/** Documentation: https://esbuild.github.io/api/#inject */
inject?: string[]
/** Documentation: https://esbuild.github.io/api/#banner */
banner?: { [type: string]: string }
/** Documentation: https://esbuild.github.io/api/#footer */
footer?: { [type: string]: string }
/** Documentation: https://esbuild.github.io/api/#entry-points */
entryPoints?: string[] | Record<string, string> | { in: string, out: string }[]
/** Documentation: https://esbuild.github.io/api/#stdin */
stdin?: StdinOptions
/** Documentation: https://esbuild.github.io/plugins/ */
plugins?: Plugin[]
/** Documentation: https://esbuild.github.io/api/#working-directory */
absWorkingDir?: string
/** Documentation: https://esbuild.github.io/api/#node-paths */
nodePaths?: string[]; // The "NODE_PATH" variable from Node.js
}
export interface StdinOptions {
contents: string | Uint8Array
resolveDir?: string
sourcefile?: string
loader?: Loader
}
export interface Message {
id: string
pluginName: string
text: string
location: Location | null
notes: Note[]
/**
* Optional user-specified data that is passed through unmodified. You can
* use this to stash the original error, for example.
*/
detail: any
}
export interface Note {
text: string
location: Location | null
}
export interface Location {
file: string
namespace: string
/** 1-based */
line: number
/** 0-based, in bytes */
column: number
/** in bytes */
length: number
lineText: string
suggestion: string
}
export interface OutputFile {
path: string
contents: Uint8Array
hash: string
/** "contents" as text (changes automatically with "contents") */
readonly text: string
}
export interface BuildResult<ProvidedOptions extends BuildOptions = BuildOptions> {
errors: Message[]
warnings: Message[]
/** Only when "write: false" */
outputFiles: OutputFile[] | (ProvidedOptions['write'] extends false ? never : undefined)
/** Only when "metafile: true" */
metafile: Metafile | (ProvidedOptions['metafile'] extends true ? never : undefined)
/** Only when "mangleCache" is present */
mangleCache: Record<string, string | false> | (ProvidedOptions['mangleCache'] extends Object ? never : undefined)
}
export interface BuildFailure extends Error {
errors: Message[]
warnings: Message[]
}
/** Documentation: https://esbuild.github.io/api/#serve-arguments */
export interface ServeOptions {
port?: number
host?: string
servedir?: string
keyfile?: string
certfile?: string
fallback?: string
onRequest?: (args: ServeOnRequestArgs) => void
}
export interface ServeOnRequestArgs {
remoteAddress: string
method: string
path: string
status: number
/** The time to generate the response, not to send it */
timeInMS: number
}
/** Documentation: https://esbuild.github.io/api/#serve-return-values */
export interface ServeResult {
port: number
host: string
}
export interface TransformOptions extends CommonOptions {
/** Documentation: https://esbuild.github.io/api/#sourcefile */
sourcefile?: string
/** Documentation: https://esbuild.github.io/api/#loader */
loader?: Loader
/** Documentation: https://esbuild.github.io/api/#banner */
banner?: string
/** Documentation: https://esbuild.github.io/api/#footer */
footer?: string
}
export interface TransformResult<ProvidedOptions extends TransformOptions = TransformOptions> {
code: string
map: string
warnings: Message[]
/** Only when "mangleCache" is present */
mangleCache: Record<string, string | false> | (ProvidedOptions['mangleCache'] extends Object ? never : undefined)
/** Only when "legalComments" is "external" */
legalComments: string | (ProvidedOptions['legalComments'] extends 'external' ? never : undefined)
}
export interface TransformFailure extends Error {
errors: Message[]
warnings: Message[]
}
export interface Plugin {
name: string
setup: (build: PluginBuild) => (void | Promise<void>)
}
export interface PluginBuild {
/** Documentation: https://esbuild.github.io/plugins/#build-options */
initialOptions: BuildOptions
/** Documentation: https://esbuild.github.io/plugins/#resolve */
resolve(path: string, options?: ResolveOptions): Promise<ResolveResult>
/** Documentation: https://esbuild.github.io/plugins/#on-start */
onStart(callback: () =>
(OnStartResult | null | void | Promise<OnStartResult | null | void>)): void
/** Documentation: https://esbuild.github.io/plugins/#on-end */
onEnd(callback: (result: BuildResult) =>
(OnEndResult | null | void | Promise<OnEndResult | null | void>)): void
/** Documentation: https://esbuild.github.io/plugins/#on-resolve */
onResolve(options: OnResolveOptions, callback: (args: OnResolveArgs) =>
(OnResolveResult | null | undefined | Promise<OnResolveResult | null | undefined>)): void
/** Documentation: https://esbuild.github.io/plugins/#on-load */
onLoad(options: OnLoadOptions, callback: (args: OnLoadArgs) =>
(OnLoadResult | null | undefined | Promise<OnLoadResult | null | undefined>)): void
/** Documentation: https://esbuild.github.io/plugins/#on-dispose */
onDispose(callback: () => void): void
// This is a full copy of the esbuild library in case you need it
esbuild: {
context: typeof context,
build: typeof build,
buildSync: typeof buildSync,
transform: typeof transform,
transformSync: typeof transformSync,
formatMessages: typeof formatMessages,
formatMessagesSync: typeof formatMessagesSync,
analyzeMetafile: typeof analyzeMetafile,
analyzeMetafileSync: typeof analyzeMetafileSync,
initialize: typeof initialize,
version: typeof version,
}
}
/** Documentation: https://esbuild.github.io/plugins/#resolve-options */
export interface ResolveOptions {
pluginName?: string
importer?: string
namespace?: string
resolveDir?: string
kind?: ImportKind
pluginData?: any
with?: Record<string, string>
}
/** Documentation: https://esbuild.github.io/plugins/#resolve-results */
export interface ResolveResult {
errors: Message[]
warnings: Message[]
path: string
external: boolean
sideEffects: boolean
namespace: string
suffix: string
pluginData: any
}
export interface OnStartResult {
errors?: PartialMessage[]
warnings?: PartialMessage[]
}
export interface OnEndResult {
errors?: PartialMessage[]
warnings?: PartialMessage[]
}
/** Documentation: https://esbuild.github.io/plugins/#on-resolve-options */
export interface OnResolveOptions {
filter: RegExp
namespace?: string
}
/** Documentation: https://esbuild.github.io/plugins/#on-resolve-arguments */
export interface OnResolveArgs {
path: string
importer: string
namespace: string
resolveDir: string
kind: ImportKind
pluginData: any
with: Record<string, string>
}
export type ImportKind =
| 'entry-point'
// JS
| 'import-statement'
| 'require-call'
| 'dynamic-import'
| 'require-resolve'
// CSS
| 'import-rule'
| 'composes-from'
| 'url-token'
/** Documentation: https://esbuild.github.io/plugins/#on-resolve-results */
export interface OnResolveResult {
pluginName?: string
errors?: PartialMessage[]
warnings?: PartialMessage[]
path?: string
external?: boolean
sideEffects?: boolean
namespace?: string
suffix?: string
pluginData?: any
watchFiles?: string[]
watchDirs?: string[]
}
/** Documentation: https://esbuild.github.io/plugins/#on-load-options */
export interface OnLoadOptions {
filter: RegExp
namespace?: string
}
/** Documentation: https://esbuild.github.io/plugins/#on-load-arguments */
export interface OnLoadArgs {
path: string
namespace: string
suffix: string
pluginData: any
with: Record<string, string>
}
/** Documentation: https://esbuild.github.io/plugins/#on-load-results */
export interface OnLoadResult {
pluginName?: string
errors?: PartialMessage[]
warnings?: PartialMessage[]
contents?: string | Uint8Array
resolveDir?: string
loader?: Loader
pluginData?: any
watchFiles?: string[]
watchDirs?: string[]
}
export interface PartialMessage {
id?: string
pluginName?: string
text?: string
location?: Partial<Location> | null
notes?: PartialNote[]
detail?: any
}
export interface PartialNote {
text?: string
location?: Partial<Location> | null
}
/** Documentation: https://esbuild.github.io/api/#metafile */
export interface Metafile {
inputs: {
[path: string]: {
bytes: number
imports: {
path: string
kind: ImportKind
external?: boolean
original?: string
with?: Record<string, string>
}[]
format?: 'cjs' | 'esm'
with?: Record<string, string>
}
}
outputs: {
[path: string]: {
bytes: number
inputs: {
[path: string]: {
bytesInOutput: number
}
}
imports: {
path: string
kind: ImportKind | 'file-loader'
external?: boolean
}[]
exports: string[]
entryPoint?: string
cssBundle?: string
}
}
}
export interface FormatMessagesOptions {
kind: 'error' | 'warning'
color?: boolean
terminalWidth?: number
}
export interface AnalyzeMetafileOptions {
color?: boolean
verbose?: boolean
}
export interface WatchOptions {
}
export interface BuildContext<ProvidedOptions extends BuildOptions = BuildOptions> {
/** Documentation: https://esbuild.github.io/api/#rebuild */
rebuild(): Promise<BuildResult<ProvidedOptions>>
/** Documentation: https://esbuild.github.io/api/#watch */
watch(options?: WatchOptions): Promise<void>
/** Documentation: https://esbuild.github.io/api/#serve */
serve(options?: ServeOptions): Promise<ServeResult>
cancel(): Promise<void>
dispose(): Promise<void>
}
// This is a TypeScript type-level function which replaces any keys in "In"
// that aren't in "Out" with "never". We use this to reject properties with
// typos in object literals. See: https://stackoverflow.com/questions/49580725
type SameShape<Out, In extends Out> = In & { [Key in Exclude<keyof In, keyof Out>]: never }
/**
* This function invokes the "esbuild" command-line tool for you. It returns a
* promise that either resolves with a "BuildResult" object or rejects with a
* "BuildFailure" object.
*
* - Works in node: yes
* - Works in browser: yes
*
* Documentation: https://esbuild.github.io/api/#build
*/
export declare function build<T extends BuildOptions>(options: SameShape<BuildOptions, T>): Promise<BuildResult<T>>
/**
* This is the advanced long-running form of "build" that supports additional
* features such as watch mode and a local development server.
*
* - Works in node: yes
* - Works in browser: no
*
* Documentation: https://esbuild.github.io/api/#build
*/
export declare function context<T extends BuildOptions>(options: SameShape<BuildOptions, T>): Promise<BuildContext<T>>
/**
* This function transforms a single JavaScript file. It can be used to minify
* JavaScript, convert TypeScript/JSX to JavaScript, or convert newer JavaScript
* to older JavaScript. It returns a promise that is either resolved with a
* "TransformResult" object or rejected with a "TransformFailure" object.
*
* - Works in node: yes
* - Works in browser: yes
*
* Documentation: https://esbuild.github.io/api/#transform
*/
export declare function transform<T extends TransformOptions>(input: string | Uint8Array, options?: SameShape<TransformOptions, T>): Promise<TransformResult<T>>
/**
* Converts log messages to formatted message strings suitable for printing in
* the terminal. This allows you to reuse the built-in behavior of esbuild's
* log message formatter. This is a batch-oriented API for efficiency.
*
* - Works in node: yes
* - Works in browser: yes
*/
export declare function formatMessages(messages: PartialMessage[], options: FormatMessagesOptions): Promise<string[]>
/**
* Pretty-prints an analysis of the metafile JSON to a string. This is just for
* convenience to be able to match esbuild's pretty-printing exactly. If you want
* to customize it, you can just inspect the data in the metafile yourself.
*
* - Works in node: yes
* - Works in browser: yes
*
* Documentation: https://esbuild.github.io/api/#analyze
*/
export declare function analyzeMetafile(metafile: Metafile | string, options?: AnalyzeMetafileOptions): Promise<string>
/**
* A synchronous version of "build".
*
* - Works in node: yes
* - Works in browser: no
*
* Documentation: https://esbuild.github.io/api/#build
*/
export declare function buildSync<T extends BuildOptions>(options: SameShape<BuildOptions, T>): BuildResult<T>
/**
* A synchronous version of "transform".
*
* - Works in node: yes
* - Works in browser: no
*
* Documentation: https://esbuild.github.io/api/#transform
*/
export declare function transformSync<T extends TransformOptions>(input: string | Uint8Array, options?: SameShape<TransformOptions, T>): TransformResult<T>
/**
* A synchronous version of "formatMessages".
*
* - Works in node: yes
* - Works in browser: no
*/
export declare function formatMessagesSync(messages: PartialMessage[], options: FormatMessagesOptions): string[]
/**
* A synchronous version of "analyzeMetafile".
*
* - Works in node: yes
* - Works in browser: no
*
* Documentation: https://esbuild.github.io/api/#analyze
*/
export declare function analyzeMetafileSync(metafile: Metafile | string, options?: AnalyzeMetafileOptions): string
/**
* This configures the browser-based version of esbuild. It is necessary to
* call this first and wait for the returned promise to be resolved before
* making other API calls when using esbuild in the browser.
*
* - Works in node: yes
* - Works in browser: yes ("options" is required)
*
* Documentation: https://esbuild.github.io/api/#browser
*/
export declare function initialize(options: InitializeOptions): Promise<void>
export interface InitializeOptions {
/**
* The URL of the "esbuild.wasm" file. This must be provided when running
* esbuild in the browser.
*/
wasmURL?: string | URL
/**
* The result of calling "new WebAssembly.Module(buffer)" where "buffer"
* is a typed array or ArrayBuffer containing the binary code of the
* "esbuild.wasm" file.
*
* You can use this as an alternative to "wasmURL" for environments where it's
* not possible to download the WebAssembly module.
*/
wasmModule?: WebAssembly.Module
/**
* By default esbuild runs the WebAssembly-based browser API in a web worker
* to avoid blocking the UI thread. This can be disabled by setting "worker"
* to false.
*/
worker?: boolean
}
export let version: string
// Call this function to terminate esbuild's child process. The child process
// is not terminated and re-created after each API call because it's more
// efficient to keep it around when there are multiple API calls.
//
// In node this happens automatically before the parent node process exits. So
// you only need to call this if you know you will not make any more esbuild
// API calls and you want to clean up resources.
//
// Unlike node, Deno lacks the necessary APIs to clean up child processes
// automatically. You must manually call stop() in Deno when you're done
// using esbuild or Deno will continue running forever.
//
// Another reason you might want to call this is if you are using esbuild from
// within a Deno test. Deno fails tests that create a child process without
// killing it before the test ends, so you have to call this function (and
// await the returned promise) in every Deno test that uses esbuild.
export declare function stop(): Promise<void>
// Note: These declarations exist to avoid type errors when you omit "dom" from
// "lib" in your "tsconfig.json" file. TypeScript confusingly declares the
// global "WebAssembly" type in "lib.dom.d.ts" even though it has nothing to do
// with the browser DOM and is present in many non-browser JavaScript runtimes
// (e.g. node and deno). Declaring it here allows esbuild's API to be used in
// these scenarios.
//
// There's an open issue about getting this problem corrected (although these
// declarations will need to remain even if this is fixed for backward
// compatibility with older TypeScript versions):
//
// https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/826
//
declare global {
namespace WebAssembly {
interface Module {
}
}
interface URL {
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
{
"name": "esbuild",
"version": "0.24.2",
"description": "An extremely fast JavaScript and CSS bundler and minifier.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"scripts": {
"postinstall": "node install.js"
},
"main": "lib/main.js",
"types": "lib/main.d.ts",
"engines": {
"node": ">=18"
},
"bin": {
"esbuild": "bin/esbuild"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.24.2",
"@esbuild/android-arm": "0.24.2",
"@esbuild/android-arm64": "0.24.2",
"@esbuild/android-x64": "0.24.2",
"@esbuild/darwin-arm64": "0.24.2",
"@esbuild/darwin-x64": "0.24.2",
"@esbuild/freebsd-arm64": "0.24.2",
"@esbuild/freebsd-x64": "0.24.2",
"@esbuild/linux-arm": "0.24.2",
"@esbuild/linux-arm64": "0.24.2",
"@esbuild/linux-ia32": "0.24.2",
"@esbuild/linux-loong64": "0.24.2",
"@esbuild/linux-mips64el": "0.24.2",
"@esbuild/linux-ppc64": "0.24.2",
"@esbuild/linux-riscv64": "0.24.2",
"@esbuild/linux-s390x": "0.24.2",
"@esbuild/linux-x64": "0.24.2",
"@esbuild/netbsd-arm64": "0.24.2",
"@esbuild/netbsd-x64": "0.24.2",
"@esbuild/openbsd-arm64": "0.24.2",
"@esbuild/openbsd-x64": "0.24.2",
"@esbuild/sunos-x64": "0.24.2",
"@esbuild/win32-arm64": "0.24.2",
"@esbuild/win32-ia32": "0.24.2",
"@esbuild/win32-x64": "0.24.2"
},
"license": "MIT"
}

View File

@@ -0,0 +1,38 @@
{
"name": "meshcore-cracker",
"version": "1.0.0",
"description": "Standalone MeshCore GroupText packet cracker library",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && node build.js",
"watch": "tsc --watch"
},
"dependencies": {
"@michaelhart/meshcore-decoder": "^0.2.7",
"crypto-js": "^4.2.0"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@webgpu/types": "^0.1.68",
"esbuild": "^0.24.2",
"typescript": "^5.7.2"
},
"keywords": [
"meshcore",
"cracker",
"brute-force",
"webgpu"
],
"license": "MIT"
}

View File

@@ -0,0 +1,282 @@
// Core logic for MeshCore packet cracker - pure functions
import SHA256 from 'crypto-js/sha256';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import Hex from 'crypto-js/enc-hex';
// Room name character set
export const CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789';
export const CHARS_LEN = CHARS.length; // 36
export const CHARS_WITH_DASH = CHARS + '-';
// Public room special case
export const PUBLIC_ROOM_NAME = '[[public room]]';
export const PUBLIC_KEY = '8b3387e9c5cdea6ac9e5edbaa115cd72';
/**
* Convert room name to (length, index) for resuming/skipping.
* Index encoding: LSB-first (first character = least significant digit).
*/
export function roomNameToIndex(name: string): { length: number; index: number } | null {
if (!name || name.length === 0) {
return null;
}
const length = name.length;
let index = 0;
let multiplier = 1;
// Process from left to right (first char is LSB, matching indexToRoomName)
for (let i = 0; i < length; i++) {
const c = name[i];
const charIdx = CHARS_WITH_DASH.indexOf(c);
if (charIdx === -1) {
return null;
} // Invalid character
const isFirst = i === 0;
const isLast = i === length - 1;
const charCount = isFirst || isLast ? 36 : 37;
// Dash not allowed at start/end
if ((isFirst || isLast) && charIdx === 36) {
return null;
}
index += charIdx * multiplier;
multiplier *= charCount;
}
return { length, index };
}
/**
* Convert (length, index) to room name.
* Index encoding: LSB-first (first character = least significant digit).
*/
export function indexToRoomName(length: number, idx: number): string | null {
if (length <= 0) {
return null;
}
let result = '';
let remaining = idx;
let prevWasDash = false;
for (let i = 0; i < length; i++) {
const isFirst = i === 0;
const isLast = i === length - 1;
const charCount = isFirst || isLast ? 36 : 37;
const charIdx = remaining % charCount;
remaining = Math.floor(remaining / charCount);
const isDash = charIdx === 36;
if (isDash && prevWasDash) {
return null;
} // Invalid: consecutive dashes
prevWasDash = isDash;
result += CHARS_WITH_DASH[charIdx];
}
return result;
}
/**
* Derive 128-bit key from room name using SHA256.
* Room names are prefixed with '#' before hashing.
*/
export function deriveKeyFromRoomName(roomName: string): string {
if (roomName === PUBLIC_ROOM_NAME) {
return PUBLIC_KEY;
}
const hash = SHA256(roomName);
return hash.toString(Hex).substring(0, 32);
}
/**
* Compute channel hash (first byte of SHA256(key)).
*/
export function getChannelHash(keyHex: string): string {
const hash = SHA256(Hex.parse(keyHex));
return hash.toString(Hex).substring(0, 2);
}
/**
* Verify MAC using HMAC-SHA256 with 32-byte padded key.
*/
export function verifyMac(ciphertext: string, cipherMac: string, keyHex: string): boolean {
const paddedKey = keyHex.padEnd(64, '0');
const hmac = HmacSHA256(Hex.parse(ciphertext), Hex.parse(paddedKey));
const computed = hmac.toString(Hex).substring(0, 4).toLowerCase();
return computed === cipherMac.toLowerCase();
}
/**
* Count valid room names for a given length.
* Accounts for dash rules (no start/end dash, no consecutive dashes).
*/
export function countNamesForLength(len: number): number {
if (len === 1) {
return CHARS_LEN;
}
if (len === 2) {
return CHARS_LEN * CHARS_LEN;
}
// For length >= 3: first and last are CHARS (36), middle follows no-consecutive-dash rule
// Middle length = len - 2
// Use DP: count sequences of length k with no consecutive dashes
// endsWithNonDash[k], endsWithDash[k]
let endsNonDash = CHARS_LEN; // length 1 middle
let endsDash = 1;
for (let i = 2; i <= len - 2; i++) {
const newEndsNonDash = (endsNonDash + endsDash) * CHARS_LEN;
const newEndsDash = endsNonDash; // dash can only follow non-dash
endsNonDash = newEndsNonDash;
endsDash = newEndsDash;
}
const middleCount = len > 2 ? endsNonDash + endsDash : 1;
return CHARS_LEN * middleCount * CHARS_LEN;
}
/**
* Check if timestamp is within last month.
*/
export function isTimestampValid(timestamp: number, now?: number): boolean {
const ONE_MONTH_SECONDS = 30 * 24 * 60 * 60;
const currentTime = now ?? Math.floor(Date.now() / 1000);
return timestamp <= currentTime && timestamp >= currentTime - ONE_MONTH_SECONDS;
}
/**
* Check for valid UTF-8 (no replacement characters).
*/
export function isValidUtf8(text: string): boolean {
return !text.includes('\uFFFD');
}
/**
* Room name generator - iterates through all valid room names.
*/
export class RoomNameGenerator {
private length = 1;
private indices: number[] = [0];
private done = false;
private currentInLength = 0;
private totalForLength = CHARS_LEN;
current(): string {
return this.indices.map((i) => (i === CHARS_LEN ? '-' : CHARS[i])).join('');
}
getLength(): number {
return this.length;
}
getCurrentInLength(): number {
return this.currentInLength;
}
getTotalForLength(): number {
return this.totalForLength;
}
getRemainingInLength(): number {
return this.totalForLength - this.currentInLength;
}
isDone(): boolean {
return this.done;
}
next(): boolean {
if (this.done) {
return false;
}
this.currentInLength++;
// Increment with carry, respecting dash rules
let pos = this.length - 1;
while (pos >= 0) {
const isFirst = pos === 0;
const isLast = pos === this.length - 1;
const maxVal = isFirst || isLast ? CHARS_LEN - 1 : CHARS_LEN; // CHARS_LEN = dash index
if (this.indices[pos] < maxVal) {
this.indices[pos]++;
// Check dash rule: no consecutive dashes
if (this.indices[pos] === CHARS_LEN && pos > 0 && this.indices[pos - 1] === CHARS_LEN) {
// Would create consecutive dashes, continue incrementing
continue;
}
// Reset all positions after this one
for (let i = pos + 1; i < this.length; i++) {
this.indices[i] = 0;
}
// Validate: check no consecutive dashes in reset portion
if (this.isValid()) {
return true;
}
continue;
}
pos--;
}
// Overflow - increase length
this.length++;
this.indices = new Array(this.length).fill(0);
this.currentInLength = 0;
this.totalForLength = countNamesForLength(this.length);
return true;
}
private isValid(): boolean {
for (let i = 0; i < this.length; i++) {
const isDash = this.indices[i] === CHARS_LEN;
if (isDash && (i === 0 || i === this.length - 1)) {
return false;
}
if (isDash && i > 0 && this.indices[i - 1] === CHARS_LEN) {
return false;
}
}
return true;
}
// Skip invalid combinations efficiently
nextValid(): boolean {
do {
if (!this.next()) {
return false;
}
} while (!this.isValid());
return true;
}
// Skip to a specific (length, index) position
// Index encoding: first char is LSB (consistent with indexToRoomName)
skipTo(targetLength: number, targetIndex: number): void {
this.length = targetLength;
this.indices = new Array(targetLength).fill(0);
this.totalForLength = countNamesForLength(targetLength);
// Convert index to indices array (LSB first = position 0)
let remaining = targetIndex;
for (let i = 0; i < targetLength; i++) {
const isFirst = i === 0;
const isLast = i === targetLength - 1;
const charCount = isFirst || isLast ? CHARS_LEN : CHARS_LEN + 1;
this.indices[i] = remaining % charCount;
remaining = Math.floor(remaining / charCount);
}
this.currentInLength = targetIndex;
}
}

View File

@@ -0,0 +1,403 @@
/**
* GroupTextCracker - Standalone MeshCore GroupText packet cracker
*
* Cracks encrypted GroupText packets by trying room names until the
* correct encryption key is found.
*/
import { MeshCorePacketDecoder, ChannelCrypto } from '@michaelhart/meshcore-decoder';
import { GpuBruteForce, isWebGpuSupported } from './gpu-bruteforce';
import {
PUBLIC_ROOM_NAME,
PUBLIC_KEY,
indexToRoomName,
roomNameToIndex,
deriveKeyFromRoomName,
getChannelHash,
verifyMac,
countNamesForLength,
isTimestampValid,
isValidUtf8,
} from './core';
import type { CrackOptions, CrackResult, ProgressReport, ProgressCallback, DecodedPacket } from './types';
// Valid room name characters (for wordlist filtering)
const VALID_CHARS = /^[a-z0-9-]+$/;
const NO_DASH_AT_ENDS = /^[a-z0-9].*[a-z0-9]$|^[a-z0-9]$/;
const NO_CONSECUTIVE_DASHES = /--/;
function isValidRoomName(name: string): boolean {
if (!name || name.length === 0) return false;
if (!VALID_CHARS.test(name)) return false;
if (name.length > 1 && !NO_DASH_AT_ENDS.test(name)) return false;
if (NO_CONSECUTIVE_DASHES.test(name)) return false;
return true;
}
/**
* Main cracker class for MeshCore GroupText packets.
*/
export class GroupTextCracker {
private gpuInstance: GpuBruteForce | null = null;
private wordlist: string[] = [];
private abortFlag = false;
private useTimestampFilter = true;
private useUtf8Filter = true;
/**
* Load a wordlist from a URL for dictionary attacks.
* The wordlist should be a text file with one word per line.
*
* @param url - URL to fetch the wordlist from
*/
async loadWordlist(url: string): Promise<void> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load wordlist: ${response.status} ${response.statusText}`);
}
const text = await response.text();
const allWords = text
.split('\n')
.map((w) => w.trim().toLowerCase())
.filter((w) => w.length > 0);
// Filter to valid room names only
this.wordlist = allWords.filter(isValidRoomName);
}
/**
* Set the wordlist directly from an array of words.
*
* @param words - Array of room names to try
*/
setWordlist(words: string[]): void {
this.wordlist = words
.map((w) => w.trim().toLowerCase())
.filter(isValidRoomName);
}
/**
* Abort the current cracking operation.
* The crack() method will return with aborted: true.
*/
abort(): void {
this.abortFlag = true;
}
/**
* Check if WebGPU is available in the current environment.
*/
isGpuAvailable(): boolean {
return isWebGpuSupported();
}
/**
* Decode a packet and extract the information needed for cracking.
*
* @param packetHex - The packet data as a hex string
* @returns Decoded packet info or null if not a GroupText packet
*/
async decodePacket(packetHex: string): Promise<DecodedPacket | null> {
const cleanHex = packetHex.trim().replace(/\s+/g, '').replace(/^0x/i, '');
if (!cleanHex || !/^[0-9a-fA-F]+$/.test(cleanHex)) {
return null;
}
try {
const decoded = await MeshCorePacketDecoder.decodeWithVerification(cleanHex, {});
const payload = decoded.payload?.decoded as {
channelHash?: string;
ciphertext?: string;
cipherMac?: string;
} | null;
if (!payload?.channelHash || !payload?.ciphertext || !payload?.cipherMac) {
return null;
}
return {
channelHash: payload.channelHash,
ciphertext: payload.ciphertext,
cipherMac: payload.cipherMac,
isGroupText: true,
};
} catch {
return null;
}
}
/**
* Crack a GroupText packet to find the room name and decrypt the message.
*
* @param packetHex - The packet data as a hex string
* @param options - Cracking options
* @param onProgress - Optional callback for progress updates
* @returns The cracking result
*/
async crack(
packetHex: string,
options?: CrackOptions,
onProgress?: ProgressCallback,
): Promise<CrackResult> {
this.abortFlag = false;
this.useTimestampFilter = options?.useTimestampFilter ?? true;
this.useUtf8Filter = options?.useUtf8Filter ?? true;
const maxLength = options?.maxLength ?? 8;
// Decode packet
const decoded = await this.decodePacket(packetHex);
if (!decoded) {
return { found: false, error: 'Invalid packet or not a GroupText packet' };
}
const { channelHash, ciphertext, cipherMac } = decoded;
const targetHashByte = parseInt(channelHash, 16);
// Initialize GPU if not already done
if (!this.gpuInstance) {
this.gpuInstance = new GpuBruteForce();
const gpuOk = await this.gpuInstance.init();
if (!gpuOk) {
return { found: false, error: 'WebGPU not available' };
}
}
const startTime = performance.now();
let totalChecked = 0;
let lastProgressUpdate = performance.now();
// Determine starting position
let startFromLength = 1;
let startFromOffset = 0;
if (options?.startFrom) {
const pos = roomNameToIndex(options.startFrom);
if (pos) {
startFromLength = pos.length;
startFromOffset = pos.index + 1; // Start after the given position
if (startFromOffset >= countNamesForLength(startFromLength)) {
startFromLength++;
startFromOffset = 0;
}
}
}
// Calculate total candidates for progress
let totalCandidates = 0;
for (let l = startFromLength; l <= maxLength; l++) {
totalCandidates += countNamesForLength(l);
}
totalCandidates -= startFromOffset;
// Helper to report progress
const reportProgress = (
phase: ProgressReport['phase'],
currentLength: number,
currentPosition: string,
) => {
if (!onProgress) return;
const now = performance.now();
const elapsed = (now - startTime) / 1000;
const rate = elapsed > 0 ? Math.round(totalChecked / elapsed) : 0;
const remaining = totalCandidates - totalChecked;
const eta = rate > 0 ? remaining / rate : 0;
onProgress({
checked: totalChecked,
total: totalCandidates,
percent: totalCandidates > 0 ? Math.min(100, (totalChecked / totalCandidates) * 100) : 0,
rateKeysPerSec: rate,
etaSeconds: eta,
elapsedSeconds: elapsed,
currentLength,
currentPosition,
phase,
});
};
// Helper to verify MAC and filters
const verifyMacAndFilters = (
key: string,
): { valid: boolean; message?: string } => {
if (!verifyMac(ciphertext, cipherMac, key)) {
return { valid: false };
}
const result = ChannelCrypto.decryptGroupTextMessage(ciphertext, cipherMac, key);
if (!result.success || !result.data) {
return { valid: false };
}
if (this.useTimestampFilter && !isTimestampValid(result.data.timestamp)) {
return { valid: false };
}
if (this.useUtf8Filter && !isValidUtf8(result.data.message)) {
return { valid: false };
}
return { valid: true, message: result.data.message };
};
// Phase 1: Try public key
if (startFromLength === 1 && startFromOffset === 0) {
reportProgress('public-key', 0, PUBLIC_ROOM_NAME);
const publicChannelHash = getChannelHash(PUBLIC_KEY);
if (channelHash === publicChannelHash) {
const result = verifyMacAndFilters(PUBLIC_KEY);
if (result.valid) {
return {
found: true,
roomName: PUBLIC_ROOM_NAME,
key: PUBLIC_KEY,
decryptedMessage: result.message,
};
}
}
}
// Phase 2: Dictionary attack
if (this.wordlist.length > 0 && startFromLength === 1 && startFromOffset === 0) {
for (let i = 0; i < this.wordlist.length; i++) {
if (this.abortFlag) {
return {
found: false,
aborted: true,
resumeFrom: this.wordlist[i],
};
}
const word = this.wordlist[i];
const key = deriveKeyFromRoomName('#' + word);
const wordChannelHash = getChannelHash(key);
if (parseInt(wordChannelHash, 16) === targetHashByte) {
const result = verifyMacAndFilters(key);
if (result.valid) {
return {
found: true,
roomName: word,
key,
decryptedMessage: result.message,
};
}
}
// Progress update
const now = performance.now();
if (now - lastProgressUpdate >= 200) {
reportProgress('wordlist', word.length, word);
lastProgressUpdate = now;
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
// Phase 3: GPU brute force
const INITIAL_BATCH_SIZE = 32768;
const TARGET_DISPATCH_MS = 1000;
let currentBatchSize = INITIAL_BATCH_SIZE;
let batchSizeTuned = false;
for (let length = startFromLength; length <= maxLength; length++) {
if (this.abortFlag) {
const resumePos = indexToRoomName(length, 0);
return {
found: false,
aborted: true,
resumeFrom: resumePos || undefined,
};
}
const totalForLength = countNamesForLength(length);
let offset = length === startFromLength ? startFromOffset : 0;
while (offset < totalForLength) {
if (this.abortFlag) {
const resumePos = indexToRoomName(length, offset);
return {
found: false,
aborted: true,
resumeFrom: resumePos || undefined,
};
}
const batchSize = Math.min(currentBatchSize, totalForLength - offset);
const dispatchStart = performance.now();
const matches = await this.gpuInstance.runBatch(
targetHashByte,
length,
offset,
batchSize,
ciphertext,
cipherMac,
);
const dispatchTime = performance.now() - dispatchStart;
totalChecked += batchSize;
// Auto-tune batch size
if (!batchSizeTuned && batchSize >= INITIAL_BATCH_SIZE && dispatchTime > 0) {
const scaleFactor = TARGET_DISPATCH_MS / dispatchTime;
const optimalBatchSize = Math.round(batchSize * scaleFactor);
const rounded = Math.pow(
2,
Math.round(Math.log2(Math.max(INITIAL_BATCH_SIZE, optimalBatchSize))),
);
currentBatchSize = Math.max(INITIAL_BATCH_SIZE, rounded);
batchSizeTuned = true;
}
// Check matches
for (const matchIdx of matches) {
const roomName = indexToRoomName(length, matchIdx);
if (!roomName) continue;
const key = deriveKeyFromRoomName('#' + roomName);
const result = verifyMacAndFilters(key);
if (result.valid) {
return {
found: true,
roomName,
key,
decryptedMessage: result.message,
};
}
}
offset += batchSize;
// Progress update
const now = performance.now();
if (now - lastProgressUpdate >= 200) {
const currentPos = indexToRoomName(length, Math.min(offset, totalForLength - 1)) || '';
reportProgress('bruteforce', length, currentPos);
lastProgressUpdate = now;
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
// Not found
const lastPos = indexToRoomName(maxLength, countNamesForLength(maxLength) - 1);
return {
found: false,
resumeFrom: lastPos || undefined,
};
}
/**
* Clean up GPU resources.
* Call this when you're done using the cracker.
*/
destroy(): void {
if (this.gpuInstance) {
this.gpuInstance.destroy();
this.gpuInstance = null;
}
}
}

View File

@@ -0,0 +1,708 @@
// WebGPU-accelerated brute force key cracking for MeshCore packets
import { indexToRoomName, countNamesForLength } from './core';
export interface GpuBruteForceResult {
found: boolean;
roomName?: string;
key?: string;
candidateIndices?: number[];
}
export class GpuBruteForce {
private device: GPUDevice | null = null;
private pipeline: GPUComputePipeline | null = null;
private bindGroupLayout: GPUBindGroupLayout | null = null;
// Persistent buffers for reuse between batches
private paramsBuffer: GPUBuffer | null = null;
private matchCountBuffer: GPUBuffer | null = null;
private matchIndicesBuffer: GPUBuffer | null = null;
private ciphertextBuffer: GPUBuffer | null = null;
private ciphertextBufferSize: number = 0;
// Double-buffered staging buffers for overlapping GPU/CPU work
private matchCountReadBuffers: [GPUBuffer | null, GPUBuffer | null] = [null, null];
private matchIndicesReadBuffers: [GPUBuffer | null, GPUBuffer | null] = [null, null];
private currentReadBufferIndex: number = 0;
// Cached bind group (recreated only when ciphertext buffer changes)
private bindGroup: GPUBindGroup | null = null;
private bindGroupDirty: boolean = true;
// Reusable zero buffer for resetting match count
private static readonly ZERO_DATA = new Uint32Array([0]);
// Shader for SHA256 computation
private shaderCode = /* wgsl */ `
// SHA256 round constants
const K: array<u32, 64> = array<u32, 64>(
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u
);
// Character lookup table (a-z = 0-25, 0-9 = 26-35, dash = 36)
const CHARS: array<u32, 37> = array<u32, 37>(
0x61u, 0x62u, 0x63u, 0x64u, 0x65u, 0x66u, 0x67u, 0x68u, 0x69u, 0x6au, // a-j
0x6bu, 0x6cu, 0x6du, 0x6eu, 0x6fu, 0x70u, 0x71u, 0x72u, 0x73u, 0x74u, // k-t
0x75u, 0x76u, 0x77u, 0x78u, 0x79u, 0x7au, // u-z
0x30u, 0x31u, 0x32u, 0x33u, 0x34u, 0x35u, 0x36u, 0x37u, 0x38u, 0x39u, // 0-9
0x2du // dash
);
struct Params {
target_channel_hash: u32,
batch_offset: u32,
name_length: u32,
batch_size: u32,
target_mac: u32, // First 2 bytes of target MAC (in high 16 bits)
ciphertext_words: u32, // Number of 32-bit words in ciphertext
ciphertext_len_bits: u32, // Length of ciphertext in bits
verify_mac: u32, // 1 to verify MAC, 0 to skip
}
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var<storage, read_write> match_count: atomic<u32>;
@group(0) @binding(2) var<storage, read_write> match_indices: array<u32>;
@group(0) @binding(3) var<storage, read> ciphertext: array<u32>; // Ciphertext data
fn rotr(x: u32, n: u32) -> u32 {
return (x >> n) | (x << (32u - n));
}
fn ch(x: u32, y: u32, z: u32) -> u32 {
return (x & y) ^ (~x & z);
}
fn maj(x: u32, y: u32, z: u32) -> u32 {
return (x & y) ^ (x & z) ^ (y & z);
}
fn sigma0(x: u32) -> u32 {
return rotr(x, 2u) ^ rotr(x, 13u) ^ rotr(x, 22u);
}
fn sigma1(x: u32) -> u32 {
return rotr(x, 6u) ^ rotr(x, 11u) ^ rotr(x, 25u);
}
fn gamma0(x: u32) -> u32 {
return rotr(x, 7u) ^ rotr(x, 18u) ^ (x >> 3u);
}
fn gamma1(x: u32) -> u32 {
return rotr(x, 17u) ^ rotr(x, 19u) ^ (x >> 10u);
}
// Convert index to room name bytes, returns the hash as a u32 for the first byte check
fn index_to_room_name(idx: u32, length: u32, msg: ptr<function, array<u32, 16>>) -> bool {
// Message starts with '#' (0x23)
var byte_pos = 0u;
var word_idx = 0u;
var current_word = 0x23000000u; // '#' in big-endian position 0
byte_pos = 1u;
var remaining = idx;
var prev_was_dash = false;
// Generate room name from index
for (var i = 0u; i < length; i++) {
let char_count = select(37u, 36u, i == 0u || i == length - 1u); // no dash at start/end
var char_idx = remaining % char_count;
remaining = remaining / char_count;
// Check for consecutive dashes (invalid)
let is_dash = char_idx == 36u && i > 0u && i < length - 1u;
if (is_dash && prev_was_dash) {
return false; // Invalid: consecutive dashes
}
prev_was_dash = is_dash;
// Map char index to actual character
let c = CHARS[char_idx];
// Pack byte into current word (big-endian)
let shift = (3u - byte_pos % 4u) * 8u;
if (byte_pos % 4u == 0u && byte_pos > 0u) {
(*msg)[word_idx] = current_word;
word_idx = word_idx + 1u;
current_word = 0u;
}
current_word = current_word | (c << shift);
byte_pos = byte_pos + 1u;
}
// Add padding: 0x80 followed by zeros, then length in bits
let msg_len_bits = (length + 1u) * 8u; // +1 for '#'
// Add 0x80 padding byte
let shift = (3u - byte_pos % 4u) * 8u;
if (byte_pos % 4u == 0u) {
(*msg)[word_idx] = current_word;
word_idx = word_idx + 1u;
current_word = 0x80000000u;
} else {
current_word = current_word | (0x80u << shift);
}
byte_pos = byte_pos + 1u;
// Store current word
if (byte_pos % 4u == 0u || word_idx < 14u) {
(*msg)[word_idx] = current_word;
word_idx = word_idx + 1u;
}
// Zero-fill until word 14
for (var i = word_idx; i < 14u; i++) {
(*msg)[i] = 0u;
}
// Length in bits (64-bit, but we only use lower 32 bits for short messages)
(*msg)[14u] = 0u;
(*msg)[15u] = msg_len_bits;
return true;
}
fn sha256_block(msg: ptr<function, array<u32, 16>>) -> array<u32, 8> {
// Initialize hash values
var h: array<u32, 8> = array<u32, 8>(
0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au,
0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u
);
// Message schedule
var w: array<u32, 64>;
for (var i = 0u; i < 16u; i++) {
w[i] = (*msg)[i];
}
for (var i = 16u; i < 64u; i++) {
w[i] = gamma1(w[i-2u]) + w[i-7u] + gamma0(w[i-15u]) + w[i-16u];
}
// Compression
var a = h[0]; var b = h[1]; var c = h[2]; var d = h[3];
var e = h[4]; var f = h[5]; var g = h[6]; var hh = h[7];
for (var i = 0u; i < 64u; i++) {
let t1 = hh + sigma1(e) + ch(e, f, g) + K[i] + w[i];
let t2 = sigma0(a) + maj(a, b, c);
hh = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
h[0] = h[0] + a; h[1] = h[1] + b; h[2] = h[2] + c; h[3] = h[3] + d;
h[4] = h[4] + e; h[5] = h[5] + f; h[6] = h[6] + g; h[7] = h[7] + hh;
return h;
}
// Compute SHA256 of the key (16 bytes) to get channel hash
fn sha256_key(key: array<u32, 4>) -> u32 {
var msg: array<u32, 16>;
// Key bytes (16 bytes = 4 words)
msg[0] = key[0];
msg[1] = key[1];
msg[2] = key[2];
msg[3] = key[3];
// Padding: 0x80 followed by zeros
msg[4] = 0x80000000u;
for (var i = 5u; i < 14u; i++) {
msg[i] = 0u;
}
// Length: 128 bits
msg[14] = 0u;
msg[15] = 128u;
let hash = sha256_block(&msg);
// Return first byte of hash (big-endian)
return hash[0] >> 24u;
}
// HMAC-SHA256 for MAC verification
// Key is 16 bytes (4 words), padded to 32 bytes with zeros for MeshCore
// Returns first 2 bytes of HMAC (as u32 in high 16 bits)
fn hmac_sha256_mac(key: array<u32, 4>, ciphertext_len: u32) -> u32 {
// HMAC: H((K' ^ opad) || H((K' ^ ipad) || message))
// K' is 64 bytes (32 bytes key + 32 bytes zero padding for MeshCore, then padded to 64)
// ipad = 0x36 repeated, opad = 0x5c repeated
// Build padded key (64 bytes = 16 words)
// MeshCore uses 32-byte secret: 16-byte key + 16 zero bytes
var k_pad: array<u32, 16>;
k_pad[0] = key[0];
k_pad[1] = key[1];
k_pad[2] = key[2];
k_pad[3] = key[3];
for (var i = 4u; i < 16u; i++) {
k_pad[i] = 0u;
}
// Inner hash: SHA256((K' ^ ipad) || message)
// First block: K' ^ ipad (64 bytes)
var inner_block: array<u32, 16>;
for (var i = 0u; i < 16u; i++) {
inner_block[i] = k_pad[i] ^ 0x36363636u;
}
// Initialize hash state with first block
var h: array<u32, 8> = sha256_block(&inner_block);
// Process ciphertext blocks (continuing from h state)
let ciphertext_words = params.ciphertext_words;
var word_idx = 0u;
// Process full 64-byte blocks of ciphertext
while (word_idx + 16u <= ciphertext_words) {
var block: array<u32, 16>;
for (var i = 0u; i < 16u; i++) {
block[i] = ciphertext[word_idx + i];
}
h = sha256_block_continue(&block, h);
word_idx = word_idx + 16u;
}
// Final block with remaining ciphertext + padding
var final_block: array<u32, 16>;
var remaining = ciphertext_words - word_idx;
for (var i = 0u; i < 16u; i++) {
if (i < remaining) {
final_block[i] = ciphertext[word_idx + i];
} else if (i == remaining) {
// Add 0x80 padding
final_block[i] = 0x80000000u;
} else {
final_block[i] = 0u;
}
}
// Add length (64 bytes of ipad + ciphertext length)
let total_bits = 512u + params.ciphertext_len_bits;
if (remaining < 14u) {
final_block[14] = 0u;
final_block[15] = total_bits;
h = sha256_block_continue(&final_block, h);
} else {
// Need extra block for length
h = sha256_block_continue(&final_block, h);
var len_block: array<u32, 16>;
for (var i = 0u; i < 14u; i++) {
len_block[i] = 0u;
}
len_block[14] = 0u;
len_block[15] = total_bits;
h = sha256_block_continue(&len_block, h);
}
let inner_hash = h;
// Outer hash: SHA256((K' ^ opad) || inner_hash)
var outer_block: array<u32, 16>;
for (var i = 0u; i < 16u; i++) {
outer_block[i] = k_pad[i] ^ 0x5c5c5c5cu;
}
h = sha256_block(&outer_block);
// Second block: inner_hash (32 bytes) + padding
var hash_block: array<u32, 16>;
for (var i = 0u; i < 8u; i++) {
hash_block[i] = inner_hash[i];
}
hash_block[8] = 0x80000000u;
for (var i = 9u; i < 14u; i++) {
hash_block[i] = 0u;
}
hash_block[14] = 0u;
hash_block[15] = 512u + 256u; // 64 bytes opad + 32 bytes inner hash
h = sha256_block_continue(&hash_block, h);
// Return first 2 bytes (high 16 bits of first word)
return h[0] & 0xFFFF0000u;
}
// SHA256 block computation continuing from existing state
fn sha256_block_continue(msg: ptr<function, array<u32, 16>>, h_in: array<u32, 8>) -> array<u32, 8> {
var h = h_in;
// Message schedule
var w: array<u32, 64>;
for (var i = 0u; i < 16u; i++) {
w[i] = (*msg)[i];
}
for (var i = 16u; i < 64u; i++) {
w[i] = gamma1(w[i-2u]) + w[i-7u] + gamma0(w[i-15u]) + w[i-16u];
}
// Compression
var a = h[0]; var b = h[1]; var c = h[2]; var d = h[3];
var e = h[4]; var f = h[5]; var g = h[6]; var hh = h[7];
for (var i = 0u; i < 64u; i++) {
let t1 = hh + sigma1(e) + ch(e, f, g) + K[i] + w[i];
let t2 = sigma0(a) + maj(a, b, c);
hh = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
h[0] = h[0] + a; h[1] = h[1] + b; h[2] = h[2] + c; h[3] = h[3] + d;
h[4] = h[4] + e; h[5] = h[5] + f; h[6] = h[6] + g; h[7] = h[7] + hh;
return h;
}
// Process a single candidate and record match if found
fn process_candidate(name_idx: u32) {
// Generate message for this room name
var msg: array<u32, 16>;
let valid = index_to_room_name(name_idx, params.name_length, &msg);
if (!valid) {
return;
}
// Compute SHA256("#roomname") - this gives us the key
let key_hash = sha256_block(&msg);
// Take first 16 bytes (4 words) as the key
var key: array<u32, 4>;
key[0] = key_hash[0];
key[1] = key_hash[1];
key[2] = key_hash[2];
key[3] = key_hash[3];
// Compute SHA256(key) to get channel hash
let channel_hash = sha256_key(key);
// Check if channel hash matches target
if (channel_hash != params.target_channel_hash) {
return;
}
// Channel hash matches - verify MAC if enabled
if (params.verify_mac == 1u) {
let computed_mac = hmac_sha256_mac(key, params.ciphertext_len_bits);
if (computed_mac != params.target_mac) {
return;
}
}
// Found a match - record the index
let match_idx = atomicAdd(&match_count, 1u);
if (match_idx < 1024u) { // Limit stored matches
match_indices[match_idx] = name_idx;
}
}
// Each thread processes 16 candidates to amortize thread overhead
const CANDIDATES_PER_THREAD: u32 = 16u;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let base_idx = global_id.x * CANDIDATES_PER_THREAD;
for (var i = 0u; i < CANDIDATES_PER_THREAD; i++) {
let idx = base_idx + i;
if (idx >= params.batch_size) {
return;
}
let name_idx = params.batch_offset + idx;
process_candidate(name_idx);
}
}
`;
async init(): Promise<boolean> {
if (!navigator.gpu) {
console.warn('WebGPU not supported');
return false;
}
try {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
console.warn('No GPU adapter found');
return false;
}
this.device = await adapter.requestDevice();
// Create bind group layout
this.bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
],
});
// Create persistent buffers
this.paramsBuffer = this.device.createBuffer({
size: 32, // 8 u32s
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.matchCountBuffer = this.device.createBuffer({
size: 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
this.matchIndicesBuffer = this.device.createBuffer({
size: 1024 * 4, // Max 1024 matches per batch
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
// Double-buffered staging buffers
for (let i = 0; i < 2; i++) {
this.matchCountReadBuffers[i] = this.device.createBuffer({
size: 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
this.matchIndicesReadBuffers[i] = this.device.createBuffer({
size: 1024 * 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
}
// Create pipeline
const shaderModule = this.device.createShaderModule({
code: this.shaderCode,
});
const pipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [this.bindGroupLayout],
});
this.pipeline = this.device.createComputePipeline({
layout: pipelineLayout,
compute: {
module: shaderModule,
entryPoint: 'main',
},
});
return true;
} catch (e) {
console.error('WebGPU initialization failed:', e);
return false;
}
}
isAvailable(): boolean {
return this.device !== null && this.pipeline !== null;
}
// Convert room name index to actual room name string (delegates to core)
indexToRoomName(idx: number, length: number): string | null {
return indexToRoomName(length, idx);
}
// Count valid names for a given length (delegates to core)
countNamesForLength(len: number): number {
return countNamesForLength(len);
}
async runBatch(
targetChannelHash: number,
nameLength: number,
batchOffset: number,
batchSize: number,
ciphertextHex?: string,
targetMacHex?: string,
): Promise<number[]> {
if (
!this.device ||
!this.pipeline ||
!this.bindGroupLayout ||
!this.paramsBuffer ||
!this.matchCountBuffer ||
!this.matchIndicesBuffer ||
!this.matchCountReadBuffers[0] ||
!this.matchCountReadBuffers[1] ||
!this.matchIndicesReadBuffers[0] ||
!this.matchIndicesReadBuffers[1]
) {
throw new Error('GPU not initialized');
}
// Swap to alternate staging buffer set (double-buffering)
const readBufferIdx = this.currentReadBufferIndex;
this.currentReadBufferIndex = 1 - this.currentReadBufferIndex;
const matchCountReadBuffer = this.matchCountReadBuffers[readBufferIdx]!;
const matchIndicesReadBuffer = this.matchIndicesReadBuffers[readBufferIdx]!;
// Parse ciphertext if provided
const verifyMac = ciphertextHex && targetMacHex ? 1 : 0;
let ciphertextWords: Uint32Array;
let ciphertextLenBits = 0;
let targetMac = 0;
if (verifyMac) {
// Convert hex to bytes then to big-endian u32 words
const ciphertextBytes = new Uint8Array(ciphertextHex!.length / 2);
for (let i = 0; i < ciphertextBytes.length; i++) {
ciphertextBytes[i] = parseInt(ciphertextHex!.substr(i * 2, 2), 16);
}
ciphertextLenBits = ciphertextBytes.length * 8;
// Pad to 4-byte boundary and convert to big-endian u32
const paddedLen = Math.ceil(ciphertextBytes.length / 4) * 4;
const padded = new Uint8Array(paddedLen);
padded.set(ciphertextBytes);
ciphertextWords = new Uint32Array(paddedLen / 4);
for (let i = 0; i < ciphertextWords.length; i++) {
ciphertextWords[i] =
(padded[i * 4] << 24) |
(padded[i * 4 + 1] << 16) |
(padded[i * 4 + 2] << 8) |
padded[i * 4 + 3];
}
// Parse target MAC (2 bytes in high 16 bits)
const macByte0 = parseInt(targetMacHex!.substr(0, 2), 16);
const macByte1 = parseInt(targetMacHex!.substr(2, 2), 16);
targetMac = (macByte0 << 24) | (macByte1 << 16);
} else {
ciphertextWords = new Uint32Array([0]); // Dummy
}
// Resize ciphertext buffer if needed (marks bind group as dirty)
const requiredCiphertextSize = Math.max(ciphertextWords.length * 4, 4);
if (!this.ciphertextBuffer || this.ciphertextBufferSize < requiredCiphertextSize) {
if (this.ciphertextBuffer) {
this.ciphertextBuffer.destroy();
}
this.ciphertextBuffer = this.device.createBuffer({
size: requiredCiphertextSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
this.ciphertextBufferSize = requiredCiphertextSize;
this.bindGroupDirty = true;
}
// Write params
const paramsData = new Uint32Array([
targetChannelHash,
batchOffset,
nameLength,
batchSize,
targetMac,
ciphertextWords.length,
ciphertextLenBits,
verifyMac,
]);
this.device.queue.writeBuffer(this.paramsBuffer, 0, paramsData);
// Write ciphertext
this.device.queue.writeBuffer(this.ciphertextBuffer, 0, ciphertextWords as Uint32Array<ArrayBuffer>);
// Reset match count (reuse static zero buffer)
this.device.queue.writeBuffer(this.matchCountBuffer, 0, GpuBruteForce.ZERO_DATA);
// Recreate bind group only if needed
if (this.bindGroupDirty || !this.bindGroup) {
this.bindGroup = this.device.createBindGroup({
layout: this.bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this.paramsBuffer } },
{ binding: 1, resource: { buffer: this.matchCountBuffer } },
{ binding: 2, resource: { buffer: this.matchIndicesBuffer } },
{ binding: 3, resource: { buffer: this.ciphertextBuffer } },
],
});
this.bindGroupDirty = false;
}
// Create command encoder
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.pipeline);
passEncoder.setBindGroup(0, this.bindGroup);
// Each workgroup has 256 threads, each processing 16 candidates
const CANDIDATES_PER_THREAD = 16;
passEncoder.dispatchWorkgroups(Math.ceil(batchSize / (256 * CANDIDATES_PER_THREAD)));
passEncoder.end();
// Copy results to current staging buffers
commandEncoder.copyBufferToBuffer(this.matchCountBuffer, 0, matchCountReadBuffer, 0, 4);
commandEncoder.copyBufferToBuffer(
this.matchIndicesBuffer,
0,
matchIndicesReadBuffer,
0,
1024 * 4,
);
// Submit
this.device.queue.submit([commandEncoder.finish()]);
// Read results from current staging buffers
await matchCountReadBuffer.mapAsync(GPUMapMode.READ);
const matchCount = new Uint32Array(matchCountReadBuffer.getMappedRange())[0];
matchCountReadBuffer.unmap();
const matches: number[] = [];
if (matchCount > 0) {
await matchIndicesReadBuffer.mapAsync(GPUMapMode.READ);
const indices = new Uint32Array(matchIndicesReadBuffer.getMappedRange());
for (let i = 0; i < Math.min(matchCount, 1024); i++) {
matches.push(indices[i]);
}
matchIndicesReadBuffer.unmap();
}
return matches;
}
destroy(): void {
// Clean up persistent buffers
this.paramsBuffer?.destroy();
this.matchCountBuffer?.destroy();
this.matchIndicesBuffer?.destroy();
this.ciphertextBuffer?.destroy();
// Clean up double-buffered staging buffers
this.matchCountReadBuffers[0]?.destroy();
this.matchCountReadBuffers[1]?.destroy();
this.matchIndicesReadBuffers[0]?.destroy();
this.matchIndicesReadBuffers[1]?.destroy();
this.paramsBuffer = null;
this.matchCountBuffer = null;
this.matchIndicesBuffer = null;
this.ciphertextBuffer = null;
this.ciphertextBufferSize = 0;
this.matchCountReadBuffers = [null, null];
this.matchIndicesReadBuffers = [null, null];
this.currentReadBufferIndex = 0;
this.bindGroup = null;
this.bindGroupDirty = true;
if (this.device) {
this.device.destroy();
this.device = null;
}
this.pipeline = null;
this.bindGroupLayout = null;
}
}
/**
* Check if WebGPU is supported in the current browser.
*/
export function isWebGpuSupported(): boolean {
return typeof navigator !== 'undefined' && 'gpu' in navigator;
}

View File

@@ -0,0 +1,56 @@
/**
* MeshCore Cracker - Standalone library for cracking MeshCore GroupText packets
*
* @example
* ```typescript
* import { GroupTextCracker } from 'meshcore-cracker';
*
* const cracker = new GroupTextCracker();
*
* // Optional: load wordlist for dictionary attack
* await cracker.loadWordlist('/words_alpha.txt');
*
* const result = await cracker.crack(packetHex, {
* maxLength: 6,
* useTimestampFilter: true,
* useUtf8Filter: true,
* }, (progress) => {
* console.log(`${progress.percent.toFixed(1)}% - ETA: ${progress.etaSeconds}s`);
* });
*
* if (result.found) {
* console.log(`Room: #${result.roomName}`);
* console.log(`Message: ${result.decryptedMessage}`);
* }
*
* cracker.destroy();
* ```
*/
// Main cracker class
export { GroupTextCracker } from './cracker';
// Types
export type {
CrackOptions,
CrackResult,
ProgressReport,
ProgressCallback,
DecodedPacket,
} from './types';
// Utility exports for advanced usage
export {
deriveKeyFromRoomName,
getChannelHash,
verifyMac,
isTimestampValid,
isValidUtf8,
indexToRoomName,
roomNameToIndex,
countNamesForLength,
PUBLIC_ROOM_NAME,
PUBLIC_KEY,
} from './core';
export { isWebGpuSupported } from './gpu-bruteforce';

View File

@@ -0,0 +1,110 @@
/**
* Options for configuring the cracking process.
*/
export interface CrackOptions {
/**
* Maximum room name length to search (default: 8).
* Longer names exponentially increase search time.
*/
maxLength?: number;
/**
* Filter results by timestamp validity (default: true).
* When enabled, rejects results where the decrypted timestamp
* is more than 30 days old.
*/
useTimestampFilter?: boolean;
/**
* Filter results by UTF-8 validity (default: true).
* When enabled, rejects results containing invalid UTF-8 sequences.
*/
useUtf8Filter?: boolean;
/**
* Resume cracking from a specific room name position.
* Useful for resuming interrupted searches.
*/
startFrom?: string;
}
/**
* Progress information reported during cracking.
*/
export interface ProgressReport {
/** Total candidates checked so far */
checked: number;
/** Total candidates to check */
total: number;
/** Progress percentage (0-100) */
percent: number;
/** Current cracking rate in keys/second */
rateKeysPerSec: number;
/** Estimated time remaining in seconds */
etaSeconds: number;
/** Time elapsed since start in seconds */
elapsedSeconds: number;
/** Current room name length being tested */
currentLength: number;
/** Current room name position being tested */
currentPosition: string;
/** Current phase of cracking */
phase: 'public-key' | 'wordlist' | 'bruteforce';
}
/**
* Callback function for progress updates.
* Called approximately 5 times per second during cracking.
*/
export type ProgressCallback = (report: ProgressReport) => void;
/**
* Result of a cracking operation.
*/
export interface CrackResult {
/** Whether a matching room name was found */
found: boolean;
/** The room name (without '#' prefix) if found */
roomName?: string;
/** The derived encryption key (hex) if found */
key?: string;
/** The decrypted message content if found */
decryptedMessage?: string;
/** Whether the operation was aborted */
aborted?: boolean;
/** Position to resume from if aborted or failed */
resumeFrom?: string;
/** Error message if an error occurred */
error?: string;
}
/**
* Decoded packet information extracted from a MeshCore GroupText packet.
*/
export interface DecodedPacket {
/** Channel hash (1 byte, hex) */
channelHash: string;
/** Encrypted ciphertext (hex) */
ciphertext: string;
/** MAC for verification (2 bytes, hex) */
cipherMac: string;
/** Whether this is a GroupText packet */
isGroupText: boolean;
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM"],
"types": ["@webgpu/types"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"meshcore-cracker": "file:../references/standalone_cracker",
"meshcore-cracker": "file:./lib/meshcore-cracker",
"nosleep.js": "^0.12.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -44,6 +44,7 @@
"../references/standalone_cracker": {
"name": "meshcore-cracker",
"version": "1.0.0",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@michaelhart/meshcore-decoder": "^0.2.7",
@@ -56,6 +57,486 @@
"typescript": "^5.7.2"
}
},
"lib/meshcore-cracker": {
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@michaelhart/meshcore-decoder": "^0.2.7",
"crypto-js": "^4.2.0"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@webgpu/types": "^0.1.68",
"esbuild": "^0.24.2",
"typescript": "^5.7.2"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/android-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/android-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/android-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/darwin-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/darwin-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/freebsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/freebsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-loong64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-mips64el": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-riscv64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-s390x": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/linux-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/netbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/netbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/openbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/openbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/sunos-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/win32-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/win32-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/@esbuild/win32-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"lib/meshcore-cracker/node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.24.2",
"@esbuild/android-arm": "0.24.2",
"@esbuild/android-arm64": "0.24.2",
"@esbuild/android-x64": "0.24.2",
"@esbuild/darwin-arm64": "0.24.2",
"@esbuild/darwin-x64": "0.24.2",
"@esbuild/freebsd-arm64": "0.24.2",
"@esbuild/freebsd-x64": "0.24.2",
"@esbuild/linux-arm": "0.24.2",
"@esbuild/linux-arm64": "0.24.2",
"@esbuild/linux-ia32": "0.24.2",
"@esbuild/linux-loong64": "0.24.2",
"@esbuild/linux-mips64el": "0.24.2",
"@esbuild/linux-ppc64": "0.24.2",
"@esbuild/linux-riscv64": "0.24.2",
"@esbuild/linux-s390x": "0.24.2",
"@esbuild/linux-x64": "0.24.2",
"@esbuild/netbsd-arm64": "0.24.2",
"@esbuild/netbsd-x64": "0.24.2",
"@esbuild/openbsd-arm64": "0.24.2",
"@esbuild/openbsd-x64": "0.24.2",
"@esbuild/sunos-x64": "0.24.2",
"@esbuild/win32-arm64": "0.24.2",
"@esbuild/win32-ia32": "0.24.2",
"@esbuild/win32-x64": "0.24.2"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
@@ -991,6 +1472,39 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@michaelhart/meshcore-decoder": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@michaelhart/meshcore-decoder/-/meshcore-decoder-0.2.7.tgz",
"integrity": "sha512-a3zNbqeACibYy7XlMx6F2fMfg8FT+mP7lcYCmr8EvQk2MvBw7Qs7+bV2DAnwTf+51IRZqFEPCq55GB0scsw1WA==",
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^2.3.0",
"chalk": "^4.1.2",
"commander": "^12.0.0",
"crypto-js": "^4.2.0"
},
"bin": {
"meshcore-decoder": "dist/cli.js"
}
},
"node_modules/@michaelhart/meshcore-decoder/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@noble/ed25519": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-2.3.0.tgz",
"integrity": "sha512-M7dvXL2B92/M7dw9+gzuydL8qn/jiqNHaoR3Q+cb1q1GHV7uwE17WCyFMG+Y+TZb5izcaXk5TdJRrDUxHXL78A==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2154,6 +2668,13 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2306,6 +2827,13 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@webgpu/types": {
"version": "0.1.68",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.68.tgz",
"integrity": "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -2581,6 +3109,37 @@
"node": ">=18"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/check-error": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
@@ -2648,6 +3207,24 @@
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2677,6 +3254,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -3168,6 +3751,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -3516,7 +4108,7 @@
}
},
"node_modules/meshcore-cracker": {
"resolved": "../references/standalone_cracker",
"resolved": "lib/meshcore-cracker",
"link": true
},
"node_modules/micromatch": {
@@ -4288,6 +4880,18 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",

View File

@@ -20,7 +20,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"meshcore-cracker": "file:../references/standalone_cracker",
"meshcore-cracker": "file:./lib/meshcore-cracker",
"nosleep.js": "^0.12.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",