view cutils/genpwd.py @ 240:32616df2732e

Renamed algorithms/repertoire selection: use a "bin-" prefix when just the output of os.urandom() is encoded
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 08 Feb 2025 09:37:48 +0100
parents 1eae57292c7c
children d4501acb0a7c
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 os
import sys

from . import (__version__, __revision__)


WEB_CHARS = b"ABCDEFGHIJKLMNOPQRSTUVWYXZabcdefghijklmnopqrstuvwxyz" \
            b"0123456789.,-_;!()[]{}*"
WEB_SAFE_CHARS = b"ABCDEFGHJKLMNPQRSTUVWYXZabcdefghijkmnopqrstuvwxyz" \
                 b"23456789.,-_;!"
WEB_SAFE2_CHARS = b".,-_;!" + WEB_SAFE_CHARS   # prefer punctionation chars

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", "web-safe", "web-safe2",
                 "bin-base64", "bin-urlsafe-base64", "bin-base32",
                 "bin-ascii85", ),
        default="web-safe2",
        help="Select the character repertoire. Default: web-safe2")
    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 == "web-safe":
        pwd = gen_from_repertoire(opts.req_length, WEB_SAFE_CHARS)
    elif opts.repertoire == "web-safe2":
        pwd = gen_from_repertoire(opts.req_length, WEB_SAFE2_CHARS)
    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)
    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()