I took my code from this answer by me, I found it useful, so I developed it further, and here is the result.
This script generates cryptographically secure passwords. The characters used by the passwords are chosen from the 94 printable ASCII characters that aren't blank characters or control characters.
The characters are split into 4 categories: 26 lowercase letters, 26 UPPERCASE letters, 10 digits and 32 punctuations. Each password must contain UPPERCASE letters, lowercase letters, and digits, with optional punctuations.
The length of the passwords is 8 to 94 inclusive if special characters are allowed, or 8 to 62 if they aren't. The passwords can contain unique characters only and also duplicates, whether passwords will contain duplicate characters is controlled by an optional argument. And the count of characters from each of the categories in the passwords are approximately the same.
Code
import random import secrets from functools import reduce from operator import iconcat from string import ascii_lowercase, ascii_uppercase, digits, punctuation from typing import Generator, List CHARSETS = ( (ascii_lowercase, ascii_uppercase, digits), (ascii_lowercase, ascii_uppercase, punctuation, digits) ) COUNTS = ( ((3, 36), (2, 10)), ((4, 68), (3, 42), (2, 10)) ) def choose_charset(charset: str, number: int, unique: bool = False) -> List[str]: if not unique: return [secrets.choice(charset) for _ in range(number)] charset = list(charset) choices = [] for _ in range(number): choice = secrets.choice(charset) choices.append(choice) charset.remove(choice) return choices def split(number: int, symbol: bool = False) -> Generator[int, None, None]: for a, b in COUNTS[symbol]: n = max( 2 + secrets.randbelow( number // a - 1 ), number - b ) number -= n yield n if number > 10: raise ValueError('remaining number is too big') yield number def generate_password(length: int, unique: bool = False, symbol: bool = True) -> str: if not 8 <= length <= (62, 94)[symbol]: raise ValueError( 'argument `length` should be an `int` between 8 and 94, or between 8 and 62 if symbols are not allowed') password = reduce( iconcat, map( choose_charset, CHARSETS[symbol], split(length, symbol), [unique]*4 ), [] ) random.shuffle(password) return ''.join(password) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser( prog='Password_Generator', description='This program generates cryptographically secure passwords.' ) parser.add_argument( 'length', type=int, help='length of the passwords to be generated' ) unique_parser = parser.add_mutually_exclusive_group(required=False) unique_parser.add_argument( '-U', '--unique', dest='unique', action='store_true', help='specifies passwords should contain unique characters only' ) unique_parser.add_argument( '-NU', '--no-unique', dest='unique', action='store_false', help='specifies passwords should not contain unique characters only' ) parser.set_defaults(unique=False) symbol_parser = parser.add_mutually_exclusive_group(required=False) symbol_parser.add_argument( '-S', '--symbol', dest='symbol', action='store_true', help='specifies passwords should contain special characters' ) symbol_parser.add_argument( '-NS', '--no-symbol', dest='symbol', action='store_false', help='specifies passwords should not contain special characters' ) parser.set_defaults(symbol=True) parser.add_argument( '-C', '--count', type=int, default=1, help='specifies the number of passwords to generate, default 1' ) namespace = parser.parse_args() length, unique, symbol, count = [ getattr(namespace, name) for name in ('length', 'unique', 'symbol', 'count')] for _ in range(count): print(generate_password(length, unique, symbol))
Help message
PS C:\Users\Xeni> D:\MyScript\password_generator.py -h usage: Password_Generator [-h] [-U | -NU] [-S | -NS] [-C COUNT] length This program generates cryptographically secure passwords. positional arguments: length length of the passwords to be generated options: -h, --help show this help message and exit -U, --unique specifies passwords should contain unique characters only -NU, --no-unique specifies passwords should not contain unique characters only -S, --symbol specifies passwords should contain special characters -NS, --no-symbol specifies passwords should not contain special characters -C COUNT, --count COUNT specifies the number of passwords to generate, default 1
Example usage
PS C:\Users\Xeni> D:\MyScript\password_generator.py 16 -C 8 1o71NV8{rt*.3l4W L533*8X1m$9`!w6R 9#0<W4~ZuK#"51Vd V9y93Y^2B34Go71/ 50]9o1]E1&ap6HU# ~42pL"46T'7633zd 6Zw1a9z"F16H7~3Z 2Ab7S<r82o7_KN49 PS C:\Users\Xeni> D:\MyScript\password_generator.py 16 -U -C 8 xt7)kG48O9KW\^0] Pz02ac51>8_UY43g 6[kYF?0H98Oq3'21 Fp50N>P47n+369A1 9v743R0%V5\2s186 Dbr1%\0a}486TQF; K?l1Cz;92Wn7|Z54 1a@7]4yBsCx52Y0G PS C:\Users\Xeni> D:\MyScript\password_generator.py 16 -U -NS -C 8 c0YgQ9C54k86Ff3m Dwd3W412059ujm8H 17pWJvhyC9b82460 Qs52409Az673R18Z 89R2Pk6z401nN73H 7gDcO30yxG54d86K i62s1kQC89IuBb45 iw02Zn96Ez1xQY8N
As the program became sufficiently complex I wanted it to be reviewed, in particular this is the first time I have ever used argparse
. I would like my code to be more concise and efficient.
import typer
. You elected to not write any """docstrings""". The identifiersa, b
are not well chosen. There's no unit tests, and since you rely on global PRNG state there won't be. Dup suppression is an odd choice, as it reduces entropy. Choosing to suppress punctuation near start / end of password would make good UX sense, in order to make double-click copy-n-paste a little easier.\$\endgroup\$