mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
657 lines
22 KiB
TypeScript
657 lines
22 KiB
TypeScript
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
import { GroupTextCracker, type ProgressReport } from 'meshcore-hashtag-cracker';
|
|
import NoSleep from 'nosleep.js';
|
|
import type { RawPacket, Channel } from '../types';
|
|
import { api } from '../api';
|
|
import { toast } from './ui/sonner';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* Extract the payload from a raw packet hex string, skipping header and path.
|
|
* Returns the payload as a hex string, or null if malformed.
|
|
*/
|
|
function extractPayload(packetHex: string): string | null {
|
|
if (packetHex.length < 4) return null; // Need at least 2 bytes
|
|
|
|
try {
|
|
const header = parseInt(packetHex.slice(0, 2), 16);
|
|
const routeType = header & 0x03;
|
|
let offset = 2; // 1 byte = 2 hex chars
|
|
|
|
// Skip transport codes if present (TRANSPORT_FLOOD=0, TRANSPORT_DIRECT=3)
|
|
if (routeType === 0x00 || routeType === 0x03) {
|
|
if (packetHex.length < offset + 8) return null; // Need 4 more bytes
|
|
offset += 8; // 4 bytes = 8 hex chars
|
|
}
|
|
|
|
// Get path length
|
|
if (packetHex.length < offset + 2) return null;
|
|
const pathLength = parseInt(packetHex.slice(offset, offset + 2), 16);
|
|
offset += 2;
|
|
|
|
// Skip path data
|
|
const pathBytes = pathLength * 2; // hex chars
|
|
if (packetHex.length < offset + pathBytes) return null;
|
|
offset += pathBytes;
|
|
|
|
// Rest is payload
|
|
return packetHex.slice(offset);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
interface CrackedRoom {
|
|
roomName: string;
|
|
key: string;
|
|
packetId: number;
|
|
message: string;
|
|
crackedAt: number;
|
|
}
|
|
|
|
interface QueueItem {
|
|
packet: RawPacket;
|
|
attempts: number;
|
|
lastAttemptLength: number;
|
|
status: 'pending' | 'cracking' | 'cracked' | 'failed';
|
|
}
|
|
|
|
interface CrackerPanelProps {
|
|
packets: RawPacket[];
|
|
channels: Channel[];
|
|
onChannelCreate: (name: string, key: string) => Promise<void>;
|
|
onRunningChange?: (running: boolean) => void;
|
|
visible?: boolean;
|
|
}
|
|
|
|
export function CrackerPanel({
|
|
packets,
|
|
channels,
|
|
onChannelCreate,
|
|
onRunningChange,
|
|
visible = false,
|
|
}: CrackerPanelProps) {
|
|
const [isRunning, setIsRunning] = useState(false);
|
|
const [maxLength, setMaxLength] = useState(6);
|
|
const [retryFailedAtNextLength, setRetryFailedAtNextLength] = useState(false);
|
|
const [decryptHistorical, setDecryptHistorical] = useState(true);
|
|
const [turboMode, setTurboMode] = useState(false);
|
|
const [twoWordMode, setTwoWordMode] = useState(false);
|
|
const [progress, setProgress] = useState<ProgressReport | null>(null);
|
|
const [queue, setQueue] = useState<Map<number, QueueItem>>(new Map());
|
|
const [crackedRooms, setCrackedRooms] = useState<CrackedRoom[]>([]);
|
|
const [wordlistLoaded, setWordlistLoaded] = useState(false);
|
|
const [gpuAvailable, setGpuAvailable] = useState<boolean | null>(null);
|
|
const [undecryptedPacketCount, setUndecryptedPacketCount] = useState<number | null>(null);
|
|
const [skippedDuplicates, setSkippedDuplicates] = useState(0);
|
|
|
|
const crackerRef = useRef<GroupTextCracker | null>(null);
|
|
const noSleepRef = useRef<NoSleep | null>(null);
|
|
const isRunningRef = useRef(false);
|
|
const abortedRef = useRef(false);
|
|
const isProcessingRef = useRef(false);
|
|
const queueRef = useRef<Map<number, QueueItem>>(new Map());
|
|
const retryFailedRef = useRef(false);
|
|
const maxLengthRef = useRef(6);
|
|
const decryptHistoricalRef = useRef(true);
|
|
const turboModeRef = useRef(false);
|
|
const twoWordModeRef = useRef(false);
|
|
const undecryptedIdsRef = useRef<Set<number>>(new Set());
|
|
const seenPayloadsRef = useRef<Set<string>>(new Set());
|
|
const existingChannelKeysRef = useRef<Set<string>>(new Set());
|
|
|
|
// Initialize cracker and NoSleep
|
|
useEffect(() => {
|
|
const cracker = new GroupTextCracker();
|
|
crackerRef.current = cracker;
|
|
setGpuAvailable(cracker.isGpuAvailable());
|
|
|
|
const noSleep = new NoSleep();
|
|
noSleepRef.current = noSleep;
|
|
|
|
return () => {
|
|
cracker.destroy();
|
|
crackerRef.current = null;
|
|
noSleep.disable();
|
|
noSleepRef.current = null;
|
|
};
|
|
}, []);
|
|
|
|
// Load wordlist dynamically when panel becomes visible for the first time
|
|
useEffect(() => {
|
|
if (!visible || wordlistLoaded) return;
|
|
|
|
import('meshcore-hashtag-cracker/wordlist')
|
|
.then(({ ENGLISH_WORDLIST }) => {
|
|
if (crackerRef.current) {
|
|
crackerRef.current.setWordlist(ENGLISH_WORDLIST);
|
|
setWordlistLoaded(true);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error('Failed to load wordlist:', err);
|
|
toast.error('Failed to load wordlist', {
|
|
description: 'Cracking will not be available',
|
|
});
|
|
});
|
|
}, [visible, wordlistLoaded]);
|
|
|
|
// Fetch undecrypted packet count
|
|
useEffect(() => {
|
|
const fetchCount = () => {
|
|
api
|
|
.getUndecryptedPacketCount()
|
|
.then(({ count }) => setUndecryptedPacketCount(count))
|
|
.catch(() => setUndecryptedPacketCount(null));
|
|
};
|
|
fetchCount();
|
|
// Refresh hourly (this is just for display; not critical to be up-to-date)
|
|
const interval = setInterval(fetchCount, 3600000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
// Get existing channel keys for filtering (memoized to avoid recreating on every render)
|
|
const existingChannelKeys = useMemo(
|
|
() => new Set(channels.map((c) => c.key.toUpperCase())),
|
|
[channels]
|
|
);
|
|
|
|
useEffect(() => {
|
|
existingChannelKeysRef.current = existingChannelKeys;
|
|
}, [existingChannelKeys]);
|
|
|
|
// Filter packets to only undecrypted GROUP_TEXT
|
|
const undecryptedGroupText = packets.filter(
|
|
(p) => p.payload_type === 'GROUP_TEXT' && !p.decrypted
|
|
);
|
|
|
|
// Update queue when packets change (deduplicated by payload)
|
|
// Note: We intentionally depend on .length only to avoid re-running on every array identity change
|
|
useEffect(() => {
|
|
let newSkipped = 0;
|
|
|
|
setQueue((prev) => {
|
|
const newQueue = new Map(prev);
|
|
let changed = false;
|
|
|
|
for (const packet of undecryptedGroupText) {
|
|
if (!newQueue.has(packet.id)) {
|
|
// Extract payload and check for duplicates
|
|
const payload = extractPayload(packet.data);
|
|
if (payload && seenPayloadsRef.current.has(payload)) {
|
|
// Skip - we already have a packet with this payload queued
|
|
newSkipped++;
|
|
continue;
|
|
}
|
|
|
|
// Track this payload as seen
|
|
if (payload) {
|
|
seenPayloadsRef.current.add(payload);
|
|
}
|
|
|
|
newQueue.set(packet.id, {
|
|
packet,
|
|
attempts: 0,
|
|
lastAttemptLength: 0,
|
|
status: 'pending',
|
|
});
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
queueRef.current = newQueue;
|
|
return newQueue;
|
|
}
|
|
return prev;
|
|
});
|
|
|
|
if (newSkipped > 0) {
|
|
setSkippedDuplicates((prev) => prev + newSkipped);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [undecryptedGroupText.length]);
|
|
|
|
// Keep refs in sync with state
|
|
useEffect(() => {
|
|
queueRef.current = queue;
|
|
}, [queue]);
|
|
|
|
useEffect(() => {
|
|
retryFailedRef.current = retryFailedAtNextLength;
|
|
}, [retryFailedAtNextLength]);
|
|
|
|
useEffect(() => {
|
|
maxLengthRef.current = maxLength;
|
|
}, [maxLength]);
|
|
|
|
useEffect(() => {
|
|
decryptHistoricalRef.current = decryptHistorical;
|
|
}, [decryptHistorical]);
|
|
|
|
useEffect(() => {
|
|
turboModeRef.current = turboMode;
|
|
}, [turboMode]);
|
|
|
|
useEffect(() => {
|
|
twoWordModeRef.current = twoWordMode;
|
|
}, [twoWordMode]);
|
|
|
|
// Keep undecrypted IDs ref in sync - used to skip packets already decrypted by other means
|
|
useEffect(() => {
|
|
undecryptedIdsRef.current = new Set(undecryptedGroupText.map((p) => p.id));
|
|
}, [undecryptedGroupText]);
|
|
|
|
// Notify parent of running state changes
|
|
useEffect(() => {
|
|
onRunningChange?.(isRunning);
|
|
}, [isRunning, onRunningChange]);
|
|
|
|
// Stats (cracking count is implicit - if progress is shown, we're cracking one)
|
|
const pendingCount = Array.from(queue.values()).filter((q) => q.status === 'pending').length;
|
|
const crackedCount = Array.from(queue.values()).filter((q) => q.status === 'cracked').length;
|
|
const failedCount = Array.from(queue.values()).filter((q) => q.status === 'failed').length;
|
|
|
|
// Process next packet in queue
|
|
const processNext = useCallback(async () => {
|
|
// Prevent concurrent processing
|
|
if (isProcessingRef.current) return;
|
|
if (!crackerRef.current || !isRunningRef.current) return;
|
|
|
|
const currentQueue = queueRef.current;
|
|
|
|
// Find next pending packet
|
|
let nextItem: QueueItem | null = null;
|
|
let nextId: number | null = null;
|
|
|
|
for (const [id, item] of currentQueue.entries()) {
|
|
if (item.status === 'pending') {
|
|
nextItem = item;
|
|
nextId = id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no pending and retry option is enabled, pick the failed one with lowest lastAttemptLength
|
|
if (!nextItem && retryFailedRef.current) {
|
|
const failedItems = Array.from(currentQueue.entries()).filter(
|
|
([, item]) => item.status === 'failed' && item.lastAttemptLength < 10 // Hard cap at length 10
|
|
);
|
|
if (failedItems.length > 0) {
|
|
// Sort by lastAttemptLength ascending and pick the first (lowest)
|
|
failedItems.sort((a, b) => a[1].lastAttemptLength - b[1].lastAttemptLength);
|
|
[nextId, nextItem] = failedItems[0];
|
|
}
|
|
}
|
|
|
|
if (!nextItem || nextId === null) {
|
|
// Nothing to process right now, but keep running and check again later
|
|
if (isRunningRef.current) {
|
|
setTimeout(() => processNext(), 1000);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check if this packet is still undecrypted - it may have been decrypted
|
|
// by historical decrypt when we cracked another packet from the same channel
|
|
if (!undecryptedIdsRef.current.has(nextId)) {
|
|
// Already decrypted by other means, remove from queue and continue
|
|
setQueue((prev) => {
|
|
const updated = new Map(prev);
|
|
updated.delete(nextId);
|
|
return updated;
|
|
});
|
|
if (isRunningRef.current) {
|
|
setTimeout(() => processNext(), 10);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Lock processing
|
|
isProcessingRef.current = true;
|
|
|
|
const currentMaxLength = maxLengthRef.current;
|
|
const isRetry = nextItem.lastAttemptLength > 0;
|
|
const targetLength = isRetry ? nextItem.lastAttemptLength + 1 : currentMaxLength;
|
|
|
|
try {
|
|
const result = await crackerRef.current.crack(
|
|
nextItem.packet.data,
|
|
{
|
|
maxLength: targetLength,
|
|
useSenderFilter: true,
|
|
useTimestampFilter: true,
|
|
useUtf8Filter: true,
|
|
useTwoWordCombinations: twoWordModeRef.current,
|
|
...(turboModeRef.current && { gpuDispatchMs: 10000 }),
|
|
// For retries, skip dictionary and shorter lengths - we already checked those
|
|
...(isRetry && {
|
|
useDictionary: false,
|
|
useTwoWordCombinations: false,
|
|
startingLength: targetLength,
|
|
}),
|
|
},
|
|
(prog) => {
|
|
setProgress(prog);
|
|
}
|
|
);
|
|
|
|
if (abortedRef.current) {
|
|
abortedRef.current = false;
|
|
isProcessingRef.current = false;
|
|
setProgress(null);
|
|
return;
|
|
}
|
|
|
|
if (result.found && result.roomName && result.key) {
|
|
// Success!
|
|
setQueue((prev) => {
|
|
const updated = new Map(prev);
|
|
const item = updated.get(nextId!);
|
|
if (item) {
|
|
updated.set(nextId!, {
|
|
...item,
|
|
status: 'cracked',
|
|
attempts: item.attempts + 1,
|
|
lastAttemptLength: targetLength,
|
|
});
|
|
}
|
|
return updated;
|
|
});
|
|
|
|
const newRoom: CrackedRoom = {
|
|
roomName: result.roomName,
|
|
key: result.key,
|
|
packetId: nextId!,
|
|
message: result.decryptedMessage || '',
|
|
crackedAt: Date.now(),
|
|
};
|
|
setCrackedRooms((prev) => [...prev, newRoom]);
|
|
|
|
// Auto-add channel if not already exists
|
|
const keyUpper = result.key.toUpperCase();
|
|
if (!existingChannelKeysRef.current.has(keyUpper)) {
|
|
try {
|
|
const channelName = '#' + result.roomName;
|
|
await onChannelCreate(channelName, result.key);
|
|
// Optionally decrypt any other historical packets with this newly discovered key
|
|
// This prevents wasting cracking cycles on packets from the same channel
|
|
if (decryptHistoricalRef.current) {
|
|
await api.decryptHistoricalPackets({
|
|
key_type: 'channel',
|
|
channel_name: channelName,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to create channel or decrypt historical:', err);
|
|
toast.error('Failed to save cracked channel', {
|
|
description:
|
|
err instanceof Error ? err.message : 'Channel discovered but could not be saved',
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// Failed
|
|
setQueue((prev) => {
|
|
const updated = new Map(prev);
|
|
const item = updated.get(nextId!);
|
|
if (item) {
|
|
updated.set(nextId!, {
|
|
...item,
|
|
status: 'failed',
|
|
attempts: item.attempts + 1,
|
|
lastAttemptLength: targetLength,
|
|
});
|
|
}
|
|
return updated;
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Cracking error:', err);
|
|
setQueue((prev) => {
|
|
const updated = new Map(prev);
|
|
const item = updated.get(nextId!);
|
|
if (item) {
|
|
updated.set(nextId!, {
|
|
...item,
|
|
status: 'failed',
|
|
attempts: item.attempts + 1,
|
|
lastAttemptLength: targetLength,
|
|
});
|
|
}
|
|
return updated;
|
|
});
|
|
}
|
|
|
|
// Unlock processing
|
|
isProcessingRef.current = false;
|
|
setProgress(null);
|
|
|
|
// Continue processing if still running
|
|
if (isRunningRef.current) {
|
|
setTimeout(() => processNext(), 100);
|
|
}
|
|
}, [onChannelCreate]);
|
|
|
|
// Start/stop handlers
|
|
const handleStart = () => {
|
|
if (!gpuAvailable) {
|
|
toast.error('WebGPU not available', {
|
|
description: 'Cracking requires Chrome 113+ or Edge 113+ with WebGPU support.',
|
|
});
|
|
return;
|
|
}
|
|
setIsRunning(true);
|
|
isRunningRef.current = true;
|
|
abortedRef.current = false;
|
|
noSleepRef.current?.enable();
|
|
processNext();
|
|
};
|
|
|
|
const handleStop = () => {
|
|
setIsRunning(false);
|
|
isRunningRef.current = false;
|
|
abortedRef.current = true;
|
|
crackerRef.current?.abort();
|
|
noSleepRef.current?.disable();
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full p-3 gap-3 bg-background border-t border-border overflow-auto">
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<div className="flex items-center gap-2">
|
|
<label htmlFor="cracker-max-length" className="text-sm text-muted-foreground">
|
|
Max Length:
|
|
</label>
|
|
<input
|
|
id="cracker-max-length"
|
|
type="number"
|
|
min={1}
|
|
max={10}
|
|
value={maxLength}
|
|
onChange={(e) => setMaxLength(Math.min(10, Math.max(1, parseInt(e.target.value) || 6)))}
|
|
className="w-14 px-2 py-1 text-sm bg-muted border border-border rounded"
|
|
/>
|
|
</div>
|
|
|
|
<label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={retryFailedAtNextLength}
|
|
onChange={(e) => setRetryFailedAtNextLength(e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
Retry failed at n+1
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={decryptHistorical}
|
|
onChange={(e) => setDecryptHistorical(e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
Decrypt historical packets if key found
|
|
</label>
|
|
{decryptHistorical && (
|
|
<span className="text-xs text-muted-foreground">
|
|
{undecryptedPacketCount !== null && undecryptedPacketCount > 0
|
|
? `(${undecryptedPacketCount.toLocaleString()} packets; messages will stream in as decrypted)`
|
|
: '(messages will stream in as decrypted)'}
|
|
</span>
|
|
)}
|
|
|
|
<label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={twoWordMode}
|
|
onChange={(e) => setTwoWordMode(e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
Try word pairs
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={turboMode}
|
|
onChange={(e) => setTurboMode(e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
Turbo mode (experimental)
|
|
</label>
|
|
</div>
|
|
|
|
<button
|
|
onClick={isRunning ? handleStop : handleStart}
|
|
disabled={!wordlistLoaded || gpuAvailable === false}
|
|
className={cn(
|
|
'w-48 px-4 py-1.5 rounded text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
isRunning
|
|
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
|
: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
'disabled:opacity-50 disabled:cursor-not-allowed'
|
|
)}
|
|
>
|
|
{isRunning
|
|
? 'Stop Search'
|
|
: gpuAvailable === false
|
|
? 'GPU Not Available'
|
|
: !wordlistLoaded
|
|
? 'Loading dictionary...'
|
|
: 'Find Rooms'}
|
|
</button>
|
|
|
|
{/* Status */}
|
|
<div className="flex gap-4 text-sm">
|
|
<span className="text-muted-foreground">
|
|
Pending: <span className="text-foreground font-medium">{pendingCount}</span>
|
|
</span>
|
|
<span className="text-muted-foreground">
|
|
Cracked: <span className="text-success font-medium">{crackedCount}</span>
|
|
</span>
|
|
<span className="text-muted-foreground">
|
|
Failed: <span className="text-destructive font-medium">{failedCount}</span>
|
|
</span>
|
|
{skippedDuplicates > 0 && (
|
|
<span className="text-muted-foreground">
|
|
Skipped (dup):{' '}
|
|
<span className="text-muted-foreground font-medium">{skippedDuplicates}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Progress */}
|
|
{progress && (
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
|
<span>
|
|
{progress.phase === 'wordlist'
|
|
? 'Dictionary'
|
|
: progress.phase === 'wordlist-pairs'
|
|
? 'Word Pairs'
|
|
: progress.phase === 'bruteforce'
|
|
? 'Bruteforce'
|
|
: 'Public Key'}
|
|
{progress.phase === 'bruteforce' && ` - Length ${progress.currentLength}`}:{' '}
|
|
{progress.currentPosition}
|
|
</span>
|
|
<span>
|
|
{progress.rateKeysPerSec >= 1e9
|
|
? `${(progress.rateKeysPerSec / 1e9).toFixed(2)} Gkeys/s`
|
|
: `${(progress.rateKeysPerSec / 1e6).toFixed(1)} Mkeys/s`}{' '}
|
|
• ETA:{' '}
|
|
{progress.etaSeconds < 60
|
|
? `${Math.round(progress.etaSeconds)}s`
|
|
: `${Math.round(progress.etaSeconds / 60)}m`}
|
|
</span>
|
|
</div>
|
|
<div
|
|
className="h-2 bg-muted rounded overflow-hidden"
|
|
role="progressbar"
|
|
aria-valuenow={Math.round(progress.percent)}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-label="Cracking progress"
|
|
>
|
|
<div
|
|
className="h-full bg-primary transition-all duration-200"
|
|
style={{ width: `${progress.percent}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* GPU status */}
|
|
{gpuAvailable === false && (
|
|
<div className="text-sm text-destructive" role="alert">
|
|
WebGPU not available. Cracking requires Chrome 113+ or Edge 113+.
|
|
</div>
|
|
)}
|
|
{!wordlistLoaded && gpuAvailable !== false && (
|
|
<div className="text-sm text-muted-foreground" role="status">
|
|
Loading wordlist...
|
|
</div>
|
|
)}
|
|
|
|
{/* Cracked rooms list */}
|
|
{crackedRooms.length > 0 && (
|
|
<div>
|
|
<div className="text-xs text-muted-foreground mb-1">Cracked Rooms:</div>
|
|
<div className="space-y-1">
|
|
{crackedRooms.map((room, i) => (
|
|
<div
|
|
key={i}
|
|
className="text-sm bg-success/10 border border-success/20 rounded px-2 py-1"
|
|
>
|
|
<span className="text-success font-medium">#{room.roomName}</span>
|
|
<span className="text-muted-foreground ml-2 text-xs">
|
|
"{room.message.slice(0, 50)}
|
|
{room.message.length > 50 ? '...' : ''}"
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<hr className="border-border" />
|
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
For unknown-keyed GroupText packets, this will attempt to dictionary attack, then brute
|
|
force payloads as they arrive, testing room names up to the specified length to discover
|
|
active rooms on the local mesh (GroupText packets may not be hashtag messages; we have no
|
|
way of knowing but try as if they are).
|
|
<strong> Retry failed at n+1</strong> will let the cracker return to the failed queue and
|
|
pick up messages it couldn't crack, attempting them at one longer length.
|
|
<strong> Try word pairs</strong> will also try every combination of two dictionary words
|
|
concatenated together (e.g. "hello" + "world" = "#helloworld") after the single-word
|
|
dictionary pass; this can substantially increase search time.
|
|
<strong> Decrypt historical</strong> will run an async job on any room name it finds to see
|
|
if any historically captured packets will decrypt with that key.
|
|
<strong> Turbo mode</strong> will push your GPU to the max (target dispatch time of 10s) and
|
|
may allow accelerated cracking and/or system instability.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|