diff --git a/main.py b/main.py index a79722a..f9d6296 100644 --- a/main.py +++ b/main.py @@ -10,13 +10,14 @@ from modules.clipboard_manager import clear_duration, clear_clipboard from modules.leakedornot import check_pwned + class PasswordGenerator: def __init__(self): self.app = ThemedTk(theme="arc") self.app.title("SafePass - Password Generator") self.app.resizable(width=False, height=False) self.style = ttk.Style() - + self.include_spaces = tk.BooleanVar(value=True) self.numbers_var = tk.BooleanVar() self.special_chars_var = tk.BooleanVar(value=True) @@ -46,7 +47,7 @@ def create_widgets(self): def create_num_words_widget(self): num_words_label = ttk.Label(self.app, text="Number of words:") num_words_label.grid(row=0, column=0, padx=10, pady=5, sticky="w") - + self.num_words_entry = ttk.Entry(self.app, validate="key", validatecommand=(self.app.register(self.validate_num_words), '%P')) self.num_words_entry.grid(row=0, column=1, padx=10, pady=5) @@ -59,15 +60,15 @@ def create_check_buttons(self): spaces_check = ttk.Checkbutton(self.app, text="Include Spaces", variable=self.include_spaces) spaces_check.grid(row=2, column=0, padx=10, pady=5, sticky="w") - - self.multiple_passphrases_var= tk.BooleanVar(value=False) - self.multiple_passphrases_check= ttk.Checkbutton(self.app, text="Generate multiple passphrases", variable=self.multiple_passphrases_var, command=self.toggle_multiple_passphrases) + + self.multiple_passphrases_var = tk.BooleanVar(value=False) + self.multiple_passphrases_check = ttk.Checkbutton(self.app, text="Generate multiple passphrases", variable=self.multiple_passphrases_var, command=self.toggle_multiple_passphrases) self.multiple_passphrases_check.grid(row=2, column=1, padx=10, pady=5, sticky="w") self.num_passphrases_label = ttk.Label(self.app, text="Number of passphrases:") - self.num_passphrases_spinbox = ttk.Spinbox(self.app, from_=1, to=5, state="disabled", validate="key", validatecommand=(self.app.register(self.validate_num_words), '%P')) + self.num_passphrases_spinbox = ttk.Spinbox(self.app, from_=1, to=5, state="disabled", validate="key", validatecommand=(self.app.register(self.validate_num_passphrases), '%P')) def create_generate_button(self): - generate_button = ttk.Button(self.app, text="Generate", command=self.generate_button_clicked) + generate_button = ttk.Button(self.app, text="Generate", command=self.generate_button_clicked_thread) generate_button.grid(row=4, column=0, columnspan=2, padx=10, pady=10) def create_feedback_labels(self): @@ -76,7 +77,7 @@ def create_feedback_labels(self): self.feedback_label = ttk.Label(self.app, text="", wraplength=300) self.feedback_label.grid(row=8, column=0, columnspan=2, padx=10, pady=5) - + self.feedback_crack_time = ttk.Label(self.app, text="", wraplength=300) self.feedback_crack_time.grid(row=9, column=0, columnspan=2, padx=10, pady=5) @@ -84,54 +85,62 @@ def create_dark_white_mode_button(self): self.dark_toggle = ttk.Button(self.app, text="Dark/White Mode", command=self.dark_white_mode_toggle) self.dark_toggle.grid(row=10, column=0, columnspan=2, padx=10, pady=10) - def validate_num_words(self, input): - if input.isdigit() or input == "": + if input.isdigit() and 3 <= int(input) <= 9 or input == "": return True else: return False - + def dark_white_mode_toggle(self): self.status = True if self.app["background"] == "#2d2d30" else False if not self.status: self.style.configure("TLabel", background="#2d2d30", foreground="white") self.style.configure("TButton", background="#2d2d30") - self.style.configure("TCheckbutton", background="#2d2d30",foreground="white") + self.style.configure("TCheckbutton", background="#2d2d30", foreground="white") self.dark_toggle.configure(style="Gray.TButton") self.style.configure("Gray.TButton", background="#2d2d30") self.app.configure(background="#2d2d30") else: self.style.configure("TLabel", background="white", foreground="black") - self.style.configure("TButton", background="white",foreground="black") - self.style.configure("TCheckbutton", background="white",foreground="black") + self.style.configure("TButton", background="white", foreground="black") + self.style.configure("TCheckbutton", background="white", foreground="black") self.app.configure(background="white") def generate_passphrase(self, num_words, add_numbers, add_special_chars, min_digits=1, max_digits=4, capitalize_percentage=60, include_spaces=True): - words = [secrets.choice(wordlist).capitalize() for _ in range(num_words)] - - if add_numbers: - num_digits = secrets.randbelow(max(max_digits - min_digits + 1, 1)) + min_digits - for _ in range(num_digits): - insert_pos = secrets.randbelow(max(1, len(words) + 1)) - words.insert(insert_pos, secrets.choice(string.digits)) - - if add_special_chars: - words.insert(secrets.randbelow(len(words) + 1), secrets.choice(string.punctuation)) - - secrets.SystemRandom().shuffle(words) - - for _ in range(round(len(words) * (capitalize_percentage / 100))): - random_word = secrets.randbelow(len(words)) - words[random_word] = words[random_word].capitalize() - - passphrase = ' '.join(words) if include_spaces else ''.join(words) - - if not check_pwned(passphrase): - return passphrase - - raise RuntimeError(f'Unable to generate a secure passphrase after attempts') - - + for _ in range(20000): # limit the number of attempts to 100 + words = [secrets.choice(wordlist).capitalize() for _ in range(num_words)] + + if add_numbers: + num_digits = secrets.randbelow(max(max_digits - min_digits + 1, 1)) + min_digits + for _ in range(num_digits): + insert_pos = secrets.randbelow(max(1, len(words) + 1)) + words.insert(insert_pos, secrets.choice(string.digits)) + + if add_special_chars: + words.insert(secrets.randbelow(len(words) + 1), secrets.choice(string.punctuation)) + + secrets.SystemRandom().shuffle(words) + + for _ in range(round(len(words) * (capitalize_percentage / 100))): + random_word = secrets.randbelow(len(words)) + words[random_word] = words[random_word].capitalize() + + passphrase = ' '.join(words) if include_spaces else ''.join(words) + + if not check_pwned(passphrase) and test_password(passphrase)['score'] == 4: + return passphrase + + raise RuntimeError(f'Unable to generate a secure passphrase after 100 attempts') + + def generate_button_clicked_thread(self): + threading.Thread(target=self.generate_button_clicked).start() + + def validate_num_passphrases(self, input): + if input.isdigit() and 2 <= int(input) <= 4 or input == "": + return True + else: + return False + def generate_button_clicked(self): num_words = self.num_words_entry.get() if num_words is None or num_words == "": @@ -142,7 +151,7 @@ def generate_button_clicked(self): if num_words <= 0: raise ValueError("Number of words must be a positive integer") if num_words > 10: - num_words = 10 + num_words = 10 if self.multiple_passphrases_var.get(): num_passphrases = int(self.num_passphrases_spinbox.get()) if num_passphrases > 5: @@ -150,18 +159,18 @@ def generate_button_clicked(self): else: num_passphrases = 1 passphrases = [] - + for i in range(num_passphrases): passphrases.append(self.generate_passphrase(num_words, self.numbers_var.get(), self.special_chars_var.get(), include_spaces=self.include_spaces.get())) - #Destroy previous labels, so it does not overlap. + # Destroy previous labels, so it does not overlap. for label in self.passphrase_labels: label.destroy() self.passphrases = passphrases # store the passphrases self.passphrase_labels = [] # clear the old labels for i, passphrase in enumerate(passphrases): - label = ttk.Label(self.app, text=f"Passphrase {i+1}: {passphrase}", wraplength=300) - label.grid(row=5+i, column=0, columnspan=2, padx=10, pady=5) + label = ttk.Label(self.app, text=f"Passphrase {i + 1}: {passphrase}", wraplength=300) + label.grid(row=5 + i, column=0, columnspan=2, padx=10, pady=5) label.bind("", self.copy_to_clipboard) # bind click event self.passphrase_labels.append(label) # store the label @@ -169,7 +178,7 @@ def generate_button_clicked(self): self.strength_label.config(text=f"Password Strength: {strength_result['score']}/4") self.feedback_label.config(text="\n".join(strength_result['feedback'])) self.feedback_crack_time.config(text="Time needed to crack password: " + strength_result['crack_time']) - + except ValueError as e: messagebox.showerror("Error", f"An error occurred while generating the passphrase:\n{e}") @@ -188,5 +197,6 @@ def copy_to_clipboard(self, event=None): def run(self): self.app.mainloop() + if __name__ == "__main__": PasswordGenerator().run()