#!/usr/bin/env python

"""Generate random passwords in a hopefully secure manner. By default, `pw`
generates completely random passwords. To support insecure systems, you can
specify required character groups. To support humans, you can generate
pronounceable passwords. To break things, you can generate passwords from a
large unicode character set.

"""

import argparse
import string
import sys
import re
import pickle
import os
from itertools import chain
import operator
import codecs
from bisect import bisect
from collections import Counter, defaultdict

# The python3 version is more secure
if sys.version_info >= (3, 0):
    import secrets

    RG = secrets.SystemRandom()
else:
    import random

    RG = random.SystemRandom()

REGEX_LOWERCASE = re.compile("[{:s}]".format("".join(string.ascii_lowercase)))
REGEX_UPPERCASE = re.compile("[{:s}]".format("".join(string.ascii_uppercase)))
REGEX_DIGITS = re.compile("[{:s}]".format("".join(string.digits)))
REGEX_SPECIAL = re.compile("[{:s}]".format(
    re.escape("".join(string.punctuation))))

UNICODE_RANGE = chain(range(0x20, 0x7E), range(0xA1, 0x27F0))

MARKOV_DATA_ENCODED = """
gANjY29sbGVjdGlvbnMKZGVmYXVsdGRpY3QKcQBjY29sbGVjdGlvbnMKQ291bnRlcgpxAYVxAlJx
AyhOaAF9cQQoWAEAAABhcQVNSGNYAQAAAHZxBk3SFFgBAAAAYnEHTe1HWAEAAABjcQhNan1YAQAA
AGRxCU0sSVgBAAAAZXEKTXU3WAEAAABmcQtNdS5YAQAAAGdxDE3JKlgBAAAAaHENTa81WAEAAABp
cQ5NjzNYAQAAAHlxD013BFgBAAAAanEQTRgLWAEAAABrcRFNcA9YAQAAAGxxEk0SJ1gBAAAAbXET
TV1NWAEAAABucRRNkjRYAQAAAG9xFU2JMVgBAAAAcHEWTSyIWAEAAABxcRdNAQdYAQAAAHJxGE2P
QVgBAAAAc3EZTWmXWAEAAAB0cRpNgklYAQAAAHVxG03vWFgBAAAAd3EcTZ8ZWAEAAAB4cR1N+wFY
AQAAAHpxHk1sBXWFcR9ScSBoBWgBfXEhKGgFS/9oDU2qBWgSTW+eaBNNuixoFE1qnGgYTQp8aBlN
/UBoB021M2gITWdGaA9NMwpoGk1JpGgdTQYFaAlNhSVoC01jCGgOTbwZaBFN7QtoFk1CKWgMTWEh
aBtNURVoBk2nDWgeTcIFaApN8xdoFU2FAWgXTVUBaBBNPgFoHE2gB3WFcSJScSNoDWgBfXEkKGgK
TW5JaA5N0DloGU1EA2gNS3BoGE3wD2gSTfkGaBNNiANoD03tH2gVTf85aBtN8QpoBU3xNmgaTbAJ
aBFLPmgUTeADaAhLdGgcTZkBaAdNMwFoFku1aB5LEmgMS01oC00DAWgJS4poEEsPaAZLIWgXSwx1
hXElUnEmaApoAX1xJyhoCU2deGgZTVK3aBJNg0xoDU21BWgYSpkEAQBoEE2MAWgUTSKWaApNkBxo
Gk3NRmgTTbUxaA9NzwZoBU3BOmgGTTkNaAhNfDNoDE1wEGgOTQ4RaBZNoiFoEU23AmgVTWQVaB5N
wgFoG02KEWgcTT8JaAdN4gtoC02+DmgXTaMDaB1NNxR1hXEoUnEpaA5oAX1xKihoFE3W7WgOTUMC
aBlNlo5oCE2fjGgaTRZhaBVNFlZoBU26UGgKTXM0aB5NwCBoGE13HGgSTWA+aAlNUThoG03CCGgW
TXYbaAZNsyBoDE2AG2gHTT4SaBNNlyBoC01hGGgRTUkKaB1N/wJoD0tbaBxLk2gNTaQBaBdN9AFo
EEuEdYVxK1JxLGgUaAF9cS0oaAxNEXRoDk0QV2gFTcpIaAhNmjRoCk0GfmgJTd06aBNNRgZoGk1d
c2gZTa44aBRNbQ9oFU3HT2gbTeUMaBhNJAhoD03VBmgeTQ8CaBJNAgdoBk0uB2gRTZoIaBZN6wlo
B02PBWgcTawDaAtNWQ1oDU1/BWgQTQoCaBdN6QFoHUtsdYVxLlJxL2gSaAF9cTAoaA5NgYBoGU2W
C2gLTbIDaAZNQgRoEk1ESWgPTeROaBtNRxpoBU3cYWgVTf9JaApNEIdoE013BWgaTd0NaAhNCAVo
FE09BGgJTRAJaAdN5wJoDE1VA2gNTQMBaBZNsgVoGEu4aBFNhQNoHE0wAWgeSzNoEEsUaBdLE2gd
Swp1hXExUnEyaBhoAX1xMyhoCU0jFmgRTYcHaAxNuAxoFU18eWgYTXcWaBtNtxdoDk0mi2gKTVac
aBlNUS5oBU2QiWgaTfggaA9Nnh1oFE3UEGgGTcIGaBNNMhpoB00PDGgWTbQNaA1NHQhoEk0GC2gI
TW0WaAtN8AVoHE2GA2gdSwxoF0uTaBBLt2geS3d1hXE0UnE1aAloAX1xNihoBk13AWgcTe4BaApN
g1ZoBU2/IWgJTR4HaBVNfSBoDk2US2gSTckLaBtNaw1oFE2QBWgMTQUEaBlNnQpoD03zBmgYTaUT
aA1N3wFoFku4aAhLyWgLTVcBaBBNJgFoE01rAmgaS5FoHksoaAdNZgFoEUsuaBdLDGgdSwJ1hXE3
UnE4aAZoAX1xOShoBU2dE2gKTVFFaBVN/AloDk1pGmgbTcsBaAZLLmgaSwhoD0uGaBhLaWgJSwlo
DEsIaBRLDWgZSxpoEksTaAhLB2gcSwJoEUsGaB5LBGgNSwFoFksDaBNLA2gHSwF1hXE6UnE7aBFo
AX1xPChoGU3qBmgFTS0JaA1N0QFoCk3NHmgOTa0RaBhNiAFoHksDaBNL8mgUTQEDaBpNAgFoB00C
AWgVTQ8EaBJNqgRoG03lAWgISzpoEUuEaBZLjGgGSx5oHE0rAWgPTZ4CaAlLPmgLS79oDEskaBBL
GWgXSwFoHUsBdYVxPVJxPmgcaAF9cT8oaBVNrgxoDU2zBWgFTfMSaApNpw1oDk07DmgSTWcCaBRN
vwNoGU3PAmgYTeMCaBpLpmgTS5FoG0twaA9LoGgHS/NoFktnaBxLUmgMSzJoCUv6aAtLiWgRS7Ro
CEtHaBBLBWgeSxRoF0sCaAZLAnWFcUBScUFoFWgBfXFCKGgSTXhHaBRN27doDE2wKWgaTV4yaBhN
inBoGU0KPWgbTT5MaA5NuBZoE02GQmgITZgqaB5N+QJoBU1TDmgNTQQEaBZNITpoBk38GGgHTbAS
aBVNNB1oHE0XEWgJTSIeaAtNYQhoHU3LB2gRTfYGaA9NJARoCk0YC2gXTUkBaBBLm3WFcUNScURo
DGgBfXFFKGgNTTALaApNiy5oFU3qEmgFTSEeaA5NTyFoDE1BCGgSTRwWaBRN3QpoD00QCGgZTZIG
aBhNaRxoE01NA2gbTewOaAdLxmgISyJoCUt8aBFLI2gWS0VoGkvVaBxL02gLS3loBksFaBBLDGgX
SwFoHksMdYVxRlJxR2gIaAF9cUgoaAVN6VpoDk3ZLGgbTUceaBFNfRloEk3gE2gVTV1eaBpNPiho
Ck04OmgPTeYNaAhNUgloDU1uTWgZTQcEaBhNjyBoE0s0aBRL5GgWSypoF0uyaB1LAWgJSzVoB0sX
aAtLE2gcSxRoDEsOaAZLAmgeSy11hXFJUnFKaBpoAX1xSyhoCk3FrGgOTe3BaBVNbVNoGU0mFmgb
TYUcaA1Ni0BoBU19UmgYTZFPaBBLQ2gaTcIWaAhN8gZoFE3NAmgSTY8NaBNNcgNoD03EHGgHTRoC
aBZNiwFoDE0PAWgLTewCaBxNbQRoEUtFaB5NVAFoCUvEaAZLUmgXSxRoHUsHdYVxTFJxTWgZaAF9
cU4oaAZLgmgITS8laBpNSoVoCk1nUWgFTTkkaBlNe1FoDk26RmgMTQoBaA1NvStoEk2qDmgRTSEH
aBVNeiJoE011HGgbTdgoaA9NvwtoGE0ZAWgLTeUBaBdN/QNoFE0NCmgWTWQhaAdN4wFoHE2EBWgJ
TQABaBBLR2geSxR1hXFPUnFQaAdoAX1xUShoBU3tIWgJTcQBaBtNJRJoEk17MGgVTTcbaAdNrwZo
Ck2bImgPTVQCaA5N0CJoGE3PF2gITYYBaAtLkGgNS7toEE0AAWgRSyRoE0vtaBRLpmgWS85oGU31
BWgaTewBaAZLjWgcS05oDEtXaB1LBGgeSwpoF0sKdYVxUlJxU2gbaAF9cVQoaAVNIhBoGU19TWgS
TRQ3aBNNSyJoGE2UN2gKTXwNaAhNrxFoBk3CAWgUTeBraAlNSw5oDE09CWgaTV4kaB1NkwFoFk3d
E2gHTbAUaA5NOBBoHk0YAWgVTRIEaBFNEwJoD0uTaAtNyANoEEtQaBxLHmgbS0ZoF0spaA1LaXWF
cVVScVZoHWgBfXFXKGgOTVwIaAVNGgRoCk1bBGgPTfYCaBpNnARoFU33AmgTSyFoG00dAWgWTcwD
aBJLPGgITWUCaAdLKWgLSyloDUu+aBlLZWgcSyVoGEsNaAlLEmgRSwJoFEsNaAxLDWgXSw1oHUsK
aB5LAWgGSwh1hXFYUnFZaBBoAX1xWihoFU0EBGgKTR8EaBtNtwVoBU1iBWgOTZ0BaBpLA2gNSwho
EEsGaBhLC2gPSwdoFksDaBxLAmgGSwFoFEsQaAlLCWgSSwNoE0sEaAdLAmgISwVoDEsCaBlLA2gR
SwR1hXFbUnFcaAtoAX1xXShoC032C2gaTcMEaAVN6Q1oG01ED2gVTakVaA5NcxtoCk0qFGgPTa8D
aBJNCBBoGE0QC2gHSzFoCUsoaAxLGGgZTT4BaBxLKmgeSwZoFEssaAZLAmgRSwxoCEsnaA1LKmgT
Sy1oFksdaBBLCGgdSwN1hXFeUnFfaA9oAX1xYChoBU0uCGgKTZ4GaBlNRw5oE01fCWgOTaEFaBhN
9gdoCE3eCGgWTZoOaAxNjwNoEk0VD2gUTRUJaAlN0QVoGk2SCGgdS8hoFU2ZBmgeS95oG0vXaA9L
CWgHTdsBaBxNNwFoC0viaA1L0mgGSyZoEUtTaBBLCmgXSwV1hXFhUnFiaBNoAX1xYyhoBU29TGgW
TQcbaApNvEloB03sEGgOTb9GaA9N9ApoFU2LMGgbTagPaBlNOwdoDUtQaBNNYQ1oGEt7aBpLZmgS
TX0BaBRN9ANoEUskaAtL52gcS1xoCEtlaAlLXGgMSyVoBktPaB5LD2gQSxNoF0sGaB1LAXWFcWRS
cWVoFmgBfXFmKGgKTUNDaBlNHw9oDk00KGgaTeYRaBVNHjFoD038BmgNTXxBaBhN3jloBU39MGgU
TRwCaBZN+Q1oG03mDmgSTa0caBNL4GgQSxRoCUteaB1LAWgHS81oCEuOaAtLsGgcS7toEUtHaAxL
SWgGSwhoHksBaBdLBHWFcWdScWhoHmgBfXFpKGgKTZIYaB5NNwJoFU05B2gOTQUJaAVNhgloG0uD
aBJNCQFoDEsJaA1LD2gPTboBaBhLC2gaSw5oFEsMaAdLFWgJSxVoFksQaAhLEGgRSw5oGUsPaBxL
EmgTSw9oBksJaBdLBGgQSwJoC0sDdYVxalJxa2gXaAF9cWwoaBtNgxZoHEsCaBlLBWgWSwJoDksS
aA1LAmgYSwZoFEsCaBVLBGgFSxVoGksIaAxLAWgKSwZoD0sBaBJLAmgTSwFoF0sEaAZLAmgJSwJo
C0sBdYVxbVJxbnUu
"""


def random_password(opts):
    """
    Generate and return a random password.
    """
    return "".join([RG.choice(opts.alphabet) for _ in range(opts.length)])


def words_from_file(filename):
    """
    Read a file indicated by the string `filename` and return a list of words.
    File should contain words separated by newline.

    """
    with open(filename) as f:
        return f.read().rsplit()


def build_markov_chain(filename):
    """
    Read words from a file and build a Markov chain.
    """
    counts = defaultdict(Counter)
    for word in words_from_file(filename):
        prev = None
        for c in word:
            counts[prev][c] += 1
            prev = c
    return counts


def accumulate(iterable, func=operator.add):
    """
    Return running totals
    """
    it = iter(iterable)
    try:
        total = next(it)
    except StopIteration:
        return
    yield total
    for element in it:
        total = func(total, element)
        yield total


def choices(population, weights=None, cum_weights=None, k=1):
    """
    Return a k sized list of population elements chosen with replacement.

    If the relative weights or cumulative weights are not specified, the
    selections are made with equal probability.

    Adapted from https://github.com/python/cpython/blob/master/Lib/random.py

    """
    n = len(population)
    if cum_weights is None:
        if weights is None:
            _int = int
            return [population[_int(RG.random() * n)] for i in range(k)]
        cum_weights = list(accumulate(weights))
    elif weights is not None:
        raise TypeError("Cannot specify both weights and cumulative weights")
    if len(cum_weights) != n:
        raise ValueError("The number of weights does not match the population")
    total = cum_weights[-1]
    hi = n - 1
    return [
        population[bisect(cum_weights, RG.random() * total, 0, hi)]
        for i in range(k)
    ]


def markov_password(markov_data, opts):
    """
    Generate and return a pronounceable password.
    """
    word = ""
    prev = None
    for _ in range(opts.length):
        if not markov_data[prev]:
            prev = None
        prev = choices(list(markov_data[prev].keys()),
                       list(markov_data[prev].values()))[0]
        word += prev
    return word


def add_password_requirement(pw, alphabet, regex, n):
    """If necessary, add the specified password requirement to string `pw` and
    return it.

    """
    if n > 0:
        # Get positions of matches of regex in pw
        matches = [markov_data.start()
                   for markov_data in re.finditer(regex, pw)]
        # We need to replace this many characters to meet the requirement
        k = max(n - len(matches), 0)
        # Make a list of fields to change (must be in positions which are NOT
        # matches for regex)
        change = RG.sample(list(set(range(len(pw))) - set(matches)), k)
        # Make the replacements
        for i in change:
            pw = pw[:i] + RG.choice(alphabet) + pw[i + 1:]
    return pw


def add_password_requirements(pw, opts):
    """Make any necessary modifications to the password `pw` so that it conforms to
    the password requirements specified in `opts`.

    """
    pw = add_password_requirement(
        pw, string.ascii_lowercase, REGEX_LOWERCASE, opts.minimum_lower
    )
    pw = add_password_requirement(
        pw, string.ascii_uppercase, REGEX_UPPERCASE, opts.minimum_upper
    )
    pw = add_password_requirement(
        pw, string.digits, REGEX_DIGITS, opts.minimum_digits)
    pw = add_password_requirement(
        pw, string.punctuation, REGEX_SPECIAL, opts.minimum_special
    )
    return pw


def build_markov(opts,
                 input_file=os.path.realpath(__file__),
                 output_file=os.path.realpath(__file__)):
    """
    Build Markov chain using input and output files.
    """
    # Create the data
    markov_data = build_markov_chain(opts.build_markov)
    # Read in the script file
    with open(input_file, "r") as f:
        contents = f.read()
        # Build the markov chain and write back to script file
        re.sub(
            re.compile(
                r"^MARKOV_DATA_ENCODED = \"\"\"\n[^\"]\"\"\"",
                re.MULTILINE),
            'MARKOV_DATA_ENCODED = """\n'
            + codecs.encode(pickle.dumps(markov_data), "base64").decode()
            + '"""\n',
            contents,
        )
    with open(output_file, "w") as f:
        f.write(contents)


def pronounceable(opts):
    """
    Make pronounceable passwords
    """
    markov_data = pickle.loads(
        codecs.decode(MARKOV_DATA_ENCODED.encode(), "base64")
    )
    pws = []
    for _ in range(opts.number):
        pws.append(add_password_requirements(
            markov_password(markov_data, opts), opts))
    return pws


def random(opts):
    """
    Generate random passwords.
    """
    # Make completely random passwords
    if any([opts.lower, opts.upper, opts.digits, opts.special,
            opts.characters]):
        # use only specified character sets
        opts.alphabet = ""
        if opts.lower:
            opts.alphabet += string.ascii_lowercase
        if opts.upper:
            opts.alphabet += string.ascii_uppercase
        if opts.digits:
            opts.alphabet += string.digits
        if opts.special:
            opts.alphabet += string.punctuation
        opts.alphabet += opts.characters
    elif opts.unicode:
        opts.alphabet = "".join([chr(c) for c in UNICODE_RANGE])
    else:
        # no specific character sets were specified so use the defaults
        opts.alphabet = string.digits + string.ascii_letters + \
            string.punctuation
    # Generate and print some passwords
    pws = []
    for _ in range(opts.number):
        pws.append(add_password_requirements(random_password(opts), opts))

    return pws


def parse_args():
    """
    Parse the command line args and return them in object form.
    """
    parser = argparse.ArgumentParser(
        description=" Generate random passwords in a hopefully secure manner."
    )

    parser.add_argument(
        "length", nargs="?", type=int, default=16, help="Length of password"
    )
    parser.add_argument(
        "--number", "-n", type=int, default=1,
        help="Number of passwords to generate"
    )
    parser.add_argument(
        "--pronounceable",
        "-p",
        default=False,
        action="store_true",
        help="Create human pronounceable passwords",
    )
    parser.add_argument(
        "--lower",
        "-l",
        default=False,
        action="store_true",
        help="Use lower case letters",
    )
    parser.add_argument(
        "--require-lower",
        "-L",
        default=False,
        action="store_true",
        help="Require at least one lowercase character",
    )
    parser.add_argument(
        "--upper",
        "-u",
        default=False,
        action="store_true",
        help="Use upper case letters",
    )
    parser.add_argument(
        "--require-upper",
        "-U",
        default=False,
        action="store_true",
        help="Require at least one upper character",
    )
    parser.add_argument(
        "--digits", "-d", default=False, action="store_true", help="Use digits"
    )
    parser.add_argument(
        "--require-digits",
        "-D",
        default=False,
        action="store_true",
        help="Require at least one digit character",
    )
    parser.add_argument(
        "--special",
        "-s",
        default=False,
        action="store_true",
        help="Use special characters (punctuation)",
    )
    parser.add_argument(
        "--require-special",
        "-S",
        default=False,
        action="store_true",
        help="Require at least one special character",
    )
    parser.add_argument(
        "--characters", "-c", default="", help="Specify individual characters"
    )
    parser.add_argument(
        "--unicode",
        "-z",
        default=False,
        action="store_true",
        help="Use a large unicode character set",
    )
    parser.add_argument(
        "--active-directory",
        "-a",
        default=False,
        action="store_true",
        help="""
Output passwords that exceed the default requirements in Microsoft Active
Directory environments (`-LUDS`)""",
    )
    parser.add_argument(
        "--build-markov",
        "-b",
        action="store",
        metavar="FILE",
        help="Build a markov chain using the words in FILE.",
    )

    opts = parser.parse_args()

    opts.minimum_lower = 1 if opts.require_lower or opts.active_directory \
        else 0
    opts.minimum_upper = 1 if opts.require_upper or opts.active_directory \
        else 0
    opts.minimum_digits = 1 if opts.require_digits or opts.active_directory \
        else 0
    opts.minimum_special = 1 if opts.require_special or opts.active_directory \
        else 0

    return opts


def main():
    """
    Print some passwords.
    """

    opts = parse_args()

    if opts.build_markov:
        build_markov(opts)
    elif opts.pronounceable:
        for p in pronounceable(opts):
            print(p)
    else:
        for p in random(opts):
            print(p)


if __name__ == "__main__":
    main()
