torn-crime-crack-helper

Utilize password database to crack torn cracking crime.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
  });
})();