[PYTHON] Patterned password generator

I want a password that doesn't flicker, so I use python to create a password generator that specifies a pattern.

Python program

ppwgen.py


#!/usr/bin/env python

flag_debug = False
flag_verbose = False
flag_syntax = False


def debug(msg):
    if flag_debug:
        print(msg)


def verbose(msg):
    if flag_verbose:
        print(msg)


def import_random():
    try:
        from Crypto.Random import random
        verbose('import Crypto.Random')
        return random
    except ImportError:
        pass

    try:
        import secrets
        verbose('import secrets')
        return secrets
    except ImportError:
        pass

    import random
    verbose('import random')
    return random


def charset_(r, x=''):
    s = ''
    for c in r:
        c = chr(c)
        if c not in x:
            s = s + c
    return s


def charset(s, x=''):
    r = ''
    while len(s) >= 3:
        if s[1] != '-':
            break
        cs = ord(s[0])
        ce = ord(s[2])
        r = r + charset_(range(cs, ce+1), x)
        s = s[3:]

    for c in s:
        if c not in x:
            r = r + c
    return r


def isescape(c):
    return (len(c) > 1) and (c[0] == '\\')


def unescape(c):
    if isescape(c):
        return c[1:]
    return c


class InvalidPattern(Exception):
    def __init__(self):
        Exception.__init__(self)


class PatIter:
    def __init__(self, pat):
        self.pos = 0

        s = []
        for c in list(pat):
            if len(s) and s[-1] == '\\':
                s[-1] = s[-1] + c
            else:
                s.append(c)
        if len(s) and s[-1] == '\\':
            if flag_syntax:
                raise InvalidPattern()
            s = s[:-1]
        self.pat = s

    def __iter__(self):
        return self

    def __next__(self):
        return self.next()

    def next(self):
        p = self.pos
        if p >= len(self.pat):
            raise StopIteration()
        self.pos = p + 1
        return self.pat[p]

    def rewind(self):
        self.pos = self.pos - 1
        return self


class Generator:
    def __init__(self, data):
        self.data = data

    def generate(self, random):
        s = self.data
        if type(s) == str:
            return random.choice(s)

        if type(s) == tuple:
            n = s[0]
            s = [s[1]]
        elif type(s) == list:
            n = 1
        else:
            raise Exception("Bug!")

        r = ''
        for i in range(n):
            for g in s:
                r = r + g.generate(random)
        return r


PATTERN_MAP = {
    'b': '01',
    'o': '01234567',
    'd': charset('0-9'),
    'X': charset('0-9A-F'),
    'x': charset('0-9a-f'),

    'A': charset('A-Za-z'),
    'C': charset('A-Z'),
    'c': charset('a-z'),

    'B': 'AEIOUaeiou',
    'V': 'AEIOU',
    'v': 'aeiou',

    'D': charset('A-Za-z', 'AEIUOaeiou'),
    'Q': charset('A-Z', 'AEIOU'),
    'q': charset('a-z', 'aeiou'),

    'Y': charset('0-9A-Za-z'),
    'Z': charset('0-9A-Z'),
    'z': charset('0-9a-z'),

    'W': charset('0-9A-Za-z_'),
    'L': charset('0-9A-Z'),
    'l': charset('0-9a-z'),

    '%': '%',
}


def check_not_close(c, q):
    return ((c is None) or (c != q)) and flag_syntax


class CustomPasswordGenerator:
    def __init__(self, pattern):
        ptr = PatIter(pattern)
        r = []
        for c in ptr:
            if c != '%':
                g = Generator(unescape(c))
            else:
                g = self.parse_pattern(ptr, False)
            if g != None:
                r.append(g)
        self.generator = Generator(r)

    def parse_pattern(self, ptr, nest):
        n = None
        c = None
        for c in ptr:
            if not c.isdigit():
                break
            if n is None:
                n = 0
            n = (n * 10) + int(c, 10)

        if n is None:
            n = 1
        if c is None:
            if flag_syntax:
                raise InvalidPattern()
            return None

        if c in PATTERN_MAP:
            return Generator((n, Generator(PATTERN_MAP[c])))
        if c == '{':
            return self.parse_subpat(ptr, n)
        if c == '[':
            return self.parse_charset(ptr, n)

        if isescape(c):
            c = unescape(c)
        elif (not nest) or (c in ']}'):
            raise InvalidPattern()
        return Generator((n, Generator(c)))

    def parse_subpat(self, ptr, rep):
        r = []
        c = None
        for c in ptr:
            if c == '}':
                break
            g = self.parse_pattern(ptr.rewind(), True)
            if g != None:
                r.append(g)
        if check_not_close(c, '}'):
            raise InvalidPattern()
        return Generator((rep, Generator(r)))

    def parse_charset(self, ptr, rep):
        r = []
        s = []
        c = None
        for c in ptr:
            if c == ']':
                break
            if len(s) < 2:
                s.append(c)
                continue
            a = unescape(s[0])
            if s[-1] != '-':
                r.append(a)
                s = s[1:]
                s.append(c)
                continue
            b = unescape(c)
            r.append(charset_(range(ord(a), ord(b)+1)))
            s = []
        if check_not_close(c, ']'):
            raise InvalidPattern()
        for a in s:
            r.append(unescape(a))
        return Generator((rep, Generator(''.join(r))))

    def generate(self, rnd=None, length=0):
        if rnd is None:
            rnd = import_random()
        pw = self.generator.generate(rnd)
        if length == 0:
            return pw
        while length > len(pw):
            pw = pw + self.generator.generate(rnd)
        return pw[:length]


def main():
    import argparse
    global flag_debug
    global flag_verbose
    global flag_syntax

    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--debug', action='store_true', default=False)
    parser.add_argument('-v', '--verbose', action='store_true', default=False)
    parser.add_argument('-l', '--length', type=int, default=0)
    parser.add_argument('-U', '--upper', action='store_true', default=False)
    parser.add_argument('-L', '--lower', action='store_true', default=False)
    parser.add_argument('-n', '--count', type=int, default=0)
    parser.add_argument('-p', '--syntax', action='store_true', default=False)
    parser.add_argument('pattern', type=str, default='%l')

    args = parser.parse_args()
    flag_debug = args.debug
    flag_verbose = args.verbose or flag_debug
    flag_syntax = args.syntax

    converter = str
    if args.upper:
        converter = str.upper
    if args.lower:
        converter = str.lower

    pattern = args.pattern
    gen_length = args.length
    gen_count = args.count
    if gen_count == 0:
        gen_count = {False: 20, True: 2}[flag_debug]

    verbose('pattern = \'%s\'' % pattern)
    verbose('length = %d' % gen_length)
    verbose('count = %d' % gen_count)

    if flag_debug:
        verbose('PATTERN_MAP')
        for k in sorted(PATTERN_MAP):
            verbose('  \'%s\' : \'%s\'' % (k, PATTERN_MAP[k]))
        verbose('')

    try:
        gen = CustomPasswordGenerator(pattern)
    except InvalidPattern:
        print('invalid pattern: \'%s\'' % pattern)
        return

    rnd = import_random()
    fmt = '%' + str(len(str(gen_count))) + 'd: %s'
    for i in range(gen_count):
        print(fmt % (i+1, converter(gen.generate(rnd, gen_length))))


if __name__ == '__main__':
    main()

Command line arguments

Describe the generated pattern in C format style.

Specifier

Immediately after the character% is the character selected by the random number.

Specifier type Remarks
b Binary number [01]
o 8 base [0-7]
d Numbers [0-9]
X Hexadecimal capital [0-9A-F]
X Hexadecimal lowercase [0-9a-f]
A English letters [A-Za-z]
C Uppercase letters [A-Z]
c Lowercase letters [A-Z]
B English vowels [AEIOUaeiou]
V English vowels(Big) [AEIOU]
v English vowels(small) [aeiou]
D Eiko sound [AEIOUaeiuo]except for[A-Za-z]
Q Eiko sound(Big) [AEIOU]except for[A-Z]
q Eiko sound(small) [AEIOU]except for[A-Z]
Y Alphanumeric [0-9A-Za-z]
Z Alphanumeric(Big) [0-9A-Z]
z Alphanumeric(small) [0-9a-z]
W Label character [0-9A-Za-z_]
L Label character(Big) [0-9A-Z_]
l Label character(small) [0-9a-z_]

Example of arranging specifiers

$ python ppwgen.py -n 5 'qiita-%d%X%x%x%A%C%c%c%B%V%v%D%Q%q%Y%Z%z%W%L%l'
1: qiita-2D1cHSuhIEotMrdXh53r
2: qiita-2454ZXzlOIeXHc9GpD3n
3: qiita-2481ZFxhOUeZGjjHoP5y
4: qiita-7BffcNydeEaGZnUMyDUe
5: qiita-52f5nZqtAAiWTbGTphHj

Specify the number of characters

The number immediately after the character% is the number of characters selected by the random number. % [Numeric] specifier It is in the form of. For example,'% 3d' will be a 3-digit number.

$ python ppwgen.py -n 5 'qiita-%3d'
1: qiita-419
2: qiita-879
3: qiita-452
4: qiita-054
5: qiita-025

Abbreviation

Above sample   'qiita-%d%X%x%x%A%C%c%c%B%V%v%D%Q%q%Y%Z%z%W%L%l' Is abbreviated in the'% {...}' format   'qiita-%{dXxxACccBVvDQqYZzWLl}' I can do it.

$  python ppwgen.py  -n 5 'qiita-%{dXxxACccBVvDQqYZzWLl}'
1: qiita-32b3eMjmeUolYkdFjsNj
2: qiita-3F84MNzfAOoLNt1Y8GSi
3: qiita-9A3dURxtAAovPg7V8p9b
4: qiita-4D47iTquuEiDKjsQ3bM4
5: qiita-87dcXGjkuIuCMk1H5u9x

Specify selected characters

Specifies the character to select with'[...]' instead of the specifier.

#Example of 8-digit binary number
$ python ppwgen.py  -n 5 '%8[01]'     
1: 01010110
2: 00100001
3: 10100111
4: 11011000
5: 00101110

Specify the character to select with'['start character'-' end character']'.

#Select from the letters STUVWXYZ
$ python ppwgen.py -n 5 '%8[S-Z]'
1: XXTTZWTS
2: YUTTXUTV
3: UVUWXTXX
4: UTSZUWZS
5: UTWWYUVX

escape

The character immediately following the backslash \ has no special meaning.

#Letter S,-,Choose from 3 of Z
$ python ppwgen.py -n 5 '%8[S\-Z]'
1: -ZZ---S-
2: ZZZ--ZZZ
3: -ZZSS-SS
4: ZSS-SS-Z
5: Z----SZ-

Execution example

License key style

$ python ppwgen.py -n 10 'qiita%4{-c3d}'
 1: qiita-w119-i910-y458-q980
 2: qiita-j159-p127-u802-b125
 3: qiita-f923-o256-o568-h824
 4: qiita-k280-q899-a826-q831
 5: qiita-d264-a506-v904-q475
 6: qiita-u866-p655-n009-o483
 7: qiita-m101-a054-p896-x707
 8: qiita-o524-b339-j527-n802
 9: qiita-o030-k832-y107-s425
10: qiita-w842-a925-h295-p339

Repeat'-consonant vowel consonant number'.

$ python ppwgen.py -n 10 'qiita%4{-Qvqd}'
 1: qiita-Sap0-Woy0-Yeg6-Vig0
 2: qiita-Viq4-Giq7-Yoq0-Cam4
 3: qiita-Luz7-Woc8-Wek8-Xek0
 4: qiita-Saf8-Mac9-Kar4-Mif1
 5: qiita-Xen3-Kur3-Gix9-Mom4
 6: qiita-Xew6-Var0-Luf5-Guq9
 7: qiita-Gew8-Ded2-Jon0-Qac5
 8: qiita-Tiq2-Yef7-Reg3-Qiz9
 9: qiita-Paw8-Lap9-Jex7-Xuc2
10: qiita-Rud5-Yos6-Vam8-Pip8

A complex pattern with a nested structure.

$ python ppwgen.py -n 10 'qiita%2{-C3{2c[13579]}}'
 1: qiita-Oqv5ct1eg9-Cfv3ha3yo1
 2: qiita-Vtw3yq5ka3-Nha9zh7vr1
 3: qiita-Ulk5yb3ne3-Dpz7wu5xu3
 4: qiita-Yqb1lg3uf5-Iqn5qy9xa1
 5: qiita-Qck5jr1ru1-Tgo9ab1rh3
 6: qiita-Vdc7tj3oj5-Iba3eu1yc3
 7: qiita-Pqn9wn1ws1-Ksb3ys5bt1
 8: qiita-Vpo7hq1wi7-Iyh3uc3lt1
 9: qiita-Fmq5mu3wh7-Tdd3gq7ea1
10: qiita-Sqb1iv1gb5-Boy9va7ve9

Recommended Posts

Patterned password generator
Hash password generator
Password generator creation notes
generator
Generator
Generator memo.