#!/usr/bin/env bash
# Real demos for P029 "Password Security: Why Hackers Crack Weak Passwords" (Cybersecurity for Beginners, episode 4).
# One Ubuntu 24.04 container stands in for a computer in Ana's shop (hostname "shop", user ana), as in episode 3.
# Nothing here attacks anything: no cracking tools, no stolen data. We measure how fast this computer hashes,
# do the arithmetic of how many passwords exist, and ask Have I Been Pwned's Pwned Passwords range API how often
# example passwords appear in breaches (only the first 5 of 40 SHA-1 characters leave the machine).
# Tools: Python with argon2-cffi (Argon2id defaults), coreutils shuf, the EFF large word list (7,776 words).
set -euo pipefail
cd "$(dirname "$0")"
P=ce-p029
cleanup() { docker rm -f $P-shop >/dev/null 2>&1 || true; }
cleanup
trap cleanup EXIT

W=/tmp/ce-p029
rm -rf $W && mkdir -p $W/img
cat > $W/img/Dockerfile <<'DOCKER'
FROM ubuntu:24.04
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
      ca-certificates curl openssl python3 python3-argon2 >/dev/null \
 && curl -fsS https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt | cut -f2 > /usr/share/eff_words.txt \
 && test "$(wc -l < /usr/share/eff_words.txt)" = 7776 \
 && rm -rf /var/lib/apt/lists/* \
 && (userdel -r ubuntu 2>/dev/null || true) && useradd -m -s /bin/bash ana
DOCKER
docker build -q ${DOCKER_BUILD_ARGS:-} -t $P-img $W/img >/dev/null
docker run -d --name $P-shop --hostname shop $P-img sleep infinity >/dev/null
for f in lab/*.py; do docker cp "$f" $P-shop:/home/ana/; done
docker exec $P-shop sh -c "cp /usr/share/eff_words.txt /home/ana/words.txt && chown -R ana:ana /home/ana"

python3 - "$P" <<'PY'
import fcntl, json, os, pty, re, select, struct, subprocess, sys, termios, time
P = sys.argv[1]
steps = {}
READY = re.compile(rb"__READY__:(.*?):__ ")

def sh(cmd):
    return subprocess.run(cmd, shell=True, capture_output=True, text=True, check=True).stdout

def clean(text):
    text = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]|\x1b[()][A-Z0-9]", "", text)
    lines = [l.rstrip("\r").split("\r")[-1] for l in text.split("\n")]
    return "\n".join(l.rstrip() for l in lines).strip("\n")

class Shell:
    """One interactive bash in a real terminal (80 columns), like a terminal window."""
    def __init__(self, box, user):
        self.host, self.user, self.where = box, user, "~"
        pid, fd = pty.fork()
        if pid == 0:
            fcntl.ioctl(0, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 80, 0, 0))
            os.execvp("docker", ["docker", "exec", "-it", "-u", user, "-w", f"/home/{user}", "-e", f"HOME=/home/{user}",
                                 "-e", "TERM=xterm", f"{P}-{box}", "bash", "-i"])
        self.fd = fd
        os.write(fd, b"stty -echo; PS1='__REA''DY__:\\w:__ '\n")
        self.read()

    def read(self, timeout=120):
        buf, end = b"", time.time() + timeout
        while time.time() < end:
            if select.select([self.fd], [], [], 0.3)[0]:
                buf += os.read(self.fd, 65536)
                m = None
                for m in READY.finditer(buf):
                    pass
                if m:
                    self.where = m.group(1).decode()
                    return clean(buf[:m.start()].decode("utf-8", "replace"))
        raise SystemExit(f"{self.host}: timed out; last output: {buf[-400:]!r}")

    def run(self, key, cmd):
        shown = f"{self.user}@{self.host}:{self.where}$ " + cmd
        os.write(self.fd, cmd.encode() + b"\n")
        out = self.read()
        steps[key] = {"cmd": shown, "out": out}
        print(f"== {key}\n{shown}\n{out}\n", flush=True)

ana = Shell("shop", "ana")
# ---------------------------------------------------------------- 1. how fast guessing is, and how many passwords exist
ana.run("space", "python3 space.py")
# ---------------------------------------------------------------- 2. human patterns are already in breach lists
ana.run("pw_ben", "python3 pwned.py sunshine1")
ana.run("pw_cap", "python3 pwned.py 'Sunshine1!'")
ana.run("pw_leet", "python3 pwned.py 'P@ssw0rd'")
# ---------------------------------------------------------------- 3. what to use instead
ana.run("words", "wc -l words.txt")
ana.run("pp_make", "shuf -n 5 words.txt | paste -sd-")
pp = steps["pp_make"]["out"].strip()
assert re.fullmatch(r"[a-z]+(-[a-z]+){4}", pp), pp
ana.run("pp_check", f"python3 pwned.py {pp}")
ana.run("rand", "openssl rand -base64 15")

steps["version"] = {"cmd": "versions", "out": sh(
    f"docker exec {P}-shop sh -c '. /etc/os-release; echo $PRETTY_NAME; "
    f"python3 --version; python3 -c \"import importlib.metadata as m; print(\\\"argon2-cffi\\\", m.version(\\\"argon2-cffi\\\"))\"; "
    f"nproc'; date -u +%Y-%m-%dT%H:%MZ").strip()}
json.dump(steps, open("demo_output.json", "w"), indent=1)
print("saved demo_output.json")
PY
