3
\$\begingroup\$

After giving a couple of well-received answers to similar questions, I thought I'd try to come up with my own solution.

I believe that a good password generator can and should be quite simple without sacrificing security or usability.

Also, this is the first time I actually code a command-line utility (as a Windows user, command-line tools aren't part of my usual work-flow).

I'm interested in any remark about my code, especially regarding security and features.

''' Command-line utility program for generating passwords ''' from argparse import ArgumentParser, RawTextHelpFormatter import secrets from string import ascii_lowercase, ascii_uppercase, digits, punctuation ESCAPES = { r'\l' : ascii_lowercase, r'\u' : ascii_uppercase, r'\d' : digits, r'\s' : punctuation } def generate_password(length, alphabet): return ''.join(secrets.choice(alphabet) for _ in range(length)) def _parse_alphabet(specifiers): ranges = [] for escape, characters in ESCAPES.items(): if escape in specifiers: ranges.append(characters) specifiers = specifiers.replace(escape, '') ranges.append(specifiers) return ''.join(set(''.join(ranges))) if __name__ == '__main__': parser = ArgumentParser( prog='Password generator', formatter_class=RawTextHelpFormatter) parser.add_argument( '-l', '--length', default=32, type=int, help='length of password to be generated') parser.add_argument( '-a', '--alphabet', default=r'\l\u\d\s', help=r'''alphabet to use for password generation. The following escape sequences are supported: * \u: uppercase ASCII letters. * \l: lowercase ASCII letters. * \d: digits. * \s: other printable ASCII characters. Any other specified character will also be included.''') args = parser.parse_args() print(generate_password(args.length, _parse_alphabet(args.alphabet))) 

Example uses:

# show help (base) [...]>python password.py -h usage: Password generator [-h] [-l LENGTH] [-a ALPHABET] optional arguments: -h, --help show this help message and exit -l LENGTH, --length LENGTH length of password to be generated -a ALPHABET, --alphabet ALPHABET alphabet to use for password generation. The following escape sequences are supported: * \u: uppercase ASCII letters. * \l: lowercase ASCII letters. * \d: digits. * \s: other printable ASCII characters. Any other specified character will also be included. # generate a password with default settings: (base) [...]>python password.py y3S%iR!O=zNhcWYf-f9@87m_FC'WGr+^ # generate a 6-digit PIN (eg. for phone unlocking): (base) [...]>python password.py -l 6 -a \d 310696 # generate a password with smaller length and only some special chars (eg. for website compatibility): (base) [...]>python password.py -l 16 -a "\u\l\d_-&@!?" bP7L?_uuGbdHFww@ # generate a long password: (base) [...]>python password.py -l 250 FO_Hy1O,7#z}M'5K/,=:Al"dK-w{VIC:G$[Xqm|GRw8Uou@d^peD1vk$:76)n#28f112w"vz<F+-c\it$7@fb9Xq@3\[)7+*R7PTa(gdMb&\,?n6GRT93jD'L!6Ww)u:-}rWyXq&?V!pw>9<&.Nt4S!+a9X6f~z+?DZeB:);`-!{K;ftB(/;"TEeKi%yV,-,H3{2!x2Y"]'?[3$/'QM=K5mSo[!D~#;!iuY]=BF3=LLkGK92Nm4kttc\*S # generate a password with non-ASCII characters: (base) [...]>python password.py -a µùûÜ ùùùÜùùµÜܵµµÜùûÜûµÜÜÜûûÜûûùùùûܵ 
\$\endgroup\$
2
  • \$\begingroup\$I can't find many flaws to your code, except for one. Most websites require passwords to contain at least one character from each of the catergories: lowercase, UPPERCASE and digits, with optional special characters. The first three must be contained by the password, and yet your code doesn't guarantee that the password will contain all three, it generates invalid passwords. So it needs to be fixed.\$\endgroup\$CommentedMay 25, 2023 at 4:22
  • \$\begingroup\$Good point. I considered the issue, but the issue is that there are about as many different password validation rules than there are websites, with variations like "characters from 3 out of 4 character subsets", and handling them made usage too complex (especially combined with supporting only a subset of special chars, which is a very prevalent constraint). I implicitly rely on the fact that, given a long enough passwords, meeting any requirement of this type is highly probable. If it fails, generate a new password.\$\endgroup\$
    – gazoh
    CommentedMay 25, 2023 at 11:11

1 Answer 1

3
\$\begingroup\$

Configuring argparse is real code: put it in a function. There is nothing trivial about configuring argparse. Put the code in a proper function -- for maintainability, flexibility, testability, etc.

Pass the args explicitly to argparse. Don't rely on its default use of sys.argv. This is another testability consideration.

When feasible, avoid RawTextHelpFormatter. It causes your program's help text to ignore things like dynamic terminal widths. In this case, a little wordsmithing can work around the perceived need for it. With program help text (especially for arguments and options), brevity is crucial. When facing complexities needing further elaboration, keep the argument help texts brief and rely on an argparse epilog for further explanation.

Speaking of brevity, consider using argparse metavar. It can reduce the visual weight/clutter of the generated help text.

List the long options first. It enhances help text readability.

Define constants for the default alphabet and length. You need to use each in two places.

A simpler alternative for _parse_alphabet(). Perhaps I overlooked something, but I think you can start with the user-supplied alphabet and just replace all of the escapes. In the illustration below, sorted() is neither necessary nor harmful for the password algorithm, but it is helpful for any debugging/testing you might want to perform.

import sys import secrets from argparse import ArgumentParser from string import ascii_lowercase, ascii_uppercase, digits, punctuation ESCAPES = { r'\l': ascii_lowercase, r'\u': ascii_uppercase, r'\d': digits, r'\s': punctuation, } DEFAULT_ALPHABET = ''.join(ESCAPES.keys()) DEFAULT_LENTH = 32 def main(args): opts = parse_args(args) alpha = _parse_alphabet(opts.alphabet) p = generate_password(opts.length, alpha) print(p) def parse_args(args): p = ArgumentParser(prog = 'Password generator') p.add_argument( '--length', '-l', default = DEFAULT_LENTH, type = int, metavar = 'N', help = f'password length [default: {DEFAULT_LENTH}]', ) p.add_argument( '--alphabet', '-a', default = DEFAULT_ALPHABET, metavar = 'A', help = ( r'password alphabet. Supported escapes: ' r'\u upper ASCII, \l lower ASCII, \d digits, ' rf'\s other printable ASCII [default: "{DEFAULT_ALPHABET}"]' ), ) return p.parse_args(args) def _parse_alphabet(alpha): for e, chars in ESCAPES.items(): alpha = alpha.replace(e, chars) return ''.join(sorted(set(alpha))) def generate_password(length, alphabet): return ''.join(secrets.choice(alphabet) for _ in range(length)) if __name__ == '__main__': main(sys.argv[1:]) 
\$\endgroup\$

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.