In fondo a Cronache del Sottosopra c’è un regalo: un programma che Rita ha lasciato sotto il bancone di Gigi, per chi arriva fino all’ultima pagina. Qui lo trovate pronto, per chi non ha voglia di ricopiarlo a mano.

«Il codice non mente. Mente chi non lo legge.»

Rita, detta PiGreco

Quindi prima di lanciarlo leggetelo. È lo stesso codice stampato nel libro, riga per riga.

Scaricare e verificare

Scarica sottosopra.zip

Prima di fidarvi, verificate. L’impronta SHA-256 di sottosopra.py è:

c982d7ee25aebf6e1998e89ce933d19f38d6ff857af1cfca51c2934c3ced8757

Su Mac e Linux: shasum -a 256 sottosopra.py. Su Windows: certutil -hashfile sottosopra.py SHA256. Se il numero non è identico, il file non è quello del libro: non lanciatelo.

Come si usa

Serve Python 3, nient’altro. Dal terminale: python3 sottosopra.py. Gigi vi chiede una password e vi dice se gira già nei furti di dati: dal vostro computer escono solo cinque caratteri della sua impronta, il confronto lo fa la vostra macchina. Poi vi chiede cinque parole. Dove trovarle lo sa chi ha letto il libro.

Su Mac, se avete installato Python da python.org e Gigi si lamenta dei certificati: aprite la cartella Applicazioni, poi Python 3.x, e fate doppio clic su Install Certificates.command.

Il codice

#!/usr/bin/env python3
# «Il codice non mente. Mente chi non lo legge.»  Rita, detta PiGreco
# Cronache del Sottosopra, il regalo del Bar di Gigi. Solo Python 3.
import base64, getpass, hashlib, hmac, unicodedata, urllib.request

SETEC_ASTRONOMY = """
Nw9RWUB3+zcRY5zG7I4U+6d34ufGdTLjfAy0ErcCzZP0lqrUiz/N
xFAW4OzHnqz0SWDoGeR12V5mUyiI7ya3MEfBDAeXwblI7HKTBE3C
NkIjLVD1HB0DRA+WTJZo8RtajNdMxQ95gQAj0pj2iMq09vTQu6mB
oUb4eJoIkgAHiA36zPZ+s0Jw23xCxw=="""

# Al bancone: dall'hash della password escono solo 5 caratteri.
# Il resto del confronto lo fa il tuo computer (k-anonymity).
def al_bancone(pw):
    h = hashlib.sha1(pw.encode()).hexdigest().upper()
    url = "https://api.pwnedpasswords.com/range/" + h[:5]
    req = urllib.request.Request(url, headers={"User-Agent": "Bar-di-Gigi"})
    with urllib.request.urlopen(req, timeout=10) as r:
        for riga in r.read().decode().splitlines():
            coda, volte = riga.split(":")
            if coda == h[5:]:
                return int(volte)
    return 0

def apri(parole):
    dati = base64.b64decode("".join(SETEC_ASTRONOMY.split()))
    sale, sigillo, cifrato = dati[:16], dati[16:32], dati[32:]
    frase = unicodedata.normalize("NFC", " ".join(parole.lower().split()))
    k = hashlib.pbkdf2_hmac("sha256", frase.encode(), sale, 200_000)
    prova = hmac.new(k, cifrato, "sha256").digest()[:16]
    if not hmac.compare_digest(prova, sigillo):
        return None
    flusso = b"".join(hashlib.sha256(k + i.to_bytes(4, "big")).digest()
                      for i in range(len(cifrato) // 32 + 1))
    return bytes(a ^ b for a, b in zip(cifrato, flusso)).decode()

pw = getpass.getpass("Gigi: Dimmi la password, tanto non la guardo. ")
try:
    n = al_bancone(pw)
    volte = f"{n:,}".replace(",", ".")
    print(f"Gigi: L'hanno vista {volte} volte nei furti. Cambiala STASERA."
          if n else "Gigi: Pulita. Per ora.")
except OSError as e:
    if "CERTIFICATE_VERIFY_FAILED" in str(e):
        print("Gigi: Al tuo Python mancano i certificati, e io non parlo.\n"
              "      Su Mac: Applicazioni > Python > Install Certificates.")
    else:
        print("Gigi: Stasera la linea non va. Riprova domani.")

parole = input("\nGigi: E adesso le cinque parole, in ordine: ")
segreto = apri(parole)
print("\n" + segreto if segreto else
      "Gigi: Non ancora. La fiducia si guadagna: torna a leggere.")

Le parole qui non ci sono, e non le troverete nei commenti. La fiducia si guadagna.