LoginSignup
2
2

More than 3 years have passed since last update.

パターン指定型パスワード ジェネレーター

Last updated at Posted at 2020-03-26

目がチカチカしないパスワードが欲しいので、パターンを指定する形式のパスワード生成プログラムを python で作る。

パターン指定型パスワード ジェネレーターの目的は、好みのパスワードを得ることなので、生成されたパスワードを、そのまま使う必要はありません。

Python プログラム
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()

コマンド ライン引数

生成するパターンを C言語の書式風で記述します。

指定子

文字 % の直後が、乱数で選択される文字になります。

指定子 種類 備考
b 2進数 [01]
o 8進数 [0-7]
d 数字 [0-9]
X 16進大文字 [0-9A-F]
X 16進小文字 [0-9a-f]
A 英文字 [A-Za-z]
C 英大文字 [A-Z]
c 英小文字 [A-Z]
B 英母音 [AEIOUaeiou]
V 英母音(大) [AEIOU]
v 英母音(小) [aeiou]
D 英子音 [AEIOUaeiuo] を除く [A-Za-z]
Q 英子音(大) [AEIOU] を除く [A-Z]
q 英子音(小) [AEIOU] を除く [A-Z]
Y 英数 [0-9A-Za-z]
Z 英数(大) [0-9A-Z]
z 英数(小) [0-9a-z]
W ラベル文字 [0-9A-Za-z_]
L ラベル文字(大) [0-9A-Z_]
l ラベル文字(小) [0-9a-z_]

指定子を並べた例

$ 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

文字数指定

文字 % 直後の数値は、乱数で選択される文字の文字数になります。
  %[数値]指定子
の形式です。例えば '%3d' とすると、3桁の数字になります。

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

省略形

上記サンプル
  'qiita-%d%X%x%x%A%C%c%c%B%V%v%D%Q%q%Y%Z%z%W%L%l'
は、'%{...}' 形式で省略
  'qiita-%{dXxxACccBVvDQqYZzWLl}'
できます。

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

選択文字指定

指定子の代わりに '[...]' で選択する文字を指定します。

# 8桁2進数となる例
$ python ppwgen.py  -n 5 '%8[01]'     
1: 01010110
2: 00100001
3: 10100111
4: 11011000
5: 00101110

'[' 開始文字 '-' 終了文字 ']' で選択する文字を指定します。

# 文字 STUVWXYZ から選択
$ python ppwgen.py -n 5 '%8[S-Z]'
1: XXTTZWTS
2: YUTTXUTV
3: UVUWXTXX
4: UTSZUWZS
5: UTWWYUVX

エスケープ

バックスラッシュ \ の直後の文字は特別な意味を持ちません。

# 文字 S,-,Z の3つから選択
$ python ppwgen.py -n 5 '%8[S\-Z]'
1: -ZZ---S-
2: ZZZ--ZZZ
3: -ZZSS-SS
4: ZSS-SS-Z
5: Z----SZ-

実行例

ライセンス キー風

$ 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

'- 子音 母音 子音 数字' の繰り返し。

$ 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

入れ子構造を持つ複雑なパターン。

$ 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
2
2
15

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2