website

Personal website
git clone https://github.com/michaelneuper/website.git
README | Files | Log | Refs

website/src/scripts/ctf.js (3858B)


// Three beginner CTF flags hidden around the site. This file keeps only their
// SHA-256 hashes, so reading it shows how flags are checked without giving them away.
// Finding all three unlocks M-x butterfly (src/scripts/emacs/play.js), which
// the submit button then turns into, and the modeline shows root.

import { $ } from "./util.js";
import { defcommand, message, completingRead, setSegment, call } from "./emacs/index.js";
import { butterfly } from "./emacs/play.js";

//! Not that easy :) These are SHA-256 hashes, so the flags aren't in here.
const HASHES = [
  "7a80f308e7d3c86589ac38394f24154bf36149b8ad833f728c9e0ddeed29693e",
  "b8af26fa58465fa7be727749c616d906054f12928d817dd34d94284943d6f02e",
  "d43266a3ddf2cab859b257fad7ea2f2eceee3726e72f1651305a6d3f14bf064e",
];

const load = () => {
  try { return new Set(JSON.parse(localStorage.getItem("flags") || "[]")); } catch { return new Set(); }
};
const save = (set) => {
  try { localStorage.setItem("flags", JSON.stringify([...set])); } catch { /* private mode */ }
};

async function sha256(text) {
  const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
  return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
}

export function initCtf() {
  const progress = $(".ctf-progress");
  const button = $('[data-ctf="submit"]');
  let found = load();
  const count = () => [...found].filter((h) => HASHES.includes(h)).length;

  const paint = () => {
    const n = count();
    const all = n === 3;
    if (progress) {
      progress.textContent = all ? "3 of 3 found" : `${n} of 3 found`;
      progress.classList.toggle("done", all);
    }
    if (button) {
      button.textContent = all ? "M-x butterfly" : "submit a flag";
      button.classList.toggle("is-done", all);
    }
    setSegment("flags", all ? "# root" : n ? `flags ${n}/3` : "");
    // M-x only lists butterfly once it has been earned, and *Help* only then mentions it
    defcommand("butterfly", () => (count() === 3 ? butterfly() : message("Find all three flags first.")), {
      doc: "Use butterflies to flip the desired bit on the drive platter", hidden: !all,
    });
    const help = $('.help [data-unlock="butterfly"]');
    if (help) help.hidden = !all;
  };
  paint();

  async function submit() {
    const guess = await completingRead("Flag: ", [], { requireMatch: false });
    if (!guess) return;
    if (!crypto.subtle) return message("Checking flags needs a secure context (https or localhost).", "error");
    const h = await sha256(guess.trim());
    if (!HASHES.includes(h)) return message("Not a flag. Keep looking.", "error");
    if (found.has(h)) return message("You already found that one.");
    found.add(h);
    save(found);
    paint();
    const n = count();
    if (n === 3) {
      message("Correct. That’s all three. M-x butterfly is yours now.", "ok", 8000);
    } else message(`Correct. ${n} of 3 found.`, "ok", 6000);
  }

  defcommand("submit-flag", submit, { doc: "Check a CTF flag you found on this site", key: "SPC s f" });
  defcommand("copy-gpg-fingerprint", async () => {
    const fp = $(".gpg-fp code")?.textContent ?? "";
    try {
      await navigator.clipboard.writeText(fp);
      message(`Copied ${fp}`, "ok");
    } catch {
      message(fp);
    }
  }, { doc: "Copy my GPG key fingerprint", key: "SPC s g" });
  //! Not that easy :) Your guess is hashed and compared, so there are no flags to find here.
  button?.addEventListener("click", () => call(count() === 3 ? "butterfly" : "submit-flag"));

  // One flag lives in the console.
  console.log(
    "%cPoking around in the console? Good instinct.%c\nThere are three flags on this site. Here is one, base64-encoded:\nZmxhZ3tjb25zb2xlX2xvZ3NfbGVha30=\nSubmit flags with M-x submit-flag.",
    "font: 600 13px monospace",
    "font: 12px monospace",
  );
}