From d86474a0db3567c4f0fd5c1d1fa9a888971fe7ff Mon Sep 17 00:00:00 2001 From: croketillo Date: Thu, 20 Jun 2024 22:51:48 +0200 Subject: [PATCH] Version 0.4.0 --- README.md | 33 ++++++-- README.rst | 35 +++++--- belch/belch.py | 225 ++++++++++++++++++++++++++++++------------------- setup.py | 4 +- test.py | 55 ++++++++++++ 5 files changed, 242 insertions(+), 110 deletions(-) create mode 100644 test.py diff --git a/README.md b/README.md index 2269e09..399a8ae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -

- ![PyPI - Version](https://img.shields.io/pypi/v/belch) ![GitHub License](https://img.shields.io/github/license/croketillo/belch) ![Pepy Total Downlods](https://img.shields.io/pepy/dt/belch) @@ -24,18 +22,35 @@ To generate passwords, run the following command from anywhere on your system: Follow the on-screen instructions to specify the password pattern and the number of passwords to generate. -

## Password Patterns -You can use the following characters in your pattern: +ou can use the following characters in your pattern: -- /d : Digit -- /c : Lowercase -- /C : Uppercase -- /e: Special characters -- /? : Random characters +/d : Digit +/c : Lowercase +/C : Uppercase +/e : Special characters +/? : Random characters +/@ : Mixed uppercase and lowercase +/& : Mixed uppercase, lowercase, and digits For example, the pattern [/C/c-pass-/d/?] will generate passwords with a combination of uppercase, lowercase, digits, and random characters in specified order. +### Example + +```bash +Available Patterns: +/d - Digit /c - Lowercase +/C - Uppercase /e - Special characters +/? - Random characters /@ - Mixed uppercase and lowercase +/& - Mixed uppercase, lowercase, and digits + +[>] Enter pattern: /C/c/d/e/?/@/& +[*] The maximum number of possible combinations is: 1037769600000000 +[>] Enter the number of passwords to generate (Enter for default: 1037769600000000): 100 +[>] Enter the file name (or press Enter to use passlist.txt): mypasswords.txt +[+] Passwords generated and stored in the file 'mypasswords.txt' in 0.02 seconds. +``` + ## License This project is licensed under the GNU-GPL License. See the LICENSE file for more details. diff --git a/README.rst b/README.rst index dd7c295..a5cdb24 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,7 @@ -BELCH Password List Generator -============================= - |PyPI - Version| |GitHub License| |Pepy Total Downlods| - -Description ------------ +BELCH Password List Generator +============================= BELCH Password List Generator is a simple tool to generate password lists based on a given pattern. You can specify the password pattern and @@ -37,18 +33,33 @@ the number of passwords to generate. Password Patterns ----------------- -You can use the following characters in your pattern: +ou can use the following characters in your pattern: -- /d : Digit -- /c : Lowercase -- /C : Uppercase -- /e: Special characters -- /? : Random characters +/d : Digit /c : Lowercase /C : Uppercase /e : Special characters /? : +Random characters /@ : Mixed uppercase and lowercase /& : Mixed +uppercase, lowercase, and digits For example, the pattern [/C/c-pass-/d/?] will generate passwords with a combination of uppercase, lowercase, digits, and random characters in specified order. +Example +~~~~~~~ + +.. code:: bash + + Available Patterns: + /d - Digit /c - Lowercase + /C - Uppercase /e - Special characters + /? - Random characters /@ - Mixed uppercase and lowercase + /& - Mixed uppercase, lowercase, and digits + + [>] Enter pattern: /C/c/d/e/?/@/& + [*] The maximum number of possible combinations is: 1037769600000000 + [>] Enter the number of passwords to generate (Enter for default: 1037769600000000): 100 + [>] Enter the file name (or press Enter to use passlist.txt): mypasswords.txt + [+] Passwords generated and stored in the file 'mypasswords.txt' in 0.02 seconds. + License ------- diff --git a/belch/belch.py b/belch/belch.py index 54515fa..d7d39e1 100644 --- a/belch/belch.py +++ b/belch/belch.py @@ -1,22 +1,16 @@ -#!/usr/bin/env python3 -"""BELCH Password list generator v0.3.11 - -Author: Croketillo - -Description: Generate a list of character strings according to a given pattern and stores it in a file. -""" import random import string import time import os import sys from tqdm import tqdm -from colorama import init,Style,Fore +from colorama import init, Style, Fore class PasswordGenerator: - """Generate passwods based in pattern""" + """Generate passwords based on a pattern.""" + def __init__(self, pattern): - self.pattern = pattern + self.pattern = pattern.strip() def generate_single(self): result = "" @@ -36,33 +30,47 @@ def generate_single(self): result += random.choice("!@#$%^&*(),.?\":{}|<>_-+/;[]") elif token == "?": result += random.choice(string.ascii_letters + string.digits + "!@#$%^&*(),.?\":{}|<>_-+/;[]") + elif token == "@": + result += random.choice(string.ascii_uppercase + string.ascii_lowercase) + elif token == "&": + result += random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) else: - result += char + token + result += "/" + token # Handle unknown token by treating as literal else: result += char i += 1 return result + + def count_char(self,string): + return len(string) def generate_multiple(self, count): generated_passwords = set() with tqdm(total=count, desc="Generating passwords", unit="passwords", ascii=" ░▒█") as pbar: - while len(generated_passwords) < count: - generated_password = self.generate_single() - if generated_password not in generated_passwords: - generated_passwords.add(generated_password) - pbar.update(1) + try: + while len(generated_passwords) < count: + generated_password = self.generate_single() + if generated_password not in generated_passwords: + generated_passwords.add(generated_password) + pbar.update(1) + except KeyboardInterrupt: + pbar.close() + print(Fore.LIGHTRED_EX + "\n\n[!] "+Fore.RESET+"Generation interrupted by user. Saving generated passwords so far...") return list(generated_passwords) def calculate_combinations(self): - """Calculate the total combinations""" + """Calculate the total combinations based on the pattern.""" total_combinations = 1 i = 0 while i < len(self.pattern): char = self.pattern[i] if char == "/": + if i + 1 >= len(self.pattern): + total_combinations *= 1 # Interprets '/' at the end as a literal character + break i += 1 control_char = self.pattern[i] repeat_factor = 1 @@ -70,13 +78,19 @@ def calculate_combinations(self): repeat_factor += 1 i += 1 if control_char == "C": - total_combinations *= len(string.digits) ** repeat_factor + total_combinations *= len(string.ascii_uppercase) ** repeat_factor elif control_char == "c": total_combinations *= len(string.ascii_lowercase) ** repeat_factor elif control_char == "d": total_combinations *= len(string.digits) ** repeat_factor - elif control_char in ("e", "?"): + elif control_char == "e": total_combinations *= len("!@#$%^&*(),.?\":{}|<>_-+/;[]") ** repeat_factor + elif control_char == "?": + total_combinations *= len(string.ascii_letters + string.digits + "!@#$%^&*(),.?\":{}|<>_-+/;[]") ** repeat_factor + elif control_char == "@": + total_combinations *= len(string.ascii_uppercase + string.ascii_lowercase) ** repeat_factor + elif control_char == "&": + total_combinations *= len(string.ascii_uppercase + string.ascii_lowercase + string.digits) ** repeat_factor else: total_combinations *= 1 # No control character, multiply by 1 i += 1 @@ -86,26 +100,33 @@ def calculate_combinations(self): def get_integer_input(prompt, max_value): """Gets an integer input from the user.""" while True: - value = input(prompt) - if value: - try: - value=int(value) - if 0 < value <= max_value: - return value - print(f"Please enter a number between 1 and {max_value}.") - except ValueError: - print("Invalid input. Please enter a number.") - else: - value=int(max_value) - return value + try: + value = input(prompt) + if value: + try: + value = int(value) + if 0 < value <= max_value: + return value + print(Fore.LIGHTRED_EX + "[!] "+Fore.RESET+f"Please enter a number between 1 and {max_value}.") + except ValueError: + print(Fore.LIGHTRED_EX + "[!] "+Fore.RESET+"Invalid input. Please enter a number." + Fore.RESET) + else: + return max_value + except KeyboardInterrupt: + print(Fore.LIGHTRED_EX + "\n\n[!] "+Fore.RESET+"Exit by user. Bye!") + sys.exit(0) def get_filename_input(): - """Gets the name of the user's file""" + """Gets the name of the user's file.""" current_directory = os.getcwd() default_filename = "passlist.txt" default_path = os.path.join(current_directory, default_filename) - user_input = input(f">>> Enter the file name (or press Enter to passlist.txt): ").strip() + try: + user_input = input("["+Fore.LIGHTGREEN_EX+">"+Fore.RESET+"] Enter the file name (or press Enter to use passlist.txt): ").strip() + except KeyboardInterrupt: + print(Fore.LIGHTRED_EX + "\n\n[!] "+Fore.RESET+"Exit by user. Bye!") + sys.exit(0) if user_input: return os.path.join(current_directory, user_input) @@ -113,68 +134,96 @@ def get_filename_input(): return default_path def show_logo(): - print(Fore.LIGHTMAGENTA_EX+""" -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@@@@@@@ *%@@@@@@@@@@ -@@@@@@@@@@@ *@@@@@@# %@@@@@@@@ -@@@@@@@@@@@ *@@@@@@@# @@@@@@@@ -@@@@@@@@@@@ *@@@@@@@# %@@@@@@@ -@@@@@@@@@@@ *@@@@@@@ #@@@@@@@@ -@@@@@@@@@@@ *@%%%#* *#@@@@@@@@@@ -@@@@@@@@@@@ *#%@@@@@@@@@@ -@@@@@@@@@@@ *@@@@@@%* *@@@@@@@@ -@@@@@@@@@@@ *@@@@@@@@* @@@@@@@ -@@@@@@@@@@@ *@@@@@@@@# #@@@@@@ -@@@@@@@@@@@ *@@@@@@@@ %@@@@@@ -@@@@@@@@@@@ *@@@@@@% %@@@@@@@ -@@@@@@@ *%@@@@@@@@@ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@@@@@@%###########################@@@@@@ -@@@@@@%# #@@@@@@ -@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@""") + print(Style.BRIGHT+"\n\t\t BELCH Password List Generator v 0.4.0 \t") + print(Style.DIM+"\t\t\t\tBy Croketillo") + print("\t\t\t [Ctrl + C] to EXIT \n") + +def print_columns(options, num_columns=2): + max_width = max(len(opt[0]) + len(opt[1]) + 3 for opt in options) + 2 # Calculate max width with some padding + wrapped_text = [] + + for i in range(0, len(options), num_columns): + line = "" + for j in range(num_columns): + if i + j < len(options): + opt, desc = options[i + j] + text = f"{opt} - {desc}" + line += text.ljust(max_width) + wrapped_text.append(line) + + for line in wrapped_text: + print(line) + +def calculate_weight_from_length(n_lines: int, length: int) -> str: + """ + Calculates the weight of a string based on its length in characters and the number of lines, in bytes, megabytes, or gigabytes. + + :param n_lines: The number of lines. + :param length: The length of the string in characters per line. + :return: The weight of the string in bytes, megabytes, or gigabytes. + """ + # Calculate total weight in bytes, assuming each line ends with a newline character + weight_in_bytes = (length + 1) * n_lines # +1 for newline character + weight_in_megabytes = weight_in_bytes / (1024 * 1024) + weight_in_gigabytes = weight_in_megabytes / 1024 + + if weight_in_gigabytes >= 1: + return f"{weight_in_gigabytes:.2f} GB" + elif weight_in_megabytes >= 0.01: + return f"{weight_in_megabytes:.2f} MB" + return f"{weight_in_bytes} bytes" def main(): - """Main function""" + """Main function.""" init(autoreset=True) show_logo() - print(Fore.LIGHTBLACK_EX+" Password List Generator v 0.3.11") - print(Fore.LIGHTBLACK_EX+" [Ctrl + c] to EXIT \n") - print(Style.BRIGHT+"TO SET PATTERN:") - - from rich.console import Console - from rich.table import Table - - - table = Table() - table.add_column("Pattern command", style="cyan", justify="center") - table.add_column("Description", style="magenta") - table.add_row("/d", "Digit") - table.add_row("/c", "Lowercase") - table.add_row("/C", "Uppercase") - table.add_row("/e", "Special characters") - table.add_row("/?", "Random characters") - - console = Console() - console.print(table) + + options = [ + ("/d", "Digit"), + ("/c", "Lowercase"), + ("/C", "Uppercase"), + ("/e", "Special characters"), + ("/?", "Random characters"), + ("/@", "Mixed uppercase and lowercase"), + ("/&", "Mixed uppercase, lowercase and digits") + ] + print(Style.BRIGHT + "Available Patterns:") + print_columns(options) + print("_"*80) - try: - user_input = input(">>> Enter pattern: ") - password_generator = PasswordGenerator(user_input) + while True: + try: + user_input = input("\n["+Fore.LIGHTGREEN_EX+">"+Fore.RESET+"] Enter pattern: ").strip() + if not user_input or any(char.isspace() for char in user_input): + print(Fore.LIGHTRED_EX + "[!] "+Fore.RESET+"Invalid pattern. Pattern cannot be empty or contain spaces.") + continue + + password_generator = PasswordGenerator(user_input) + break + except KeyboardInterrupt: + print(Fore.LIGHTRED_EX + "\n\n[!] "+Fore.RESET+"Exit by user. Bye!") + sys.exit(0) + try: max_combinations = password_generator.calculate_combinations() - print(Style.DIM+f"(The maximum number of possible combinations is: {max_combinations})") + look_n_char=password_generator.generate_single() + nchar=password_generator.count_char(look_n_char) + + file_weight=calculate_weight_from_length(max_combinations,nchar) - n_password = get_integer_input(f">>> Enter the number of passwords to generate (Enter for default: {max_combinations}): ", max_combinations) + print(Style.DIM + f"["+Fore.YELLOW+"i"+Fore.RESET+"] The maximum number of possible combinations is: {max_combinations}. ({file_weight})") + + n_password = get_integer_input(f"["+Fore.LIGHTGREEN_EX+">"+Fore.RESET+f"] Enter the number of passwords to generate (Enter for default: {max_combinations}): ", max_combinations) file_name = get_filename_input() start_time = time.time() + print("_"*80) + generated_passwords = password_generator.generate_multiple(n_password) + with open(file_name, "w") as file: - passwords = password_generator.generate_multiple(n_password) - for generated_password in passwords: + for generated_password in generated_passwords: file.write(generated_password + "\n") end_time = time.time() original_duration = end_time - start_time @@ -183,15 +232,17 @@ def main(): minutes, seconds = divmod(original_duration, 60) if minutes > 60: hours, minutes = divmod(minutes, 60) - print(f"Passwords generated and stored in the file '{file_name}' in {int(hours)} hours, {int(minutes)} minutes, and {seconds:.2f} seconds.") + print("_"*80) + print(Fore.GREEN+f"\n\n[+] "+Fore.RESET+f"Passwords generated and stored in the file '{file_name}' in {int(hours)} hours, {int(minutes)} minutes, and {seconds:.2f} seconds.") else: - print(f"Passwords generated and stored in the file '{file_name}' in {int(minutes)} minutes and {seconds:.2f} seconds.") + print("_"*80) + print(Fore.GREEN+f"\n\n[+] "+Fore.RESET+f"Passwords generated and stored in the file '{file_name}' in {int(minutes)} minutes and {seconds:.2f} seconds.") else: - print(f"Passwords generated and stored in the file '{file_name}' in {original_duration:.2f} seconds.") + print("_"*80) + print(Fore.GREEN+"\n\n[+] "+Fore.RESET+f"Passwords generated and stored in the file '{file_name}' in {original_duration:.2f} seconds.") except KeyboardInterrupt: - print(Fore.LIGHTRED_EX+"\n\n\tExit by user. Bye!") + print(Fore.LIGHTRED_EX + "\n\n[!] "+Fore.RESET+"Exit by user. Bye!") sys.exit(0) - if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/setup.py b/setup.py index 788ba15..415fbbc 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ def read(file_name=None, is_encoding=True, ignore_raises=False): setup( name='belch', - version='0.3.11', + version='0.4.0', description='Password list generator', long_description=read("README.rst"), url='https://github.com/croketillo/belch', @@ -109,7 +109,7 @@ def read(file_name=None, is_encoding=True, ignore_raises=False): 'Topic :: Security', 'Topic :: Utilities', ], - keywords='color pattern console colorpattern', + keywords='Password, Password generator', entry_points={ 'console_scripts': [ 'belch = belch.belch:main', diff --git a/test.py b/test.py new file mode 100644 index 0000000..8dde5a2 --- /dev/null +++ b/test.py @@ -0,0 +1,55 @@ +import unittest +import os +import string +from belch import PasswordGenerator + +class TestPasswordGenerator(unittest.TestCase): + + def setUp(self): + self.pattern = "/C/c/d/e/?/@/&" + self.generator = PasswordGenerator(self.pattern) + + def test_generate_single(self): + password = self.generator.generate_single() + self.assertEqual(len(password), 7) + self.assertRegex(password[0], r'[A-Z]') + self.assertRegex(password[1], r'[a-z]') + self.assertRegex(password[2], r'\d') + self.assertRegex(password[3], r'[!@#$%^&*(),.?":{}|<>_\-+/;\[\]]') # Escaping the dash and brackets + self.assertRegex(password[4], r'[A-Za-z\d!@#$%^&*(),.?":{}|<>_\-+/;\[\]]') + self.assertRegex(password[5], r'[A-Za-z]') + self.assertRegex(password[6], r'[A-Za-z\d]') + + def test_generate_multiple(self): + passwords = self.generator.generate_multiple(100) + self.assertEqual(len(passwords), 100) + for password in passwords: + self.assertEqual(len(password), 7) + + def test_calculate_combinations(self): + combinations = self.generator.calculate_combinations() + expected_combinations = (len(string.ascii_uppercase) * len(string.ascii_lowercase) * + len(string.digits) * len("!@#$%^&*(),.?\":{}|<>_-+/;[]") * + len(string.ascii_letters + string.digits + "!@#$%^&*(),.?\":{}|<>_-+/;[]") * + len(string.ascii_uppercase + string.ascii_lowercase) * + len(string.ascii_uppercase + string.ascii_lowercase + string.digits)) + self.assertEqual(combinations, expected_combinations) + + def test_no_duplicates_in_file(self): + filename = 'test_passwords.txt' + patterns = "/C/c/d/e/?/@/&" + generator = PasswordGenerator(patterns) + passwords = generator.generate_multiple(100) + with open(filename, 'w') as f: + for password in passwords: + f.write(password + '\n') + + with open(filename, 'r') as f: + lines = f.readlines() + + os.remove(filename) # Clean up test file + + self.assertEqual(len(lines), len(set(lines))) + +if __name__ == '__main__': + unittest.main()