mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 18:15:47 +00:00
Device clocks on MeshCore nodes are wildly inaccurate (off by hours or epoch-near values like 4). The channel messages endpoint was using sender_timestamp as part of the deduplication key, which could cause messages to fail deduplication or incorrectly collide. Changed dedupe key from sender:timestamp to sender:hash, which is the correct unique identifier for a transmission. Also added TIMESTAMP-AUDIT.md documenting all device timestamp usage.
654 lines
23 KiB
HTML
Executable File
654 lines
23 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MeshCore Cracker Testbed</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: #1a1a2e;
|
|
color: #eee;
|
|
}
|
|
h1 { color: #00d4ff; margin-bottom: 5px; }
|
|
.subtitle { color: #888; margin-bottom: 20px; }
|
|
label { display: block; margin: 10px 0 5px; color: #aaa; }
|
|
input[type="text"], input[type="number"] {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
background: #16213e;
|
|
color: #fff;
|
|
font-family: monospace;
|
|
}
|
|
.options {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
margin: 15px 0;
|
|
padding: 15px;
|
|
background: #16213e;
|
|
border-radius: 4px;
|
|
}
|
|
.option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.option input[type="checkbox"] {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
.option input[type="number"] {
|
|
width: 80px;
|
|
}
|
|
.buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 15px 0;
|
|
}
|
|
button {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
#crackBtn { background: #00d4ff; color: #000; }
|
|
#continueBtn { background: #ff9f00; color: #000; }
|
|
#abortBtn { background: #ff4757; color: #fff; }
|
|
#clearBtn { background: #444; color: #fff; }
|
|
.status {
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
font-family: monospace;
|
|
}
|
|
.status.info { background: #16213e; border-left: 3px solid #00d4ff; }
|
|
.status.success { background: #1e3a2f; border-left: 3px solid #2ed573; }
|
|
.status.error { background: #3a1e1e; border-left: 3px solid #ff4757; }
|
|
.progress {
|
|
background: #16213e;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
font-family: monospace;
|
|
font-size: 13px;
|
|
}
|
|
.progress-bar {
|
|
height: 8px;
|
|
background: #333;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin: 10px 0;
|
|
}
|
|
.progress-bar-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #00d4ff, #00ff88);
|
|
transition: width 0.3s;
|
|
}
|
|
.results {
|
|
background: #16213e;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
}
|
|
.result-item {
|
|
padding: 10px;
|
|
margin: 5px 0;
|
|
background: #1a1a2e;
|
|
border-radius: 4px;
|
|
border-left: 3px solid #2ed573;
|
|
}
|
|
.result-item.false-positive {
|
|
border-left-color: #ff9f00;
|
|
}
|
|
.result-label { color: #888; font-size: 12px; }
|
|
.result-value { font-family: monospace; margin-top: 3px; word-break: break-all; }
|
|
.log {
|
|
background: #0a0a15;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
margin: 10px 0;
|
|
}
|
|
.log-entry { padding: 2px 0; color: #888; }
|
|
.log-entry.match { color: #2ed573; }
|
|
.log-entry.error { color: #ff4757; }
|
|
.gpu-status {
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
margin-left: 10px;
|
|
}
|
|
.gpu-status.available { background: #1e3a2f; color: #2ed573; }
|
|
.gpu-status.unavailable { background: #3a2e1e; color: #ff9f00; }
|
|
.preset-btn {
|
|
padding: 6px 12px;
|
|
background: #2a2a4a;
|
|
color: #aaa;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
.preset-btn:hover { background: #3a3a5a; color: #fff; }
|
|
#autotestBtn {
|
|
background: #2ed573;
|
|
color: #000;
|
|
font-size: 16px;
|
|
padding: 14px 32px;
|
|
margin-bottom: 15px;
|
|
}
|
|
#autotestBtn:disabled { background: #555; color: #999; }
|
|
#autotestPanel {
|
|
background: #16213e;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
display: none;
|
|
}
|
|
#autotestPanel h3 { margin: 0 0 10px; color: #00d4ff; }
|
|
.at-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 6px 8px;
|
|
margin: 3px 0;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 13px;
|
|
background: #1a1a2e;
|
|
}
|
|
.at-row .at-badge {
|
|
flex-shrink: 0;
|
|
width: 60px;
|
|
text-align: center;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-weight: bold;
|
|
font-size: 11px;
|
|
}
|
|
.at-badge.pass { background: #2ed573; color: #000; }
|
|
.at-badge.fail { background: #ff4757; color: #fff; }
|
|
.at-badge.run { background: #ff9f00; color: #000; }
|
|
.at-badge.wait { background: #444; color: #888; }
|
|
.at-row .at-name { flex: 1; }
|
|
.at-row .at-time { color: #888; font-size: 12px; flex-shrink: 0; }
|
|
#autotestSummary {
|
|
margin-top: 10px;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
display: none;
|
|
}
|
|
#autotestSummary.all-pass { background: #1e3a2f; color: #2ed573; }
|
|
#autotestSummary.has-fail { background: #3a1e1e; color: #ff4757; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>MeshCore Cracker Testbed <span id="gpuStatus" class="gpu-status">Checking...</span></h1>
|
|
<p class="subtitle">Test GPU vs CPU cracking with arbitrary packets</p>
|
|
|
|
<button id="autotestBtn" disabled>Autotest (waiting for wordlist...)</button>
|
|
<div id="autotestPanel">
|
|
<h3>Autotest Results</h3>
|
|
<div id="autotestRows"></div>
|
|
<div id="autotestSummary"></div>
|
|
</div>
|
|
|
|
<label for="packet">Packet (hex):</label>
|
|
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
|
<button type="button" class="preset-btn" data-packet="1502D36386F3BD71BEEB075B6BBBF1D747144E2383B11C6637531C1294DC66658DE02263558087C1AA966E5E26852CCC4688A7F387101D88C5B6997927A1127063D09501A132CC">Unknown</button>
|
|
<button type="button" class="preset-btn" data-packet="1503A6DB9DCA168D85E6C402C0E0AF422FA633B990C6946DF20842B7F03E16531B2573B76BEE7FE1">Wordlist (#bot)</button>
|
|
<button type="button" class="preset-btn" data-packet="150064F62ADF41BE89A05AC9BCFE04BA85D54BD402EDB6D973DE6F7B1811FB1FA8FD8888882429C57B8FC622C9E613F60890C46AF4">len=6 gibberish (#uetfwf)</button>
|
|
<button type="button" class="preset-btn" data-packet="1503e653aeb374a86670654924ffbf1e535b549856b6caf87aff479a3d34c6cf9c29dc2e04ccd8be1e3a1af4c8c8a4f54ca18211a56466515fe29fa53e62d7290fde841fc42cc90682c864a80191725c71ea12fd979ed35b1c82915f459d54ce655d715e20fbe15fdd7835c60da6100914c360d281af168729b5b9d60189b6cb888869ca5306f4a9">Two-word key</button>
|
|
</div>
|
|
<input type="text" id="packet" placeholder="15001234abcd...">
|
|
|
|
<div style="font-size: 12px; color: #888; margin: 10px 0;">Wordlist: <span id="wordCount">0</span> words</div>
|
|
|
|
<div class="options">
|
|
<div class="option">
|
|
<input type="checkbox" id="useDictionary" checked>
|
|
<label for="useDictionary">Dictionary first</label>
|
|
</div>
|
|
<div class="option">
|
|
<input type="checkbox" id="useTimestamp" checked>
|
|
<label for="useTimestamp">Timestamp filter</label>
|
|
</div>
|
|
<div class="option">
|
|
<input type="checkbox" id="useUtf8" checked>
|
|
<label for="useUtf8">UTF-8 filter</label>
|
|
</div>
|
|
<div class="option">
|
|
<input type="checkbox" id="forceCpu">
|
|
<label for="forceCpu">Force CPU</label>
|
|
</div>
|
|
<div class="option">
|
|
<input type="checkbox" id="useTwoWordCombinations">
|
|
<label for="useTwoWordCombinations">Two-word combos (experimental)</label>
|
|
</div>
|
|
<div class="option">
|
|
<label for="maxLength">Max length:</label>
|
|
<input type="number" id="maxLength" value="6" min="1" max="12">
|
|
</div>
|
|
<div class="option">
|
|
<label for="gpuDispatchMs">GPU dispatch (ms):</label>
|
|
<input type="number" id="gpuDispatchMs" value="1000" min="100" max="30000" style="width: 100px;">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="buttons">
|
|
<button id="crackBtn">Crack</button>
|
|
<button id="continueBtn" disabled>Continue (find next)</button>
|
|
<button id="abortBtn" disabled>Abort</button>
|
|
<button id="clearBtn">Clear</button>
|
|
</div>
|
|
|
|
<div id="statusArea"></div>
|
|
|
|
<div id="progressArea" style="display: none;">
|
|
<div class="progress">
|
|
<div>Phase: <span id="phase">-</span> | Length: <span id="currentLength">-</span></div>
|
|
<div>Position: <span id="currentPosition">-</span></div>
|
|
<div>Checked: <span id="checked">0</span> / <span id="total">0</span></div>
|
|
<div class="progress-bar"><div class="progress-bar-fill" id="progressBar" style="width: 0%"></div></div>
|
|
<div>Progress: <span id="percent">0</span>% | Rate: <span id="rate">0</span> keys/s | ETA: <span id="eta">-</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="resultsArea" class="results" style="display: none;">
|
|
<h3>Results</h3>
|
|
<div id="resultsList"></div>
|
|
</div>
|
|
|
|
<div class="log" id="log"></div>
|
|
|
|
<script src="meshcore_cracker.min.js"></script>
|
|
<script>
|
|
const { GroupTextCracker } = MeshCoreCracker;
|
|
|
|
let cracker = null;
|
|
let lastResult = null;
|
|
let results = [];
|
|
let wordlist = [];
|
|
|
|
// Elements
|
|
const packetInput = document.getElementById('packet');
|
|
const wordCountSpan = document.getElementById('wordCount');
|
|
const useDictionary = document.getElementById('useDictionary');
|
|
const useTimestamp = document.getElementById('useTimestamp');
|
|
const useUtf8 = document.getElementById('useUtf8');
|
|
const forceCpu = document.getElementById('forceCpu');
|
|
const maxLength = document.getElementById('maxLength');
|
|
|
|
const crackBtn = document.getElementById('crackBtn');
|
|
const continueBtn = document.getElementById('continueBtn');
|
|
const abortBtn = document.getElementById('abortBtn');
|
|
const clearBtn = document.getElementById('clearBtn');
|
|
const statusArea = document.getElementById('statusArea');
|
|
const progressArea = document.getElementById('progressArea');
|
|
const resultsArea = document.getElementById('resultsArea');
|
|
const resultsList = document.getElementById('resultsList');
|
|
const logArea = document.getElementById('log');
|
|
const gpuStatus = document.getElementById('gpuStatus');
|
|
|
|
// Initialize cracker and check GPU
|
|
function initCracker() {
|
|
if (cracker) cracker.destroy();
|
|
cracker = new GroupTextCracker();
|
|
|
|
const hasGpu = cracker.isGpuAvailable();
|
|
gpuStatus.textContent = hasGpu ? 'GPU Available' : 'CPU Only';
|
|
gpuStatus.className = 'gpu-status ' + (hasGpu ? 'available' : 'unavailable');
|
|
|
|
return cracker;
|
|
}
|
|
|
|
initCracker();
|
|
|
|
// Load wordlist from GitHub
|
|
async function loadWordlist() {
|
|
wordCountSpan.textContent = 'loading...';
|
|
try {
|
|
const response = await fetch('https://raw.githubusercontent.com/dwyl/english-words/master/words_alpha.txt');
|
|
const text = await response.text();
|
|
wordlist = text.split(/\r?\n/)
|
|
.map(w => w.trim().toLowerCase())
|
|
.filter(w => w.length > 0 && /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(w) && !w.includes('--'));
|
|
wordCountSpan.textContent = wordlist.length.toLocaleString();
|
|
log(`Loaded ${wordlist.length.toLocaleString()} words from GitHub`);
|
|
} catch (err) {
|
|
wordCountSpan.textContent = 'failed';
|
|
log(`Failed to load wordlist: ${err.message}`, 'error');
|
|
}
|
|
}
|
|
loadWordlist();
|
|
|
|
// Preset packet buttons
|
|
document.querySelectorAll('.preset-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
packetInput.value = btn.dataset.packet;
|
|
});
|
|
});
|
|
|
|
function log(msg, type = '') {
|
|
const entry = document.createElement('div');
|
|
entry.className = 'log-entry ' + type;
|
|
entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
|
logArea.appendChild(entry);
|
|
logArea.scrollTop = logArea.scrollHeight;
|
|
}
|
|
|
|
function setStatus(msg, type = 'info') {
|
|
statusArea.innerHTML = `<div class="status ${type}">${msg}</div>`;
|
|
}
|
|
|
|
function formatTime(seconds) {
|
|
if (seconds < 60) return `${seconds.toFixed(0)}s`;
|
|
if (seconds < 3600) return `${(seconds / 60).toFixed(1)}m`;
|
|
return `${(seconds / 3600).toFixed(1)}h`;
|
|
}
|
|
|
|
function formatRate(rate) {
|
|
if (rate >= 1e9) return `${(rate / 1e9).toFixed(2)} Gkeys/s`;
|
|
if (rate >= 1e6) return `${(rate / 1e6).toFixed(2)} Mkeys/s`;
|
|
if (rate >= 1e3) return `${(rate / 1e3).toFixed(2)} Kkeys/s`;
|
|
return `${rate.toFixed(0)} keys/s`;
|
|
}
|
|
|
|
function updateProgress(p) {
|
|
document.getElementById('phase').textContent = p.phase;
|
|
document.getElementById('currentLength').textContent = p.currentLength;
|
|
document.getElementById('currentPosition').textContent = p.currentPosition || '-';
|
|
document.getElementById('checked').textContent = p.checked.toLocaleString();
|
|
document.getElementById('total').textContent = p.total.toLocaleString();
|
|
document.getElementById('percent').textContent = p.percent.toFixed(2);
|
|
document.getElementById('progressBar').style.width = `${p.percent}%`;
|
|
document.getElementById('rate').textContent = formatRate(p.rateKeysPerSec);
|
|
document.getElementById('eta').textContent = formatTime(p.etaSeconds);
|
|
}
|
|
|
|
function addResult(result) {
|
|
results.push(result);
|
|
resultsArea.style.display = 'block';
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'result-item';
|
|
item.innerHTML = `
|
|
<div class="result-label">Room Name</div>
|
|
<div class="result-value">#${result.roomName}</div>
|
|
<div class="result-label">Decrypted Message</div>
|
|
<div class="result-value">${result.decryptedMessage || '(empty)'}</div>
|
|
<div class="result-label">Key</div>
|
|
<div class="result-value">${result.key}</div>
|
|
<div class="result-label">Resume Info</div>
|
|
<div class="result-value">type: ${result.resumeType}, from: ${result.resumeFrom}</div>
|
|
`;
|
|
resultsList.appendChild(item);
|
|
}
|
|
|
|
async function doCrack(resumeFrom = null, resumeType = null) {
|
|
const packet = packetInput.value.trim();
|
|
if (!packet) {
|
|
setStatus('Please enter a packet hex string', 'error');
|
|
return;
|
|
}
|
|
|
|
// Re-init cracker to ensure clean state
|
|
initCracker();
|
|
|
|
// Set wordlist if dictionary is enabled and loaded
|
|
if (useDictionary.checked && wordlist.length > 0) {
|
|
cracker.setWordlist(wordlist);
|
|
log(`Using dictionary with ${wordlist.length.toLocaleString()} words`);
|
|
if (document.getElementById('useTwoWordCombinations').checked) {
|
|
const shortWords = wordlist.filter(w => w.length <= 15);
|
|
const pairCount = shortWords.length * shortWords.length;
|
|
log(`Two-word combinations enabled: ${shortWords.length.toLocaleString()} short words (≤15 chars)`);
|
|
log(`Estimated pairs to check: ~${pairCount.toLocaleString()} (filtered by length)`);
|
|
}
|
|
}
|
|
|
|
const gpuDispatchMsValue = parseInt(document.getElementById('gpuDispatchMs').value) || 1000;
|
|
const useTwoWordCombinations = document.getElementById('useTwoWordCombinations').checked;
|
|
const options = {
|
|
maxLength: parseInt(maxLength.value) || 6,
|
|
useTimestampFilter: useTimestamp.checked,
|
|
useUtf8Filter: useUtf8.checked,
|
|
forceCpu: forceCpu.checked,
|
|
useDictionary: useDictionary.checked,
|
|
useTwoWordCombinations: useTwoWordCombinations,
|
|
gpuDispatchMs: gpuDispatchMsValue,
|
|
};
|
|
|
|
if (resumeFrom) {
|
|
options.startFrom = resumeFrom;
|
|
options.startFromType = resumeType || 'bruteforce';
|
|
log(`Resuming from "${resumeFrom}" (${options.startFromType})`);
|
|
}
|
|
|
|
crackBtn.disabled = true;
|
|
continueBtn.disabled = true;
|
|
abortBtn.disabled = false;
|
|
progressArea.style.display = 'block';
|
|
|
|
const startMsg = resumeFrom ? `Continuing search...` : `Starting crack (${forceCpu.checked ? 'CPU' : 'GPU'})...`;
|
|
setStatus(startMsg, 'info');
|
|
log(startMsg);
|
|
|
|
try {
|
|
const result = await cracker.crack(packet, options, updateProgress);
|
|
|
|
lastResult = result;
|
|
|
|
if (result.found) {
|
|
setStatus(`Found room: #${result.roomName}`, 'success');
|
|
log(`MATCH: #${result.roomName} -> "${result.decryptedMessage}"`, 'match');
|
|
addResult(result);
|
|
continueBtn.disabled = false;
|
|
} else if (result.aborted) {
|
|
setStatus(`Aborted at position: ${result.resumeFrom}`, 'info');
|
|
log(`Aborted. Resume from: ${result.resumeFrom} (${result.resumeType})`);
|
|
continueBtn.disabled = false;
|
|
} else {
|
|
setStatus('No match found in search space', 'error');
|
|
log('Search complete - no match found');
|
|
}
|
|
|
|
if (result.error) {
|
|
setStatus(`Error: ${result.error}`, 'error');
|
|
log(`Error: ${result.error}`, 'error');
|
|
}
|
|
|
|
} catch (err) {
|
|
setStatus(`Error: ${err.message}`, 'error');
|
|
log(`Exception: ${err.message}`, 'error');
|
|
}
|
|
|
|
crackBtn.disabled = false;
|
|
abortBtn.disabled = true;
|
|
}
|
|
|
|
crackBtn.addEventListener('click', () => {
|
|
results = [];
|
|
resultsList.innerHTML = '';
|
|
resultsArea.style.display = 'none';
|
|
lastResult = null;
|
|
doCrack();
|
|
});
|
|
|
|
continueBtn.addEventListener('click', () => {
|
|
if (lastResult && lastResult.resumeFrom) {
|
|
doCrack(lastResult.resumeFrom, lastResult.resumeType);
|
|
}
|
|
});
|
|
|
|
abortBtn.addEventListener('click', () => {
|
|
if (cracker) {
|
|
cracker.abort();
|
|
log('Abort requested...');
|
|
}
|
|
});
|
|
|
|
clearBtn.addEventListener('click', () => {
|
|
results = [];
|
|
resultsList.innerHTML = '';
|
|
resultsArea.style.display = 'none';
|
|
progressArea.style.display = 'none';
|
|
statusArea.innerHTML = '';
|
|
logArea.innerHTML = '';
|
|
lastResult = null;
|
|
continueBtn.disabled = true;
|
|
log('Cleared');
|
|
});
|
|
|
|
// ── Autotest ──────────────────────────────────────────────────────
|
|
|
|
const AUTOTEST_PACKETS = {
|
|
wordlist: '1503A6DB9DCA168D85E6C402C0E0AF422FA633B990C6946DF20842B7F03E16531B2573B76BEE7FE1',
|
|
twoWord: '1503e653aeb374a86670654924ffbf1e535b549856b6caf87aff479a3d34c6cf9c29dc2e04ccd8be1e3a1af4c8c8a4f54ca18211a56466515fe29fa53e62d7290fde841fc42cc90682c864a80191725c71ea12fd979ed35b1c82915f459d54ce655d715e20fbe15fdd7835c60da6100914c360d281af168729b5b9d60189b6cb888869ca5306f4a9',
|
|
bruteForce: '150064F62ADF41BE89A05AC9BCFE04BA85D54BD402EDB6D973DE6F7B1811FB1FA8FD8888882429C57B8FC622C9E613F60890C46AF4',
|
|
};
|
|
|
|
const AUTOTEST_CASES = [
|
|
// [label, packetKey, expectedRoom, opts]
|
|
// expectedRoom: exact string to match, or null to just check found===true
|
|
...([1000, 5000, 10000].map(d => [`Wordlist #bot (dispatch ${d}ms)`, 'wordlist', 'bot',
|
|
{ useDictionary: true, useTwoWordCombinations: false, maxLength: 6, gpuDispatchMs: d }])),
|
|
...([1000, 5000, 10000].map(d => [`Two-word combo (dispatch ${d}ms)`, 'twoWord', 'hamradio',
|
|
{ useDictionary: true, useTwoWordCombinations: true, maxLength: 6, gpuDispatchMs: d }])),
|
|
...([1000, 5000, 10000].map(d => [`Brute #uetfwf (dispatch ${d}ms)`, 'bruteForce', 'uetfwf',
|
|
{ useDictionary: false, useTwoWordCombinations: false, maxLength: 6, gpuDispatchMs: d }])),
|
|
];
|
|
|
|
const autotestBtn = document.getElementById('autotestBtn');
|
|
const autotestPanel = document.getElementById('autotestPanel');
|
|
const autotestRows = document.getElementById('autotestRows');
|
|
const autotestSummary = document.getElementById('autotestSummary');
|
|
let autotestRunning = false;
|
|
|
|
// Enable button once wordlist is loaded
|
|
function checkAutotestReady() {
|
|
if (wordlist.length > 0) {
|
|
autotestBtn.disabled = false;
|
|
autotestBtn.textContent = 'Autotest';
|
|
}
|
|
}
|
|
// Poll until wordlist loads (loadWordlist is already running)
|
|
const _atReadyInterval = setInterval(() => {
|
|
if (wordlist.length > 0) {
|
|
clearInterval(_atReadyInterval);
|
|
checkAutotestReady();
|
|
}
|
|
}, 200);
|
|
|
|
function atBuildRows() {
|
|
autotestRows.innerHTML = '';
|
|
return AUTOTEST_CASES.map(([label], i) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'at-row';
|
|
row.innerHTML = `<span class="at-badge wait">WAIT</span><span class="at-name">${label}</span><span class="at-time"></span>`;
|
|
autotestRows.appendChild(row);
|
|
return row;
|
|
});
|
|
}
|
|
|
|
function atSetRow(row, status, timeStr) {
|
|
const badge = row.querySelector('.at-badge');
|
|
badge.className = 'at-badge ' + status;
|
|
badge.textContent = status.toUpperCase();
|
|
if (timeStr != null) row.querySelector('.at-time').textContent = timeStr;
|
|
}
|
|
|
|
async function runAutotest() {
|
|
if (autotestRunning) return;
|
|
autotestRunning = true;
|
|
autotestBtn.disabled = true;
|
|
autotestBtn.textContent = 'Running...';
|
|
autotestPanel.style.display = 'block';
|
|
autotestSummary.style.display = 'none';
|
|
|
|
const rows = atBuildRows();
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
for (let i = 0; i < AUTOTEST_CASES.length; i++) {
|
|
const [label, packetKey, expectedRoom, opts] = AUTOTEST_CASES[i];
|
|
atSetRow(rows[i], 'run', '');
|
|
|
|
const t0 = performance.now();
|
|
try {
|
|
// Fresh cracker per test
|
|
const tc = new GroupTextCracker();
|
|
if (opts.useDictionary) tc.setWordlist(wordlist);
|
|
|
|
const result = await tc.crack(AUTOTEST_PACKETS[packetKey], {
|
|
maxLength: opts.maxLength || 6,
|
|
useTimestampFilter: false,
|
|
useUtf8Filter: true,
|
|
forceCpu: false,
|
|
useDictionary: opts.useDictionary,
|
|
useTwoWordCombinations: opts.useTwoWordCombinations,
|
|
gpuDispatchMs: opts.gpuDispatchMs,
|
|
}, updateProgress);
|
|
|
|
tc.destroy();
|
|
|
|
const elapsed = ((performance.now() - t0) / 1000).toFixed(1) + 's';
|
|
|
|
const ok = result.found && (expectedRoom === null || result.roomName === expectedRoom);
|
|
if (ok) {
|
|
passed++;
|
|
atSetRow(rows[i], 'pass', `${elapsed} -> #${result.roomName}`);
|
|
} else {
|
|
failed++;
|
|
const reason = !result.found
|
|
? `not found${result.error ? ': ' + result.error : ''}`
|
|
: `expected #${expectedRoom}, got #${result.roomName}`;
|
|
atSetRow(rows[i], 'fail', `${elapsed} (${reason})`);
|
|
}
|
|
} catch (err) {
|
|
failed++;
|
|
const elapsed = ((performance.now() - t0) / 1000).toFixed(1) + 's';
|
|
atSetRow(rows[i], 'fail', `${elapsed} (${err.message})`);
|
|
}
|
|
}
|
|
|
|
autotestSummary.style.display = 'block';
|
|
autotestSummary.textContent = `${passed}/${AUTOTEST_CASES.length} passed, ${failed} failed`;
|
|
autotestSummary.className = failed === 0 ? 'all-pass' : 'has-fail';
|
|
|
|
autotestBtn.disabled = false;
|
|
autotestBtn.textContent = 'Autotest';
|
|
autotestRunning = false;
|
|
log(`Autotest complete: ${passed}/${AUTOTEST_CASES.length} passed`);
|
|
}
|
|
|
|
autotestBtn.addEventListener('click', runAutotest);
|
|
|
|
// ── End Autotest ─────────────────────────────────────────────────
|
|
|
|
// Ready message
|
|
log('Ready. Select a preset packet above or paste your own.');
|
|
</script>
|
|
</body>
|
|
</html>
|