view cutils/genpwd.py @ 252:0a2a162c5ad7

genpwd: Wording in help and docs
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 08 Feb 2025 13:42:16 +0100
parents 14bb7423445d
children 4314ee20927a
line wrap: on
line source

# -*- coding: utf-8 -*-
# :-
# :Copyright: (c) 2018 Franz Glasner
# :Copyright: (c) 2025 Franz Glasner
# :License:   BSD-3-Clause
# :-
r"""A simple password generator to generate random passwords from selected
character repertoires.

Use :command:`genpwd.py --help' for a detailed help message.

"""

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 passwords with a given"
                    " length within a selected character repertoire",
        fromfile_prefix_chars='@')
    aparser.add_argument(
        "--version", "-v", action="version",
        version="%s (rv:%s)" % (__version__, __revision__))
    group = aparser.add_mutually_exclusive_group()
    group.add_argument(
        "--algorithm", "-a",
        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 an algorithm and (implicitly) a character repertoire.
All repertoires that start with `bin-' just encode the output of
"os.urandom()" with the selected encoder.
All repertoires that end with `-safe' or `safe-2' do not contain visually
similar characters (currently `0O' or `Il1').
All repertoires that end with `-2' are variants with a bias to punctuation
characters.
This is incompatible with option `--repertoire'.
Default: safe-ascii""")
    group.add_argument(
        "--repertoire", "-r",
        action="store", metavar="REPERTOIRE",
        help="""
Select from given character repertoire.
The repertoire must be characters from the ISO-8859-15 character set.
An empty REPERTOIRE selects implicitly the default algorithm.
This is incompatible with option `--algorithm'.""")
    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(
        "req_length", metavar="OUTPUT-LENGTH", type=int,
        help="The required length of the generated output")

    opts = aparser.parse_args(args=argv)

    if opts.repertoire:
        try:
            repertoire = (opts.repertoire
                          if PY2
                          else opts.repertoire.encode("iso-8859-15"))
        except UnicodeError:
            raise ValueError("non ISO-8859-15 character in given repertoire")
        pwd = gen_from_repertoire(opts.req_length, repertoire)
    elif opts.algorithm == "web":
        pwd = gen_from_repertoire(opts.req_length, WEB_CHARS)
    elif opts.algorithm == "safe-web":
        pwd = gen_from_repertoire(opts.req_length, SAFE_WEB_CHARS)
    elif opts.algorithm == "safe-web-2":
        pwd = gen_from_repertoire(opts.req_length, SAFE_WEB_CHARS_2)
    elif opts.algorithm == "uri":
        pwd = gen_from_repertoire(opts.req_length, URI_CHARS)
    elif opts.algorithm == "safe-uri":
        pwd = gen_from_repertoire(opts.req_length, SAFE_URI_CHARS)
    elif opts.algorithm == "safe-uri-2":
        pwd = gen_from_repertoire(opts.req_length, SAFE_URI_CHARS_2)
    elif opts.algorithm == "ascii":
        pwd = gen_from_repertoire(opts.req_length, FULL_ASCII)
    elif opts.algorithm == "safe-ascii":
        pwd = gen_from_repertoire(opts.req_length, SAFE_ASCII)
    elif opts.algorithm == "alnum":
        pwd = gen_from_repertoire(opts.req_length, ALNUM)
    elif opts.algorithm == "safe-alnum":
        pwd = gen_from_repertoire(opts.req_length, SAFE_ALNUM)
    elif opts.algorithm == "bin-base64":
        encoder = base64.b64encode
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder,
                      rstrip_chars=b"=")
    elif opts.algorithm == "bin-urlsafe-base64":
        encoder = base64.urlsafe_b64encode
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder,
                      rstrip_chars=b"=")
    elif opts.algorithm == "bin-base32":
        encoder = base64.b32encode
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder,
                      rstrip_chars=b"=")
    elif opts.algorithm == "bin-ascii85":
        encoder = base64.a85encode
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder)
    elif opts.algorithm == "bin-hex":
        encoder = binascii.hexlify
        pwd = gen_bin(opts.req_length, opts.use_bin_length, encoder)
    else:
        raise NotImplementedError("algorithm not yet implemented: %s"
                                  % opts.algorithm)
    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 PY2:
        print(pwd)
        sys.stdout.flush()
    else:
        sys.stdout.buffer.write(pwd)
        sys.stdout.buffer.write(b'\n')
        sys.stdout.buffer.flush()


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()