// ==UserScript==
// @name torn-crime-crack-helper
// @namespace nodelore.torn.crack-helper
// @version 1.2.4
// @description Utilize password database to crack torn cracking crime.
// @author nodelore[2786679] NEvaldas[352097]
// @match https://www.torn.com/loader.php?sid=crimes*
// @grant GM_getValue
// @grant GM.getValue
// @grant GM_setValue
// @grant GM.setValue
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// Avoid duplicate injection
if (window.CRACK_HELPER_INJECTED) {
return;
}
window.CRACK_HELPER_INJECTED = true;
const cracker_record = {};
const filter_history = {};
const isMobile = () => {
return window.innerWidth <= 768;
};
let inPDA = false;
const PDAKey = "###PDA-APIKEY###";
if (PDAKey.charAt(0) !== "#") {
inPDA = true;
}
const http_get = (url, success, failed) => {
GM_xmlhttpRequest({
method: "get",
url: url,
timeout: 30000,
ontimeout: (err) => {
failed(err);
},
onerror: (err) => {
failed(err);
},
onload: (res) => {
success(res);
},
});
};
// ========================= Configuration==============================================================================================================================
const CRACKER_STATUS_KEY = "CRACKER_STATUS";
const defaultSel = isMobile() ? "100k" : "1m";
let CRACKER_SEL = localStorage.getItem(CRACKER_STATUS_KEY) || defaultSel;
const LIMIT = 10;
// add custom password list here, and set CRACKER_SEL to the one you want to choose
const PASSWORD_DATABASE = {
"10m":
"https://raw.githubusercontent.com/ignis-sec/Pwdb-Public/master/wordlists/ignis-10M.txt",
"1m": "https://raw.githubusercontent.com/ignis-sec/Pwdb-Public/master/wordlists/ignis-1M.txt",
"1m_alter":
"https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt",
"100k":
"https://raw.githubusercontent.com/ignis-sec/Pwdb-Public/master/wordlists/ignis-100K.txt",
"100k_alter":
"https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Common-Credentials/10-million-password-list-top-100000.txt",
"10k":
"https://raw.githubusercontent.com/ignis-sec/Pwdb-Public/master/wordlists/ignis-10K.txt",
"1k": "https://raw.githubusercontent.com/ignis-sec/Pwdb-Public/master/wordlists/ignis-1K.txt",
};
// =====================================================================================================================================================================
window.CRACK_HELPER_INJECTED = true;
if (GM) {
window.GM_getValue = GM.getValue;
window.GM_setValue = GM.setValue;
}
if (!PASSWORD_DATABASE[CRACKER_SEL]) {
console.log("Fail to fetch cracker password");
return;
}
const CRACKER_HELPER_KEY = "CRACKER_HELPER_STORAGE";
let cracker_helper = {
source: "",
data: [],
};
let titleInterval, updateInterval;
let is_injected = false;
const setCrackTitle = (title) => {
if (titleInterval) {
clearInterval(titleInterval);
}
titleInterval = setInterval(() => {
const titleQuery = "div[class*=titleBar___] div[class*=title___]";
if ($(titleQuery).length > 0) {
$(titleQuery).text(`CRACKING (${title})`);
clearInterval(titleInterval);
titleInterval = undefined;
}
}, 1000);
};
const fetch_action = (useCache = true) => {
setCrackTitle("Loading from network");
http_get(
PASSWORD_DATABASE[CRACKER_SEL],
(res) => {
const text = res.responseText;
cracker_helper.data = [];
text.split("\n").forEach((pwd) => {
cracker_helper.data.push(pwd.trim().replace("\n", ""));
});
cracker_helper.source = PASSWORD_DATABASE[CRACKER_SEL];
if (useCache) {
GM_setValue(CRACKER_HELPER_KEY, cracker_helper);
}
setCrackTitle("Loaded");
updatePage();
console.log("load cracker_helper from network:");
console.log(cracker_helper);
},
(res) => {
console.error(`error: ${res}`);
}
);
};
const insertSelector = () => {
let options = "";
for (let abbr in PASSWORD_DATABASE) {
options += `<option value="${abbr}">${abbr}</option>`;
}
const selector = $(`
<div class="cracker-helper-selector">
<label>Source:</label>
<select name="crackerSel">
${options}
</select>
</div>
`);
selector.find("select").val(CRACKER_SEL);
selector.find("select").change(function () {
CRACKER_SEL = $(this).val();
localStorage.setItem(CRACKER_STATUS_KEY, CRACKER_SEL);
$("div.cracker-helper-panel").each(function () {
$(this).remove();
});
fetch_action();
});
if ($("div.cracker-helper-selector").length == 0) {
$("h4[class*=heading___]").after(selector);
}
};
const addStyle = () => {
const styles = `
.cracker-helper-selector{
display: flex;
align-items: center;
font-size: 14px;
font-weight: bold;
}
.cracker-helper-selector select{
background: transparent;
text-align: center;
border: none;
}
.dark-mode .cracker-helper-selector select{
color: #F2F2F2 !important;
}
.dark-mode .cracker-helper-selector select option{
background: #333 !important;
color: #F2F2F2 !important;
}
.cracker-helper-panel{
width: 100%;
height: 30px;
background: #F2F2F2;
box-sizing: border-box;
display: flex;
padding: 5px;
border-bottom: 1px solid rgba(1, 1, 1, .1);
}
.cracker-helper-panel:hover{
background: #FFF;
}
.dark-mode .cracker-helper-panel{
background: rgba(1, 1, 1, .15) !important;
border-bottom: 1px solid #222 !important;
}
.dark-mode .cracker-helper-panel:hover{
background: rgba(1, 1, 1, .15) !important;
}
.cracker-current-status{
display: flex;
flex-flow: column nowrap;
border-right: 1px solid;
border-image-source: linear-gradient(180deg,transparent,#ddd 53%,transparent);
border-image-slice: 1;
box-sizing: border-box;
justify-content: center;
padding-left: 5px;
}
.dark-mode .cracker-current-status{
border-image-source: linear-gradient(180deg,transparent,#000,transparent);
}
.cracker-helper-panel-mobile .cracker-current-status{
fotn-size: 5px !important;
}
.cracker-current-status div{
width: 100%;
color: #000;
font: inherit;
color: #666;
}
.cracker-status-count{
color: #c66231 !important;
}
.cracker-current-result{
flex: 1;
display: flex;
align-items: center;
border-right: 1px solid;
border-image-source: linear-gradient(180deg,transparent,#ddd 53%,transparent);
border-image-slice: 1;
box-sizing: border-box;
padding-left: 5px;
font-size: 1.25em;
}
.dark-mode .cracker-current-result{
border-image-source: linear-gradient(180deg,transparent,#000,transparent);
}
.cracker-result-item{
border: 1px solid rgba(1, 1, 1, .1);
height: 34px;
line-height: 34px;
font-size: 100%;
text-align: center;
margin-left: 6px;
box-sizing: border-box;
}
.dark-mode .cracker-result-item{
border-color: #F2F2F266 !important;
}
.cracker-helper-panel-mobile .cracker-result-item{
margin-left: 3px !important;
}
.cracker-button-set{
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
padding-right: 5px;
}
.cracker-helper-panel-mobile .cracker-button-set{
flex-flow: column nowrap !important;
}
.cracker-button-set button{
text-align: center;
height: 24px;
line-height: 24px;
}
.cracker-helper-panel-mobile .cracker-button-set button{
width: 60px;
height: 18px !important;
line-height: 18px !important;
}
`;
const isTampermonkeyEnabled = typeof unsafeWindow !== "undefined";
if (isTampermonkeyEnabled) {
GM_addStyle(styles);
} else {
let style = document.createElement("style");
style.type = "text/css";
style.innerHTML = styles;
document.head.appendChild(style);
}
};
// under experiment
const findCommonCandidates = (arr, target) => {
if (arr.length === 0) {
return arr;
}
const target_poses = [];
for (let i = 0; i < target.length; i++) {
const c = target[i];
if (c == ".") {
target_poses.push(i);
}
}
const freqs = [];
for (let pos of target_poses) {
const freq = {};
for (let res of arr) {
const c = res[pos];
if (!freq[c]) {
freq[c] = 0;
}
freq[c] += 1;
}
const freq_list = Object.entries(freq);
freq_list.sort((a, b) => {
if (a[1] > b[1]) {
return -1;
} else if (a[1] < b[1]) {
return 1;
}
return 0;
});
freqs.push({
pos: pos,
char: freq_list[0][0],
count: freq_list[0][1],
});
}
freqs.sort((a, b) => {
if (a["count"] > b["count"]) {
return -1;
} else if (a["count"] < b["count"]) {
return 1;
}
return 0;
});
const res = [];
const highest = freqs[0];
const highest_pos = highest["char"];
const highest_char = highest["pos"];
for (let c of arr) {
if (c[highest_pos] === highest_char) {
res.unshift(c);
} else {
res.push(c);
}
}
return {
res,
highest_pos,
highest_char,
};
};
let global_index = 0;
const handleCrime = (
item,
extraInfo = undefined,
panel_index = undefined
) => {
let index = panel_index;
if (index) {
index = parseInt(index);
}
let target = "";
if (extraInfo) {
target = extraInfo;
} else {
item.find("div[class*=charSlot_]").each(function () {
const val = $(this).text().trim();
if (val == "") {
target += ".";
} else {
target += val;
}
});
}
target = target.replaceAll("ø", "0");
let targetRegex = new RegExp(`^${target}$`);
// console.log(`target Regex is ${targetRegex}, index is ${index}, globalIndex: ${global_index}`);
setCrackTitle("Calculating");
let result = cracker_helper.data.filter((item) => targetRegex.test(item));
if (result.length === 0 && target.length > 6) {
let found = false;
let splitIndex = 3;
// when regex match does not work, we will split the regex and try to find out result for both side
while (!found && splitIndex < 7 && splitIndex < target.length - 1) {
const regexLeft = new RegExp(`^${target.substring(0, splitIndex)}$`);
const regexRight = new RegExp(`^${target.substring(splitIndex)}$`);
splitIndex += 1;
const leftResult = cracker_helper.data.filter((item) =>
regexLeft.test(item)
);
const rightResult = cracker_helper.data.filter((item) =>
regexRight.test(item)
);
if (leftResult.length > 0 && rightResult.length > 0) {
const minSize = Math.min(leftResult.length, rightResult.length);
result = leftResult
.map((item, index) => {
if (index < minSize) {
return item + rightResult[index];
}
})
.filter((item) => item !== undefined);
if (result.length > 10) {
found = true;
}
}
}
}
if (filter_history[index]) {
result = result.filter((item) => {
for (let history of filter_history[index]) {
const char = history.char;
const charPos = history.charPos;
if (item && item[charPos] === char) {
return false;
}
}
return true;
});
}
result = result.slice(0, LIMIT);
if (result.length > 0) {
if (index) {
cracker_record[index] = result;
} else {
cracker_record[global_index] = result;
}
}
setCrackTitle("Done");
let found = item.find(".cracker-helper-panel");
if (found.length == 0) {
const detailPanel = $(`
<div class="cracker-helper-panel" data-attr=${global_index}>
<div class="cracker-current-status">
<div class="cracker-status-count">Top ${result.length} candidates:</div>
</div>
<div class="cracker-current-result">
</div>
<div class="cracker-button-set" data-index="0">
<button title="previous one" data-attr=${global_index} class="torn-btn cracker-button-prev" data-action="prev" >Prev</button>
<button title="next one" data-attr=${global_index} class="torn-btn cracker-button-next" data-action="next">Next</button>
</div>
</div>
`);
if (index) {
detailPanel.attr("data-attr", index);
detailPanel.find("button.cracker-button-prev").attr("data-attr", index);
detailPanel.find("button.cracker-button-next").attr("data-attr", index);
} else {
global_index += 1;
}
if (window.innerWidth < 1800) {
detailPanel.addClass("cracker-helper-panel-minimize");
}
if (result[0]) {
for (let char of result[0]) {
detailPanel.find(".cracker-current-result").append(
$(`
<div class="cracker-result-item" style="width: ${
item.find("div[class*=charSlot]").width() + 2
}px">
${char.toUpperCase()}
</div>
`)
);
}
}
detailPanel.find(".cracker-button-set").css({
width:
item.find("div[class*=guessesLeftSection]").width() +
item.find("div[class*=commitButtonSection]").width() +
5,
"padding-left": item.find("div[class*=guessesLeftSection]").width(),
});
if (isMobile()) {
detailPanel.addClass("cracker-helper-panel-mobile");
detailPanel.find(".cracker-current-status").css({
width: "33px",
"font-size": "7px",
border: "none",
});
} else {
detailPanel.find(".cracker-current-status").css({
width: item.find("div[class*=targetSection]").width() + 5,
});
}
detailPanel.css({
left: item.offset().left + item.width() + 10,
top: item.offset().top,
height: item.height(),
});
detailPanel.find(".cracker-button-set button").click(function () {
const action = $(this).attr("data-action");
const action_index = $(this).attr("data-attr");
let current_index = parseInt($(this).parent().attr("data-index"));
const action_record = cracker_record[action_index];
if (action_record) {
const record_length = action_record.length;
if (action === "next") {
if (current_index < record_length - 1) {
current_index += 1;
}
} else if (action === "prev") {
if (current_index > 0) {
current_index -= 1;
}
}
$(this).parent().attr("data-index", current_index);
$(this)
.parent()
.parent()
.find(".cracker-status-text")
.text(action_record[current_index]);
let index = 0;
if (action_record[current_index]) {
for (let char of action_record[current_index]) {
$(this)
.parent()
.parent()
.find(`div.cracker-result-item:eq(${index++})`)
.text(char.toUpperCase());
}
}
} else {
console.error("Fail to fetch record detail");
console.log(
`action_index: ${action_index}, action_record: ${action_record}`
);
console.log(cracker_record);
}
});
item.find("div[class*=sections]").after(detailPanel);
} else {
const currentIdx = parseInt(found.attr("data-attr"));
if (index && currentIdx < 10000) {
found.attr("data-attr", index);
found.find("button.cracker-button-prev").attr("data-attr", index);
found.find("button.cracker-button-next").attr("data-attr", index);
cracker_record[index] = cracker_record[currentIdx];
delete cracker_record[currentIdx];
}
found.find(".cracker-info-text").text(targetRegex);
found
.find(".cracker-status-count")
.text(`Top ${result.length} candidates:`);
let idx = 0;
if (result[0]) {
for (let char of result[0]) {
found
.find(`div.cracker-result-item:eq(${idx++})`)
.text(char.toUpperCase());
}
} else {
found.find(`div.cracker-result-item`).remove();
}
found.find(".cracker-button-set").attr("data-index", "0");
}
// Fix issue due to dynamic loading
// TODO: existing approach is not complete, it will fail sometime at bruteforce
const parent = item.parent().parent()[0];
let parentNewHeight = 102;
if ($(parent).hasClass("outcome-expanded")) {
parentNewHeight += 150;
}
$(parent).css("height", `${parentNewHeight}px`);
const nextSibling = parent.nextSibling;
const currentTranslate = getTranslate(nextSibling);
let heightUpToThisItem = 0;
let previousSibling = parent;
while (previousSibling) {
if ($(previousSibling).height() > 0)
heightUpToThisItem += parseInt(
$(previousSibling).css("height").replace("px", "")
);
previousSibling =
previousSibling.previousElementSibling ||
previousSibling.previousSibling;
}
const newNextHeight = heightUpToThisItem;
if (newNextHeight > currentTranslate[1] ?? 0) {
$(nextSibling).css(
"translate",
`${currentTranslate[0]}px ${newNextHeight}px`
);
}
function getTranslate(panel) {
return $(panel)
.css("translate")
.split(" ")
.map((x) => parseInt(x.replace("px", "")));
}
};
function fixCrimeContainer(mutations) {
handlePage($(".crime-option"));
}
let crimeContainerObserver;
const handlePage = (crimes) => {
if (!crimeContainerObserver) {
crimeContainerObserver = new MutationObserver(fixCrimeContainer);
}
crimeContainerObserver.observe($("div[class*=virtualList_]")[0], {
childList: true,
});
for (let i = 0; i < crimes.length; i++) {
const target = initial_targets[i];
let crime_id;
if (target) {
crime_id = target["ID"];
}
handleCrime($(crimes[i]), undefined, crime_id);
}
};
const updatePage = () => {
if (location.href.endsWith("cracking")) {
inject_once();
insertSelector();
setCrackTitle("Loading");
const crimes = $(".crime-option");
if (crimes.length < 1) {
if (!updateInterval) {
updateInterval = setInterval(() => {
if (
$(".crime-option").length > 0 &&
cracker_helper.data.length > 0
) {
handlePage($(".crime-option"));
clearInterval(updateInterval);
updateInterval = undefined;
}
}, 1000);
}
} else {
handlePage(crimes);
}
} else {
crimeContainerObserver?.disconnect();
crimeContainerObserver = null;
$(".cracker-helper-panel").each(function () {
$(this).remove();
});
$("div.cracker-helper-selector").remove();
}
};
const handleCrackPerpare = (params, data) => {
setTimeout(() => {
const crimeValue = parseInt(params.get("value1"));
if (!params.get("value2")) {
handlePage($(".crime-option"));
return;
}
const char = params.get("value2").toLowerCase();
const charPos = parseInt(params.get("value3"));
const targets = data["DB"]["crimesByType"]["targets"];
for (let i = 0; i < targets.length; i++) {
const target = targets[i];
const target_id = target.ID;
const target_panel = $(`.cracker-helper-panel:eq(${i})`);
const target_index = parseInt(target_panel.attr("data-attr"));
if (target_index < 10000) {
target_panel.attr("data-attr", target_id);
target_panel
.find("button.cracker-button-prev")
.attr("data-attr", target_id);
target_panel
.find("button.cracker-button-next")
.attr("data-attr", target_id);
cracker_record[target_id] = cracker_record[target_index];
delete cracker_record[target_index];
}
if (target_id === crimeValue) {
const currentChar = target["password"][charPos]["char"].toString();
if (currentChar !== char) {
if (!filter_history[target_id]) {
filter_history[target_id] = [];
}
filter_history[target_id].push({
char,
charPos,
});
handleCrime(target_panel.parent(), undefined, target_id);
// handlePage($(".crime-option"));
}
}
}
handlePage($(".crime-option"));
}, 25);
};
const handleCrackAttempt = (params, data) => {
setTimeout(() => {
try {
const crimeID = parseInt(params.get("crimeID"));
if (crimeID === 205) {
const crimeValue = parseInt(params.get("value1"));
const targets = data["DB"]["crimesByType"]["targets"];
for (let i = 0; i < targets.length; i++) {
const target = targets[i];
const target_id = target.ID;
const target_panel = $(`.cracker-helper-panel:eq(${i})`);
let targetChars;
if (target_id === crimeValue) {
const target_index = parseInt(target_panel.attr("data-attr"));
if (target_index < 10000) {
target_panel.attr("data-attr", target_id);
target_panel
.find("button.cracker-button-prev")
.attr("data-attr", target_id);
target_panel
.find("button.cracker-button-next")
.attr("data-attr", target_id);
cracker_record[target_id] = cracker_record[target_index];
delete cracker_record[target_index];
}
targetChars = target.password
.map((x) => (x.char && x.char !== "*" ? x.char : "."))
.join("");
}
handleCrime(target_panel.parent(), targetChars, target_id);
}
}
} catch (err) {
console.log(err);
}
}, 25);
};
let initial_targets = [];
const handleCrackList = (data) => {
const targets = data["DB"]["crimesByType"]["targets"];
if (initial_targets.length === 0) {
initial_targets = targets;
if ($(".cracker-helper-panel").length > 0) {
for (let i = 0; i < initial_targets.length; i++) {
const target_id = initial_targets[i]["ID"];
const target_panel = $(`.cracker-helper-panel:eq(${i})`);
const target_index = parseInt(target_panel.attr("data-attr"));
target_panel.attr("data-attr", target_id);
target_panel
.find("button.cracker-button-prev")
.attr("data-attr", target_id);
target_panel
.find("button.cracker-button-next")
.attr("data-attr", target_id);
cracker_record[target_id] = cracker_record[target_index];
delete cracker_record[target_index];
}
}
}
};
const interceptFetch = () => {
const targetWindow =
typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const origFetch = targetWindow.fetch;
targetWindow.fetch = async (...args) => {
const rsp = await origFetch(...args);
const url = new URL(args[0], location.origin);
const params = new URLSearchParams(url.search);
if (
url.pathname === "/loader.php" &&
params.get("sid") === "crimesData"
) {
const step = params.get("step");
const clonedRsp = rsp.clone();
if (step === "prepare") {
handleCrackPerpare(params, await clonedRsp.json());
} else if (step === "attempt") {
handleCrackAttempt(params, await clonedRsp.json());
} else if (step === "crimesList") {
handleCrackList(await clonedRsp.json());
}
}
return rsp;
};
};
const inject_once = () => {
if (is_injected) {
return;
}
addStyle();
interceptFetch();
try {
if (inPDA) {
console.log(`Load password list for PDA`);
fetch_action(false);
} else {
GM.getValue(CRACKER_HELPER_KEY, cracker_helper)
.then((cracker) => {
cracker_helper = cracker;
if (cracker_helper.source == PASSWORD_DATABASE[CRACKER_SEL]) {
setCrackTitle("Loaded");
updatePage();
console.log("load cracker_helper from cache at Desktop:");
console.log(cracker_helper);
} else {
fetch_action();
}
})
.catch(() => {
fetch_action(false);
});
}
} catch (err) {
console.log(err);
}
is_injected = true;
};
console.log("Userscript cracker helper starts");
updatePage();
window.onhashchange = () => {
updatePage();
};
const bindEventListener = function (type) {
const historyEvent = history[type];
return function () {
const newEvent = historyEvent.apply(this, arguments);
const e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return newEvent;
};
};
history.pushState = bindEventListener("pushState");
window.addEventListener("pushState", function (e) {
updatePage();
});
})();