view cutils/genpwd.py @ 247:435687f4e071

genpwd: FIX: Help message for the default
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 08 Feb 2025 11:44:58 +0100
parents c29266444003
children 4796e5da04ee
line wrap: on
line source

# -*- coding: utf-8 -*-
# :-
# :Copyright: (c) 2018 Franz Glasner
# :Copyright: (c) 2025 Franz Glasner
# :License:   BSD-3-Clause
# :-
r"""Generate passwords.

Usage: genpwd.py [ Options ] required_length

Options:

  --type, -t    web, web-safe, web-safe2, base64, base32, ascii85

:Author:  Franz Glasner

"""

from __future__ import (division, absolute_import, print_function)

import argparse
import base64
import binascii
import os
import sys

from . import (__version__, __revision__)


#
# Unreserved characters according to RFC 1738 (URL) **and** RFC 3986 (URI)
# No general delimiters and no sub-delimiters.
#
WEB_CHARS = (b"ABCDEFGHIJKLMNOPQRSTUVWYXZabcdefghijklmnopqrstuvwxyz"
             b"0123456789-._")
# WEB_CHARS without visually similar characters (0O, 1lI)
SAFE_WEB_CHARS = (b"ABCDEFGHJKLMNPQRSTUVWYXZabcdefghijkmnopqrstuvwxyz"
                  b"23456789-._")
# SAFE_WEB_CHARS with preference to punctuation
SAFE_WEB_CHARS_2 = b".-_" + SAFE_WEB_CHARS
# Unreserved characters from URI but with sub-delims allowed
URI_CHARS = WEB_CHARS + b"~" + b"!$&'()*+,;="
# URI_CHARS without visually similar characters
SAFE_URI_CHARS = SAFE_WEB_CHARS + b"~" + b"!$&'()*+,;="
# Just like SAFE_URI_CHARS but prefers punctuation characters
SAFE_URI_CHARS_2 = (b"~" + b"!$&'()*+,;=" + SAFE_WEB_CHARS
                    + b"~" + b"!$&'()*+,;=")
# All visible characters from ASCII character set but no space
FULL_ASCII = (b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
              b"abcdefghijklmnopqrstuvwxyz!#$%&/()*+-.,:;<=>?@^_`[\\]{|}'\"~")
#
# A safer variant of FULL_ASCII:
# - no characters that are visually similar (0O, 1lI)
# - no characters with dead keys on german keyboards
# - no backslash (too easily interpret as escape character
# - no single or double quotes
SAFE_ASCII = (b"23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
              b"abcdefghijkmnopqrstuvwxyz!#$%&/()*+-.,:;<=>?@_[]{|}~")
# just numeric and alphabetic
ALNUM = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
# safer alpha-numberic without visually similar characters
SAFE_ALNUM = b"23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"


PY2 = sys.version_info[0] <= 2


def main(argv=None):
    aparser = argparse.ArgumentParser(
        description="A simple password generator for password of a given"
                    " length within a character repertoire",
        fromfile_prefix_chars='@')
    aparser.add_argument(
        "--version", "-v", action="version",
        version="%s (rv:%s)" % (__version__, __revision__))
    aparser.add_argument(
        "-E", dest="use_bin_length", action="store_true",
        help="For some repertoires make OUTPUT-LENGTH the number of bytes"
             " that is to be read from random sources instead of output bytes")
    aparser.add_argument(
        "--repertoire", "--type", "-t",
        choices=("web", "safe-web", "safe-web-2",
                 "uri", "safe-uri", "safe-uri-2",
                 "ascii", "safe-ascii",
                 "alnum", "safe-alnum",
                 "bin-base64", "bin-urlsafe-base64", "bin-base32", "bin-hex",
                 "bin-ascii85",),
        default="safe-ascii",
        help="""
Select from a character repertoire.
All repertoires that start with "bin-" just encode the output of
"os.urandom()" with the selected encoder.
Default: safe-ascii
""")
    aparser.add_argument(
        "req_length", metavar="OUTPUT-LENGTH", type=int,
        help="The required length of the generated output")

    opts = aparser.parse_args(args=argv)

    if opts.repertoire == "web":
        pwd = gen_from_repertoire(opts.req_length, WEB_CHARS)
    elif opts.repertoire == "safe-web":
        pwd = gen_from_repertoire(opts.req_length, SAFE_WEB_CHARS)
    elif opts.repertoire == "safe-web-2":
        pwd = gen_from_repertoire(opts.req_length, SAFE_WEB_CHARS_2)
    elif opts.repertoire == "uri":
        pwd = gen_from_repertoire(opts.req_length, URI_CHARS)
    elif opts.repertoire == "safe-uri":
        pwd = gen_from_repertoire(opts.req_length, SAFE_URI_CHARS)
    elif opts.repertoire == "safe-uri-2":
        pwd = gen_from_repertoire(opts.req_length, SAFE_URI_CHARS_2)
    elif opts.repertoire == "ascii":
        pwd = gen_from_repertoire(opts.req_length, FULL_ASCII)
    elif opts.repertoire == "safe-ascii":
        pwd = gen_from_repertoire(opts.req_length, SAFE_ASCII)
    elif opts.repertoire == "alnum":
        pwd = gen_from_repertoire(opts.req_length, ALNUM)
    elif opts.repertoire == "safe-alnum":
        pwd = gen_from_repertoire(opts.req_length, SAFE_ALNUM)
    elif opts.repertoire == "bin-base64":
        encoder = base64.b64encode
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder,
                      rstrip_chars=b"=")
    elif opts.repertoire == "bin-urlsafe-base64":
        encoder = base64.urlsafe_b64encode
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder,
                      rstrip_chars=b"=")
    elif opts.repertoire == "bin-base32":
        encoder = base64.b32encode
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder,
                      rstrip_chars=b"=")
    elif opts.repertoire == "bin-ascii85":
        encoder = base64.a85encode
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder)
    elif opts.repertoire == "bin-hex":
        encoder = binascii.hexlify
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder)
    else:
        raise NotImplementedError("type not yet implemented: %s"
                                  % opts.repertoire)
    if opts.use_bin_length:
        if len(pwd) < opts.req_length:
            raise AssertionError("internal length mismatch")
    else:
        if len(pwd) != opts.req_length:
            raise AssertionError("internal length mismatch")
    if not PY2:
        pwd = pwd.decode("ascii")
    print(pwd)


def gen_from_repertoire(length, repertoire):
    """Select `length` characters randomly from given character repertoire
    `repertoire`.

    """
    assert len(repertoire) <= 256
    pwd = []
    while len(pwd) < length:
        rndbytes = os.urandom(16)
        for c in rndbytes:
            if PY2:
                c = ord(c)
            if c < len(repertoire):
                pwd.append(repertoire[c])
                if len(pwd) >= length:
                    break
    if PY2:
        pwd = b''.join(pwd)
    else:
        pwd = bytes(pwd)
    return pwd


def gen_bin(length, use_bin_length, encoder, rstrip_chars=None):
    """Generate from :func:`os.urandom` and just encode with given `encoder`.

    """
    pwd = encoder(os.urandom(length))
    return pwd.rstrip(rstrip_chars) if use_bin_length else pwd[:length]


if __name__ == "__main__":
    main()