Posted on

Ci, który mnie znają, wiedzą, że jeśli miałbym jedyną rzecz, na którą mógłbym narzekać, i na żadną inną - to wybrałbym kiepskie aplikacje. Konkretnie ich interfejsy oraz szeroko rozumiane "doświadczenie użytkownika". Ponieważ zarówno zawodowo, jak i prywatnie jestem wielkim fanem programowania, komputerów, aplikacji, urządzeń cyfrowych i internetu, zwracam bardzo dużą uwagę na użyteczność i intuicyjność tego typu rozwiązań. W ogóle generalnie uważam, że współczesna technologia jest praktycznie jak magia, ale nie w kwestii tego, jak działa, tylko jak bardzo pozwala na rozwiązywanie najmniejszych oraz największych problemów na świecie. Oczywiście tworzy też nowe, ale moim zdaniem wartość netto jest dodatnia. Do rzeczy.

Jednym z banków, któremu powierzyłem swoje finanse jest ING Bank. Jest tak samo dobry i tak samo zły jak każdy inny bank na świecie. Ale jedną rzecz robią tak idiotycznie, że zasłużyli na laurkę na moim wspaniałym blogu.

ING Bank dostęp do konta zabezpiecza przy użyciu tzw. hasła maskowanego. Hasło maskowane to nic innego, jak formularz akceptujący losowe litery hasła, zamiast całego ciągu. Zdjęcie poglądowe poniżej.

hasło maskowane ING

Samo hasło maskowane to jeszcze nie koniec świata. Co prawda, wpisywanie hasła w ten sposób jest upierdliwe, bo większość ludzi nawet z hasłami alamakota123 będzie potrzebować kilku chwil, żeby doliczyć, jaka litera jest na siódmej pozycji (dalej, powiedz jaka, bez liczenia). Problem ten rozwiązują niektóre menadżery haseł - ja wiem, że robi to na pewno wtyczka BitWardena. Więc w czym problem?

Ano w tym, że ING, oprócz tego wspaniałego pomysłu, miało jeszcze jeden - i to lepszy! Otóż praktycznie każdy jeden element interfejsu strony logowania ING jest wsadzony w tak zwany Shadow DOM - co to jest, nie ma większego znaczenia. Znaczenie ma to, że to prawie całkowicie wywala dzialanie jakiegokolwiek menadżera haseł. Serio, szukałem i nie znalazłem jednego działającego rozwiązania. Nie będę się nawet zastanawiał z czego wynika taka gigantyczna kulminacja idiotyzmów na stronie ING, bo już wiem, że to z powodu bezpieczeństwa, myślenia o dzieciach, zapobiegania kradzieży tożsamości, i to dla mojej wygody!

No i zajebiście, tylko że moje hasło do ING ma maksymalną ilość wszystkich możliwych losowych znaków, jakie tylko wygenerował mi menadżer haseł. I ja naprawdę tracę cierpliwość za KAŻDYM. JEDNYM. RAZEM. jak muszę zalogować się do banku. Serio, raz tak odkładałem zapłcenie podatków, że dostałem maila od ZUSu, że oni bardzo przepraszają, ale mają wiecie gdzie to, że nie chce mi się wpisywać hasła, i że mam zapłacić odsetki w ciągu 5 dni.

Ale nie od tego mam kąkuter, żeby się przejmować takimi problemami. Enter skryptozakładka. Skryptozakładka to, jak sama nazwa wskazuje, skrypt zaklęty w zakładkę. Ponieważ nie chce mi się już pisać samemu kodu, poprosiłem oczywiście czatbota o napisanie mi skryptozakładki, która po uruchomieniu, tak zmodyfikuje ekran logowania ING, że pojawi się na nim:

  • pojedyncze pole tekstowe, do którego będę mógł jak normalny człowiek autouzupełnić hasło,
  • przycisk "Wstaw hasło", który po kliknięciu sobie poszczególne literki powkłada w te różne otwory ING Banku.

Finalnie wygląda to tak:

hasło ludzkie ING

Co robi ten skrypt?

Ocala życie.
Leczy zszargane nerwy.
Ratuje ludzkość.

javascript: (function () {
  function findElementByIdInShadowDom(id, root = document) {
    if (root.getElementById?.(id)) return root.getElementById(id);
    for (const el of root.querySelectorAll("*")) {
      if (el.shadowRoot) {
        const found = findElementByIdInShadowDom(id, el.shadowRoot);
        if (found) return found;
      }
    }
    return null;
  }

  function findElementByTagInShadowDom(tagName, root = document) {
    const elements =
      root.getElementsByTagName?.(tagName) ||
      root.querySelectorAll?.(tagName) ||
      [];
    if (elements.length > 0) return elements[0];
    for (const el of root.querySelectorAll("*")) {
      if (el.shadowRoot) {
        const found = findElementByTagInShadowDom(tagName, el.shadowRoot);
        if (found) return found;
      }
    }
    return null;
  }

  function createPasswordFieldAndButton() {
    console.log("Looking for ing-card element...");
    const ingCard = findElementByTagInShadowDom("ing-card");
    if (!ingCard) {
      console.log("Nie znaleziono <ing-card>, skrypt nie może kontynuować");
      return;
    }
    console.log("Found ing-card, checking for shadow root");
    if (!ingCard.shadowRoot) {
      console.log("ing-card doesn't have a shadow root, inserting directly");
      insertPasswordTools(ingCard);
      return;
    }
    console.log("ing-card has a shadow root, inserting inside it");
    let targetElement = ingCard.shadowRoot;
    if (targetElement.firstElementChild) {
      insertPasswordTools(targetElement);
    } else {
      console.log("Shadow root is empty, inserting directly");
      insertPasswordTools(targetElement);
    }
  }

  function insertPasswordTools(container) {
    const passwordField = document.createElement("input");
    passwordField.type = "password";
    passwordField.id = "manual-password-field";
    passwordField.name = "password";
    passwordField.className = "password-field";
    passwordField.autocomplete = "current-password";
    passwordField.setAttribute("aria-label", "Password");
    passwordField.placeholder = "Wpisz swoje hasło";
    passwordField.style.marginBottom = "10px";
    passwordField.style.padding = "8px";
    passwordField.style.width = "100%";
    passwordField.style.boxSizing = "border-box";

    const button = document.createElement("button");
    button.textContent = "Wstaw hasło";
    button.style.marginBottom = "10px";
    button.style.padding = "8px 16px";
    button.style.width = "100%";
    button.style.boxSizing = "border-box";
    button.type = "button";

    const toolContainer = document.createElement("div");
    toolContainer.style.padding = "10px";
    toolContainer.style.border = "2px solid #ff6200";
    toolContainer.style.borderRadius = "5px";
    toolContainer.style.background = "#fff";
    toolContainer.style.marginBottom = "10px";
    toolContainer.style.zIndex = "9999";
    toolContainer.style.position = "relative";

    const fieldContainer = document.createElement("div");
    fieldContainer.id = "password-form";
    fieldContainer.setAttribute("role", "form");
    fieldContainer.setAttribute("aria-label", "Login Form");
    fieldContainer.appendChild(passwordField);
    fieldContainer.appendChild(button);
    toolContainer.appendChild(fieldContainer);

    const heading = document.createElement("div");
    heading.textContent = "ING Password Tool";
    heading.style.fontWeight = "bold";
    heading.style.marginBottom = "10px";

    toolContainer.insertBefore(heading, toolContainer.firstChild);

    if (container.firstElementChild) {
      container.insertBefore(toolContainer, container.firstElementChild);
    } else {
      container.appendChild(toolContainer);
    }

    console.log("Password field and button added");

    function isFieldDisabled(field) {
      return (
        field.disabled ||
        field.hasAttribute("disabled") ||
        field.getAttribute("aria-disabled") === "true"
      );
    }

    button.addEventListener("click", () => {
      const password = passwordField.value;
      if (!password) {
        console.log("Wpisz hasło w pole tekstowe");
        return;
      }
      console.log(`Attempting to fill password of length ${password.length}`);
      let filledCount = 0;
      let disabledCount = 0;
      for (let i = 0; i < password.length; i++) {
        const currentPasswordInput = findElementByIdInShadowDom(
          `current-password-${i}`
        );
        if (currentPasswordInput) {
          if (isFieldDisabled(currentPasswordInput)) {
            console.log(`Field current-password-${i} is disabled, skipping`);
            disabledCount++;
          } else {
            currentPasswordInput.value = password[i];
            currentPasswordInput.dispatchEvent(
              new Event("input", { bubbles: true })
            );
            console.log(
              `Filled character "${password[i]}" into field current-password-${i}`
            );
            filledCount++;
          }
        } else {
          console.log(`Could not find field current-password-${i}`);
        }
      }
      console.log(
        `Filled ${filledCount} password fields (skipped ${disabledCount} disabled fields)`
      );
    });
  }
  createPasswordFieldAndButton();
})();