First version of pwgen
This commit is contained in:
parent
498c0eefd0
commit
932322011b
115
bin/pwgen
Normal file
115
bin/pwgen
Normal file
@ -0,0 +1,115 @@
|
||||
#!/usr/bin/python3
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pwgen
|
||||
|
||||
default_config_file=os.path.expanduser('~/.pwgen.cfg')
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser(description='Generate passwords')
|
||||
parser.add_argument(
|
||||
'--generate-config', '-g',
|
||||
action='store_true',
|
||||
help='Generate configuration file and then exit')
|
||||
parser.add_argument(
|
||||
'--config-file', '-c',
|
||||
help='Configuration file to use',
|
||||
default=default_config_file)
|
||||
|
||||
parser.add_argument(
|
||||
'--myspell-dir', '-i',
|
||||
help='Directory containing myspell dictionaries')
|
||||
|
||||
parser.add_argument(
|
||||
'--lang', '-l',
|
||||
help='Dictionary language to use')
|
||||
parser.add_argument(
|
||||
'--word-min-char', '-m',
|
||||
type=int,
|
||||
help='Minimum number of characters in a word')
|
||||
parser.add_argument(
|
||||
'--word-max-char', '-M',
|
||||
type=int,
|
||||
help='Maximum number of characters in a word')
|
||||
|
||||
parser.add_argument(
|
||||
'--words', '-w',
|
||||
type=int,
|
||||
help='Number of words to use in the passphrase')
|
||||
parser.add_argument(
|
||||
'--capitalize', '-C',
|
||||
choices=('true', 'false', 'random'),
|
||||
help='Capitalize the words')
|
||||
parser.add_argument(
|
||||
'--separators', '-s',
|
||||
help='Possible characters to use as separators')
|
||||
parser.add_argument(
|
||||
'--trailing-digits', '-d',
|
||||
type=int,
|
||||
help='Number of digits at the end of the passphrase')
|
||||
parser.add_argument(
|
||||
'--leading-digits', '-D',
|
||||
type=int,
|
||||
help='Number of digits at the start of the passphrase')
|
||||
parser.add_argument(
|
||||
'--special-chars', '-S',
|
||||
help='Possible characters to use as extra special characters')
|
||||
parser.add_argument(
|
||||
'--trailing-chars', '-p',
|
||||
type=int,
|
||||
help='Number of special characters to add at the end of the passphrase')
|
||||
parser.add_argument(
|
||||
'--leading-chars', '-P',
|
||||
type=int,
|
||||
help='Number of special characters to add at the start of the passphrase')
|
||||
parser.add_argument(
|
||||
'--passwords', '-n',
|
||||
type=int,
|
||||
help='Number of passwords to generate')
|
||||
parser.add_argument(
|
||||
'--max-length', '-L',
|
||||
type=int,
|
||||
help="Maximum length of the generated passwords. Full-knowledge "\
|
||||
"entropy calculation doesn't work when this is set.")
|
||||
|
||||
|
||||
|
||||
args = vars(parser.parse_args())
|
||||
|
||||
config_file = args['config_file']
|
||||
del args['config_file']
|
||||
|
||||
if not os.path.isfile(config_file):
|
||||
print("Missing configuration file; generating a new at {}".format(config_file))
|
||||
conf = pwgen.update_config()
|
||||
pwgen.save_config(config_file, conf)
|
||||
|
||||
conf = pwgen.get_config(config_file)
|
||||
save_config = args['generate_config']
|
||||
del args['generate_config']
|
||||
|
||||
conf = pwgen.update_config(config=conf, **args)
|
||||
|
||||
if save_config:
|
||||
print("Updating configuration file at {}".format(config_file))
|
||||
pwgen.save_config(config_file, conf)
|
||||
sys.exit(0)
|
||||
|
||||
pwds, seen_entropy = pwgen.generate_passwords(conf)
|
||||
|
||||
print("Generated {} passwords".format(len(pwds)))
|
||||
if seen_entropy:
|
||||
print("Full-knowledge entropy is {0:.2g}".format(seen_entropy))
|
||||
else:
|
||||
print("Unable to calculate full-knowledge entropy since max_length is used")
|
||||
print("Blind entropy\tPassword")
|
||||
print("========================")
|
||||
for pw in pwds.keys():
|
||||
print("{:.5n}\t\t{}".format(pwds[pw]['entropy'], pw))
|
||||
print("========================")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
194
pwgen/__init__.py
Normal file
194
pwgen/__init__.py
Normal file
@ -0,0 +1,194 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
#import secrets as random
|
||||
import random
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
import ConfigParser as configparser
|
||||
else:
|
||||
import configparser
|
||||
|
||||
if sys.version_info >= (3, 6):
|
||||
do_seed = False
|
||||
import secrets as random
|
||||
else:
|
||||
do_seed = True
|
||||
import random
|
||||
|
||||
class PasswordLengthError(Exception):
|
||||
pass
|
||||
|
||||
def save_config(target_file, config):
|
||||
path = os.path.expanduser(target_file)
|
||||
with open(path, 'w') as f:
|
||||
config.write(f, False)
|
||||
|
||||
def update_config(
|
||||
config=None,
|
||||
|
||||
myspell_dir='/usr/share/hunspell',
|
||||
lang='en_US',
|
||||
word_min_char=2,
|
||||
word_max_char=0,
|
||||
|
||||
words=4,
|
||||
capitalize='random',
|
||||
separators=',.- _=:',
|
||||
trailing_digits=2,
|
||||
leading_digits=0,
|
||||
special_chars='!?.,:-_$/@',
|
||||
trailing_chars=1,
|
||||
leading_chars=0,
|
||||
passwords=5,
|
||||
max_length=0
|
||||
):
|
||||
if not config:
|
||||
conf = configparser.ConfigParser()
|
||||
else:
|
||||
conf = config
|
||||
|
||||
def set_if_defined(conf, section, var, val):
|
||||
if val is not None:
|
||||
conf.set(section, var, str(val))
|
||||
|
||||
if not conf.has_section('dictionary'):
|
||||
conf.add_section('dictionary')
|
||||
|
||||
set_if_defined(conf, 'dictionary', 'myspell_dir', myspell_dir)
|
||||
set_if_defined(conf, 'dictionary', 'lang', lang)
|
||||
set_if_defined(conf, 'dictionary', 'word_min_char', word_min_char)
|
||||
set_if_defined(conf, 'dictionary', 'word_max_char', word_max_char)
|
||||
|
||||
if not conf.has_section('passwords'):
|
||||
conf.add_section('passwords')
|
||||
set_if_defined(conf, 'passwords', 'words', words)
|
||||
set_if_defined(conf, 'passwords', 'capitalize', capitalize)
|
||||
set_if_defined(conf, 'passwords', 'separators', separators)
|
||||
set_if_defined(conf, 'passwords', 'trailing_digits', trailing_digits)
|
||||
set_if_defined(conf, 'passwords', 'leading_digits', leading_digits)
|
||||
set_if_defined(conf, 'passwords', 'special_chars', special_chars)
|
||||
set_if_defined(conf, 'passwords', 'trailing_chars', trailing_chars)
|
||||
set_if_defined(conf, 'passwords', 'leading_chars', leading_chars)
|
||||
set_if_defined(conf, 'passwords', 'passwords', passwords)
|
||||
set_if_defined(conf, 'passwords', 'max_length', max_length)
|
||||
return conf
|
||||
|
||||
|
||||
def get_config(f_name):
|
||||
conf = configparser.ConfigParser()
|
||||
conf.read(f_name)
|
||||
return conf
|
||||
|
||||
def _read_dictionary(f_name, word_min_chars, word_max_chars):
|
||||
words = set()
|
||||
chars = 0
|
||||
with open(f_name, 'r') as f:
|
||||
for line in f:
|
||||
if not line:
|
||||
continue
|
||||
first_char = line[0]
|
||||
if first_char in '1234567890,.-:':
|
||||
continue
|
||||
word = line.split('/', 1)[0]
|
||||
word = word.strip() # remove newlines
|
||||
last_char = word[-1]
|
||||
if last_char in '-':
|
||||
continue
|
||||
if word_min_chars and len(word) < word_min_chars:
|
||||
continue
|
||||
if word_max_chars and len(word) > word_max_chars:
|
||||
continue
|
||||
words.add(word)
|
||||
chars += len(word)
|
||||
return {'words': list(words), 'wordlength': int(chars/len(words))}
|
||||
|
||||
|
||||
def generate_passwords(conf):
|
||||
|
||||
nr_of_words = conf.getint('passwords', 'words')
|
||||
nr_of_ld = conf.getint('passwords', 'leading_digits')
|
||||
nr_of_td = conf.getint('passwords', 'trailing_digits')
|
||||
nr_of_lc = conf.getint('passwords', 'leading_chars')
|
||||
nr_of_tc = conf.getint('passwords', 'trailing_chars')
|
||||
special_chars = conf.get('passwords', 'special_chars')
|
||||
separators = conf.get('passwords', 'separators')
|
||||
capitalize = conf.get('passwords', 'capitalize')
|
||||
max_len = conf.getint('passwords', 'max_length')
|
||||
|
||||
if do_seed:
|
||||
random.seed()
|
||||
res = {}
|
||||
dict_data = _read_dictionary(
|
||||
os.path.join(conf.get('dictionary', 'myspell_dir'), '{}.dic'.format(conf.get('dictionary', 'lang'))),
|
||||
conf.getint('dictionary', 'word_min_char'),
|
||||
conf.getint('dictionary', 'word_max_char'))
|
||||
|
||||
words = dict_data['words']
|
||||
word_length = dict_data['wordlength']
|
||||
approx_pwd_length = nr_of_words*word_length+nr_of_ld+nr_of_td+nr_of_lc+nr_of_tc+nr_of_words-1
|
||||
if max_len and approx_pwd_length > max_len:
|
||||
raise PasswordLengthError(
|
||||
"Password would be ~{} charactes long but max_len is {}; "\
|
||||
"please adjust max_lenght, words and/or the "\
|
||||
"trailing/leading settings.".format(
|
||||
approx_pwd_length,
|
||||
max_len)
|
||||
)
|
||||
|
||||
capitalize_entropy = 1
|
||||
if capitalize == 'random':
|
||||
capitalize_entropy = 2
|
||||
# seen entropy is calculated from the password rules.
|
||||
# At the moment setting max_length breaks this calculation as not all
|
||||
# combinations of words are possible.
|
||||
if max_len:
|
||||
seen_entropy = False
|
||||
else:
|
||||
seen_entropy = math.log(
|
||||
len(words)**nr_of_words *\
|
||||
len(separators) *\
|
||||
10**nr_of_ld *\
|
||||
10**nr_of_td *\
|
||||
len(special_chars)**nr_of_lc *\
|
||||
len(special_chars)**nr_of_tc *\
|
||||
capitalize_entropy
|
||||
, 2)
|
||||
|
||||
while len(res) < conf.getint('passwords', 'passwords'):
|
||||
separator = random.choice(separators)
|
||||
my_words = []
|
||||
|
||||
for i in range(nr_of_words):
|
||||
word = random.choice(words)
|
||||
if capitalize == 'random':
|
||||
if random.choice((True, False)):
|
||||
word = word.capitalize()
|
||||
elif capitalize == 'true':
|
||||
word = word.capitalize()
|
||||
my_words.append(word)
|
||||
|
||||
base_pwd = separator.join(my_words)
|
||||
leading_digits = ''.join(random.choice('1234567890') for i in range(nr_of_ld))
|
||||
trailing_digits = ''.join(random.choice('1234567890') for i in range(nr_of_td))
|
||||
leading_chars = ''.join(random.choice(special_chars) for i in range(nr_of_lc))
|
||||
trailing_chars = ''.join(random.choice(special_chars) for i in range(nr_of_tc))
|
||||
pwd = '{ld}{lc}{base}{tc}{td}'.format(
|
||||
lc=leading_chars,
|
||||
ld=leading_digits,
|
||||
base=base_pwd,
|
||||
td=trailing_digits,
|
||||
tc=trailing_chars)
|
||||
# blind entropy is calculated only by the number of unique characters
|
||||
# and password length
|
||||
blind_entropy = math.log(len(''.join(set(pwd)))**len(pwd), 2)
|
||||
|
||||
if max_len and len(pwd) > max_len:
|
||||
continue
|
||||
res[pwd] = {
|
||||
'length': len(pwd),
|
||||
'entropy': blind_entropy,
|
||||
}
|
||||
return (res, seen_entropy)
|
34
setup.py
Normal file
34
setup.py
Normal file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
from os import environ
|
||||
|
||||
try:
|
||||
from setuptools import setup
|
||||
except ImportError:
|
||||
from distutils import setup
|
||||
|
||||
import pwgen
|
||||
|
||||
version = '0.1'
|
||||
|
||||
setup(
|
||||
name='pwgen',
|
||||
version=str(version),
|
||||
description="Passphrase generator",
|
||||
author="Fredrik Eriksson",
|
||||
author_email="pwgen@wb9.se",
|
||||
url="https://github.com/fredrik-eriksson/pwgen",
|
||||
platforms=['any'],
|
||||
license='BSD',
|
||||
packages=['pwgen'],
|
||||
classifiers=[
|
||||
'Development Status :: 1 - Planning',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
keywords='password passphrase',
|
||||
scripts=['bin/pwgen']
|
||||
)
|
Loading…
Reference in New Issue
Block a user