#!/usr/bin/env python3 # # Author : Dennis Eriksen # File : makepass.py # Created : 2023-09-14 # License : BSD-3-Clause # # Copyright (c) 2018-2023 Dennis Eriksen import argparse import os import random import sys # Global variables MAX = 255 RANGE_MAX = 42 RANGE_MIN = 8 PASS_WORDS = 8 LOWER = "abcdefghijklmnopqrstuvwxyz" UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" DIGIT = "0123456789" OTHER = "!$%&#/()=?+-_,.;:<>[]{}|@*" ALPHA = LOWER + UPPER ALNUM = ALPHA + DIGIT EVERY = ALNUM + OTHER NORMAL = ALNUM + "_-" SPECIAL = EVERY DEFAULT_LENGTH = 0 DEFAULT_NUMBER = 10 DEFAULT_WORDLIST = "/usr/share/dict/words" class CommonPrintData: def __init__(self, length, printbool, printlen, col_width, col_num): self.length = length self.printbool = printbool self.printlen = printlen self.col_num = col_num self.col_width = col_width # # Main function # def main(): normal = os.getenv("MAKEPASS_NORMAL") or NORMAL special = os.getenv("MAKEPASS_SPECIAL") or SPECIAL wordlist_file = os.getenv("MAKEPASS_WORDLIST") or DEFAULT_WORDLIST # Initialize parser parser = argparse.ArgumentParser( description="Process some strings.", add_help=False ) # Add arguments parser.add_argument("-h", "--help", action="store_true") parser.add_argument("-l", "--length", type=int, help="Length of strings") parser.add_argument("-n", "--number", type=int, help="Number of strings") parser.add_argument("-p", "--printlen", action="store_true") parser.add_argument("arg_length", type=int, nargs="?") # Parse arguments args = parser.parse_args() if args.help: usage() # Some error-checking # length try: # env or default length = int(os.getenv("MAKEPASS_LENGTH") or DEFAULT_LENGTH) # -l flag if args.length is not None: if args.length < 0 or MAX < args.length: raise ValueError("-l") length = args.length # solo argument if args.arg_length is not None: length = args.arg_length # check result if length < 0 or MAX < length: raise ValueError("length") except ValueError as e: print(f"{e} must be a number between 0 and {MAX}") sys.exit(1) # number try: # env or default number = int(os.getenv("MAKEPASS_NUMBER") or DEFAULT_NUMBER) # -n flag if args.number is not None: if args.number < 1 or MAX < args.number: raise ValueError("-n") number = args.number # check result if number < 1 or MAX < number: raise ValueError("number") except ValueError as e: print(f"{e} must be a number between 1 and {MAX}") sys.exit(1) # Set some other stuff printbool = args.printlen if printbool: printlen = 3 if length < 100 else 4 else: printlen = 0 # get terminal width try: xwidth = os.get_terminal_size().columns except Exception as _: xwidth = 1 # get width of columns to print if length == 0: col_width = RANGE_MAX + 2 else: col_width = length + 2 # number of columns to print col_num = int(xwidth / (col_width + printlen)) if col_num == 0: col_num = 1 # config config = CommonPrintData( length=length, printbool=printbool, printlen=printlen, col_width=col_width, col_num=col_num, ) # # print passwords # # Generate and print normal and special passwords print_columns("Normal passwords", number, normal, config) print() print_columns( "Passwords with special characters", int(number / 3 * 2 + 1), special, config ) # Generate and print passphrases if wordlist exists if os.path.isfile(wordlist_file): print() print("Generating passphrases: ") passphrase(int(number / 2), wordlist_file) # # Print passwords in neat columns # def print_columns(title: str, num: int, chars: str, c: CommonPrintData): # Generate random strings strings = [randstring(c.length, chars) for _ in range(num)] print(f"{title}:") for i in range(num): # Print the length of the string if printlen is set if c.printbool: print(f"{len(strings[i]):0{c.printlen-1}}", end=" ") # Print the string, left justified based on column width print(f"{strings[i]:<{c.col_width}}", end="") # If we have printed enough strings for a single line or the # rest of strings are fewer than column number, we break lines. i += 1 if i % c.col_num == 0 or (i == num and i % c.col_num > 0): print() # # Generate random strings that can work as passwords # def randstring(length, chars): """ Generate a random password of the specified length. Use special characters if special_chars option is True. """ if length == 0: length = random.randint(RANGE_MIN, RANGE_MAX) password = [] password.append(random.choice(ALPHA)) for i in range(length - 2): password.append(random.choice(chars)) password.append(random.choice(ALNUM)) # Randomly reorder the characters return "".join(password) # # Passphrases! # def passphrase(number, file_name): """ Generate a number of random passphrases from the wordlist. """ try: with open(file_name, "r") as f: lines = f.read().splitlines() except IOError: print(f"Cannot open file: {file_name}") sys.exit(1) for i in range(number): word_list = random.choices(lines, k=5) passphrase = "-".join(word_list) print(passphrase) def usage(): text = """NAME makepass - create several random passwords SYNOPSIS makepass [OPTIONS] [NUM] If a NUM is provided, passwords will be NUM characters long. By default `makepass` will output passwords from the three following classes: - Normal passwords - random strings with letters (both lower and upper case), numbers, and dashes and underscores. - Passwords with special characters - random strings generated from lower and upper case letters, numbers, and the following characters: !#$%&/()=?+-_,.;:<>[]{}|@* - Passphrases - if we find a dictionary, a series of eight random words from the dictionary, separated by dashes. The number of words can not be changed, but you do not have to use all of them. Use as mane as you want. The first character will always be alphabetic, and the last will always be alphanumeric. DESCRIPTION makepass has the following options: -h output this help-text -l length of passwords. See MAKEPASS_LENGTH below -n number of passwords. See MAKEPASS_NUMBER below -p print length of number ENVIRONMENT makepass examines the following environmental variables. MAKEPASS_LENGTH Specifies the length of passwords. Valid values are 0-255. If 0, a random value between 8 and 42 will be used for each password. -l overrides this environmental variable, and the argument NUM overrides that again. So `MAKEPASS_LENGTH=10 makepass -l 12 14` will give passwords that are 14 characters long, even though both -l and MAKEPASS_LENGTH also specifies a length. MAKEPASS_NUMBER The number of passwords to generate. This formula is used to determine how many passwords from each group should be generated: - (n) normal passwords - (n / 3 * 2 + 1) special passwords - (n / 2) passphrases Where n is 10 by default. Valid values for n are 1-255. Floating-poing math is not used, so results may vary. MAKEPASS_PRINTLEN If 1, print length of all passwords. If 0, don\'t. MAKEPASS_NORMAL String of characters from which to generate "normal" passwords. Defaults to: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_ MAKEPASS_SPECIAL String of characters from which to generate passwords with special characters. Defaults to the same characters as in MAKEPASS_NORMAL, plus these: !#$%&/()=?+-_,.;:<>[]{}|@* MAKEPASS_WORDLIST Specifies the dictionary we find words for passphrases in. If this is unset or empty, we try "/usr/share/dict/words". If that file does not exist, no passphrases will be provided. AUTHOR Dennis Eriksen """ print(text) sys.exit(0) if __name__ == "__main__": main()