torn-crack

Simple Cracking Helper

// ==UserScript==
// @name         torn-crack
// @namespace    torn-crack
// @version      0.0.7.4
// @description  Simple Cracking Helper
// @author       SirAua [3785905]
// @match        *://www.torn.com/page.php?sid=crimes*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license      mit
// ==/UserScript==

(function () {
    'use strict';

    if (window.CRACK_INJECTED) return;
    window.CRACK_INJECTED = true;

    const debug = false;
    const UPDATE_INTERVAL = 800;
    const MAX_SUG = 8;
    const MAX_WORDS_PER_LENGTH = 2000;
    const MIN_LENGTH = 4;
    const MAX_LENGTH = 10;

    const wordlistUrl = 'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Common-Credentials/xato-net-10-million-passwords-1000000.txt';

    const CACHE_KEY_PREFIX = 'crack_dict_';
    const CACHE_TIMESTAMP_KEY = 'crack_dict_timestamp';
    const CACHE_EXPIRY_HOURS = 24;

    let dict = [];
    let dictLoaded = false;

    function crackLog(...args) {
        if (debug) console.log('[Crack]', ...args);
    }

    function clearLocalDictCache() {
        for (let len = MIN_LENGTH; len <= MAX_LENGTH; len++) {
            localStorage.removeItem(CACHE_KEY_PREFIX + len);
        }
        localStorage.removeItem(CACHE_TIMESTAMP_KEY);
        crackLog('Cleared cached dictionary and timestamp from localStorage');
    }

    async function loadDict() {
        if (dictLoaded) return;

        const timestamp = parseInt(localStorage.getItem(CACHE_TIMESTAMP_KEY), 10);
        const now = Date.now();
        const expiry = CACHE_EXPIRY_HOURS * 60 * 60 * 1000;

        if (timestamp && now - timestamp < expiry) {
            crackLog('Attempting to load dictionary from localStorage...');
            let hasData = false;
            dict = [];

            for (let len = MIN_LENGTH; len <= MAX_LENGTH; len++) {
                const raw = localStorage.getItem(`${CACHE_KEY_PREFIX}${len}`);
                if (raw) {
                    try {
                        dict[len] = JSON.parse(raw);
                        hasData = true;
                    } catch {
                        localStorage.removeItem(`${CACHE_KEY_PREFIX}${len}`);
                    }
                }
            }

            if (hasData) {
                dictLoaded = true;
                crackLog('Dictionary loaded from localStorage');
                return;
            }
        } else {
            crackLog('Cache expired or missing');
            clearLocalDictCache();
        }

        crackLog('Downloading dictionary...');
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'get',
                url: wordlistUrl,
                timeout: 30000,
                ontimeout: reject,
                onerror: reject,
                onload: (res) => {
                    const lines = res.responseText.split(/\r?\n/).map(w => w.trim().toUpperCase());
                    dict = [];

                    for (const word of lines) {
                        if (!/^[A-Z0-9_.]+$/.test(word)) continue;
                        const len = word.length;
                        if (len < MIN_LENGTH || len > MAX_LENGTH) continue;
                        if (!dict[len]) dict[len] = [];
                        if (dict[len].length < MAX_WORDS_PER_LENGTH) {
                            dict[len].push(word);
                        }
                    }

                    for (let len = MIN_LENGTH; len <= MAX_LENGTH; len++) {
                        if (dict[len]) {
                            localStorage.setItem(`${CACHE_KEY_PREFIX}${len}`, JSON.stringify(dict[len]));
                        }
                    }

                    localStorage.setItem(CACHE_TIMESTAMP_KEY, now.toString());
                    dictLoaded = true;
                    crackLog('Dictionary downloaded and cached');
                    resolve();
                },
            });
        });
    }

    async function suggest(pat) {
        const len = pat.length;
        if (len < MIN_LENGTH || len > MAX_LENGTH) return [];

        if (!dict[len]) {
            const raw = localStorage.getItem(`${CACHE_KEY_PREFIX}${len}`);
            if (!raw) return [];
            try {
                dict[len] = JSON.parse(raw);
            } catch {
                crackLog(`Failed to parse dictionary chunk for len=${len}`);
                return [];
            }
        }

        const worker = new Worker(URL.createObjectURL(new Blob([`
            self.onmessage = function(e) {
                const { dictChunk, pattern, max } = e.data;
                const regex = new RegExp('^' + pattern.replace(/[*]/g, '.') + '$');
                const out = [];
                for (const word of dictChunk) {
                    if (regex.test(word)) out.push(word);
                    if (out.length >= max) break;
                }
                self.postMessage(out);
            };
        `], { type: 'application/javascript' })));

        return new Promise((resolve) => {
            worker.onmessage = (e) => {
                worker.terminate();
                resolve([...new Set(e.data)]);
            };
            worker.postMessage({ dictChunk: dict[len], pattern: pat.toUpperCase(), max: MAX_SUG });
        });
    }

    function prependPanelToRow(row, pat) {
        const existing = row.querySelector('.__crackhelp_panel');
        //if pattern didnt change just return
        if (existing && existing.dataset.pattern === pat && existing.querySelector('span')) return;
        if (existing) existing.remove();

        const panel = document.createElement('div');
        panel.className = '__crackhelp_panel';
        panel.dataset.pattern = pat;
        panel.style.cssText = 'background: #000; font-size: 10px; text-align: center; position: absolute; z-index: 9999;';
        const listDiv = document.createElement('div');
        listDiv.style.cssText = 'margin-top: 2px;';
        panel.appendChild(listDiv);
        row.prepend(panel);

        async function updateSuggestions() {
            const sugs = await suggest(pat);
            listDiv.innerHTML = '';
            sugs.forEach(word => {
                const sp = document.createElement('span');
                sp.style.cssText = 'padding:2px; color: #00ff00;';
                sp.textContent = word;
                listDiv.appendChild(sp);
            });
            if (sugs.length === 0) {
                const none = document.createElement('span');
                none.textContent = '(no matches)';
                none.style.color = '#a00';
                listDiv.appendChild(none);
            }
        }

        loadDict().then(updateSuggestions);
    }

    function scanCrimePage() {
        if (!location.href.endsWith('cracking')) return;

        const currentCrime = document.querySelector('[class^="currentCrime"]');
        if (!currentCrime) return;

        const container = currentCrime.querySelector('[class^="virtualList"]');
        if (!container) return;

        const crimeOptions = container.querySelectorAll('[class^="crimeOptionWrapper"]');
        crackLog('Scanning crime options:', crimeOptions.length);

        for (const crimeOption of crimeOptions) {
            let patText = '';
            //explicitly look for charSlot and exlude charSlotDummy
            const charSlots = crimeOption.querySelectorAll('[class^="charSlot"]:not([class*="charSlotDummy"])');
            for (const charSlot of charSlots) {
                let char = charSlot.textContent.trim();
                patText += char ? char.toUpperCase() : '*';
            }

            if (!/^[*]+$/.test(patText)) prependPanelToRow(crimeOption, patText);
        }
    }

    function injectRefreshButton() {
        if (!location.href.endsWith('cracking')) return;
        if (document.getElementById('__crack_reset_btn')) return;

        const appHeader = document.querySelector('[class^="appHeaderDelimiter"]');
        if (!appHeader) return;

        const btn = document.createElement('button');
        btn.id = '__crack_reset_btn';
        btn.textContent = 'Bruteforce characters to show suggestions! (Click this message to clear the wordlist cache)';
        btn.style.cssText = 'background: #000; color: #0f0; font-size: 10px; text-align: left; z-index: 9999; cursor: pointer;';
        btn.onclick = () => {
            clearLocalDictCache();
            location.reload();
        };

        appHeader.appendChild(btn);
    }

    scanCrimePage();
    setInterval(scanCrimePage, UPDATE_INTERVAL);
    setInterval(injectRefreshButton, UPDATE_INTERVAL);
})();