From 583966d06f8e1b9bf054391a2b04c2fe81b0a7b8 Mon Sep 17 00:00:00 2001 From: Marchy02 Date: Sun, 7 Jun 2026 11:29:09 +0200 Subject: [PATCH] Fix entropy calc, crash on long passwords, and CLI loop/verdict bugs Make charset entropy additive (lower+digit now base 36, special class always counted) in password_tester.py and script.js, kept in parity via a shared SPECIAL_CHARS set. Load common-passwords from a path built off __file__ under assets/ once into a set, degrading gracefully if missing. Compute time_to_crack in log space to avoid OverflowError on long inputs and guard base==0. Fix CLI yes/no loop to use equality and add the missing lowercase check to match the web verdict. Pin rich in requirements.txt. Co-Authored-By: Claude Opus 4.8 (1M context) --- js/script.js | 41 +++++----- password_tester.py | 185 ++++++++++++++++++++++++--------------------- requirements.txt | 1 + 3 files changed, 117 insertions(+), 110 deletions(-) create mode 100644 requirements.txt diff --git a/js/script.js b/js/script.js index 12ce58b..bbe02dd 100644 --- a/js/script.js +++ b/js/script.js @@ -1,6 +1,12 @@ // Password comuni - caricata dal file let PASSWORD_COMUNI = []; +// Insieme dei caratteri speciali riconosciuti. Tenuto in parità con password_tester.py. +const SPECIAL_CHARS = '!"£%&/()=?^*°#@|$§{[}]~€&'; + +// Numero di simboli distinti: base di entropia per la classe speciale. +const SPECIAL_CHARSET_SIZE = new Set(SPECIAL_CHARS).size; + // Carica il file delle password comuni all'avvio async function carica_password_comuni() { try { @@ -67,11 +73,11 @@ function there_is_a_number(psw) { function there_is_a_special_caracter(psw) { for (let caratteri of psw) { - if ('!"£%&/()=?^*°#@|$§{[}]~€&'.includes(caratteri)) { + if (SPECIAL_CHARS.includes(caratteri)) { return true; } } - + return false; } @@ -91,7 +97,7 @@ function count_special_caracters(psw) { let count_special_caracters = 0; for (let carattere of psw) { - if ('!"£%&/()=?^*°#@|$§{[}]~€&'.includes(carattere)) { + if (SPECIAL_CHARS.includes(carattere)) { count_special_caracters += 1; } } @@ -147,29 +153,16 @@ function time_to_crack(psw) { let is_speciale_presente = there_is_a_special_caracter(psw); - if (is_num_presente && is_minuscola_presente && is_maiuscola_presente && is_speciale_presente) { - base = 94; - } + // Entropia additiva: ogni classe di caratteri presente allarga l'alfabeto. + if (is_minuscola_presente) base += 26; + if (is_maiuscola_presente) base += 26; + if (is_num_presente) base += 10; + if (is_speciale_presente) base += SPECIAL_CHARSET_SIZE; - else if (is_minuscola_presente && is_maiuscola_presente && is_num_presente) { - base = 62; // 26 (m) + 26 (M) + 10 (N) - } - - else if (is_minuscola_presente && is_maiuscola_presente) { - base = 52; // 26 (m) + 26 (M) + // Alfabeto vuoto (es. solo spazi): trattata come banalmente debole. + if (base === 0) { + return "0 secondi"; } - - else if (is_num_presente) { - base = 10; - } - else if (is_minuscola_presente || is_maiuscola_presente) { - base = 26; - } - else if (is_speciale_presente) { - base = 32; - } - - let n_combinazini = Math.pow(base, psw.length); diff --git a/password_tester.py b/password_tester.py index 47a1ea9..c6c0a8e 100644 --- a/password_tester.py +++ b/password_tester.py @@ -1,4 +1,34 @@ -from rich import print +import math +from pathlib import Path + +from rich import print + +# Insieme dei caratteri speciali riconosciuti. Tenuto in parità con script.js. +SPECIAL_CHARS = '!"£%&/()=?^*°#@|$§{[}]~€&' + +# Numero di simboli distinti: usato come "base" di entropia per la classe speciale. +SPECIAL_CHARSET_SIZE = len(set(SPECIAL_CHARS)) + + +def _load_common_passwords(): + """Carica una sola volta le password comuni in un set. + + Il file vive in assets/ accanto a questo script: costruiamo il percorso da + __file__ cosi' funziona anche se lanciato da un'altra cwd. Se manca, l'app + continua senza il controllo "password comune" invece di crashare. + """ + percorso = Path(__file__).parent / "assets" / "100k-most-used-passwords-NCSC.txt" + try: + with open(percorso, "r", encoding="utf-8") as file: + return {riga.strip().lower() for riga in file if riga.strip()} + except FileNotFoundError: + print("[yellow]⚠️ File password comuni non trovato: controllo disabilitato.[/yellow]") + return set() + + +# Caricato una volta sola all'avvio invece di rileggere il file ad ogni chiamata. +PASSWORD_COMUNI = _load_common_passwords() + def there_is_an_uppercase(psw): for caratteri in psw: @@ -29,30 +59,15 @@ def there_is_a_number(psw): def there_is_a_special_caracter(psw): - for caratteri in psw: - if caratteri in "!""£%&/()=?^*°#@|$§{[}]~€&": + for caratteri in psw: + if caratteri in SPECIAL_CHARS: return True - - return False - -def is_password_common(psw): - - password_comuni = [] - - with open("./100k-most-used-passwords-NCSC.txt", "r") as file: - - for riga in file.readlines(): - - temp= riga.strip().lower() - - password_comuni.append(temp) + return False - if psw.lower() in password_comuni: - return True - +def is_password_common(psw): - return False + return psw.lower() in PASSWORD_COMUNI def count_special_caracters(psw): @@ -60,8 +75,8 @@ def count_special_caracters(psw): count_special_caracters = 0 for carattere in psw: - if carattere in "!""£%&/()=?^*°#@|$§{[}]~€&": - count_special_caracters += 1 + if carattere in SPECIAL_CHARS: + count_special_caracters += 1 if count_special_caracters == len(psw): return True @@ -105,62 +120,57 @@ def time_to_crack(psw): is_speciale_presente = there_is_a_special_caracter(psw) - if is_num_presente and is_minuscola_presente and is_maiuscola_presente and is_speciale_presente: - base = 94 - - elif is_minuscola_presente and is_maiuscola_presente and is_num_presente: - base = 62 # 26 (m) + 26 (M) + 10 (N) - - elif is_minuscola_presente and is_maiuscola_presente: - base = 52 # 26 (m) + 26 (M) - - elif is_num_presente: - base = 10 - elif is_minuscola_presente or is_maiuscola_presente: - base = 26 - elif is_speciale_presente: - base = 32 - - - - - n_combinazini = pow(base, len(psw)) + # Entropia additiva: ogni classe di caratteri presente allarga l'alfabeto. + base = 0 + if is_minuscola_presente: + base += 26 + if is_maiuscola_presente: + base += 26 + if is_num_presente: + base += 10 + if is_speciale_presente: + base += SPECIAL_CHARSET_SIZE + + # Alfabeto vuoto (es. password fatta di soli spazi): impossibile da forzare + # statisticamente, ma la trattiamo come banalmente debole. + if base == 0: + return "0 secondi" # Velocità di attacco al secondo (quello che riesce a fare un pc buono oggi secondo gemini ) tentativi_al_secondo = 100000000000 - - secondi = n_combinazini / tentativi_al_secondo - - - minuti = secondi / 60 - ore = minuti / 60 - giorni = ore / 24 - anni = giorni / 365 - secoli = anni / 100 - millenni = anni / 1000 - milioni_anni = anni / 1000000 - miliardi_anni = anni / 1000000000 - - # Restituiamo il tempo nel formato più appropriato - if miliardi_anni >= 1: - return f"{int(miliardi_anni)} miliardi di anni" - elif milioni_anni >= 1: - return f"{int(milioni_anni)} milioni di anni" - elif millenni >= 1: - return f"{int(millenni)} millenni" - elif secoli >= 1: - return f"{int(secoli)} secoli" - elif anni >= 1: - return f"{int(anni)} anni" - elif giorni >= 1: - return f"{int(giorni)} giorni" - elif ore >= 1: - return f"{int(ore)} ore" - elif minuti >= 1: - return f"{int(minuti)} minuti" - else: - return f"{int(secondi)} secondi" + # Lavoriamo in spazio logaritmico (base 10): pow(base, len) esploderebbe e + # genererebbe OverflowError su password lunghe. Non materializziamo mai + # l'intero gigante: confrontiamo e formattiamo solo i logaritmi. + combos_log = len(psw) * math.log10(base) + secondi_log = combos_log - math.log10(tentativi_al_secondo) + + # (secondi in una unità, etichetta), dal più grande al più piccolo. + ANNO = 60 * 60 * 24 * 365 + UNITA = [ + (ANNO * 1_000_000_000, "miliardi di anni"), + (ANNO * 1_000_000, "milioni di anni"), + (ANNO * 1000, "millenni"), + (ANNO * 100, "secoli"), + (ANNO, "anni"), + (60 * 60 * 24, "giorni"), + (60 * 60, "ore"), + (60, "minuti"), + (1, "secondi"), + ] + + for unita_secondi, etichetta in UNITA: + if secondi_log >= math.log10(unita_secondi): + valore_log = secondi_log - math.log10(unita_secondi) + # Oltre ~1e15 il numero esatto non aggiunge informazione: lo + # mostriamo in notazione 10^k per evitare interi giganti/overflow. + if valore_log >= 15: + return f"oltre 10^{int(valore_log)} {etichetta}" + # valore = secondi_totali / unita_secondi, calcolato in spazio log + # cosi' l'intero combinatorio non viene mai materializzato. + return f"{int(10 ** valore_log)} {etichetta}" + + return "0 secondi" @@ -188,14 +198,18 @@ def main(): output2 = there_is_a_number(password) output3 = there_is_a_special_caracter(password) output4 = there_is_an_uppercase(password) + output5 = there_is_a_lowercase(password) errori_riscontrati = [] - + if output4 == False: errori_riscontrati.append("una lettera maiuscola") + if output5 == False: + errori_riscontrati.append("una lettera minuscola") + if output1 == False: errori_riscontrati.append("lunghezza (minimo 8 caratteri)") @@ -223,7 +237,7 @@ def main(): #Password sicura - if output1 and output2 and output3 and output4 and not password_is_common: + if output1 and output2 and output3 and output4 and output5 and not password_is_common: print("\n\n✅ [green]Tecnicamente la tua password è forte![/green]\n\n") @@ -249,16 +263,15 @@ def main(): main() -#loop nel main +#loop nel main +# Confronto di uguaglianza: "in 'Yy'" matchava qualsiasi stringa contenente y/Y +# (e anche "" che e' sottostringa di ogni stringa), causando un loop scorretto. continuare = "y" -while continuare in "Yy": - - continuare = input("\nVuoi controllare un'altra password? [y/N]: ") - - if continuare == "": - continuare = "n" - - if continuare in "Yy": +while continuare.strip().lower() == "y": + + continuare = input("\nVuoi controllare un'altra password? [y/N]: ") + + if continuare.strip().lower() == "y": main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dad6a4e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +rich==14.3.1