From c1247f96eec89bbc0f20ec727a0234886531eb2b Mon Sep 17 00:00:00 2001 From: Dean Jackson Date: Fri, 31 Jul 2015 14:56:39 +0200 Subject: [PATCH] Add custom generator support - Add separate base class for word-based generators - Refactor built-in generators - Improve documentation --- ...w => Password Generator-1.2.alfredworkflow | Bin 182795 -> 183174 bytes README.md | 152 +++++++++++- src/generators/__init__.py | 224 ++++++++++++++++-- src/generators/base.py | 79 ------ src/generators/gen_basic.py | 9 +- src/generators/gen_dictionary.py | 52 +--- src/generators/gen_german.py | 10 +- src/generators/gen_pronounceable.py | 54 ++--- src/generators/gen_pronounceable_markov.py | 59 ++--- src/pwgen.py | 19 +- src/version | 2 +- 11 files changed, 434 insertions(+), 226 deletions(-) rename Password Generator-1.1.alfredworkflow => Password Generator-1.2.alfredworkflow (91%) delete mode 100644 src/generators/base.py diff --git a/Password Generator-1.1.alfredworkflow b/Password Generator-1.2.alfredworkflow similarity index 91% rename from Password Generator-1.1.alfredworkflow rename to Password Generator-1.2.alfredworkflow index 11fb5d95a298eae3945a6f11baefc4536cd2a691..fbb3775f15331e5e307d6a4504eec0d32acc576e 100644 GIT binary patch delta 12991 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 P!2B%&@!fCTQ^5ZN<~rn-1, so 2,147,483,647 guesses for a 32-bit password. Or **0.048 seconds** with the above hardware. -Fortunately, every added bit doubles the amount of entropy passwords, so 64 bits is a good deal stronger: 6.5 **years** on average to guess on the same hardware. +Fortunately, every added bit doubles the amount of entropy passwords, so 64 bits is a good deal stronger: 6.5 *years* on average to guess on the same hardware. | Level | Min. entropy | Time to guess \* | Application | | ------: | --------------------: | --------------: | --------------------------- | @@ -101,7 +144,7 @@ If no update is available, "No Update is Available" will be shown. Action this i ### Default Password Strength ### -The default strength for passwords generated with `pwgen`. For strength *n*, passwords will have *n\*32* bits of entropy. Default is `3`, which should be proof against anything but the NSA. `4` will generate extremely secure passwords. +The default strength for passwords generated with `pwgen`. For strength *n*, passwords will have *n*\*32 bits of entropy. Default is `3`, which should be proof against anything but the NSA. `4` will generate extremely secure passwords. Action this item to enter a new default strength. @@ -132,8 +175,12 @@ Action a generator to toggle it on or off. ## Built-in generators ## +See [below](#custom-generators) for details of how to add your own custom generators. + The workflow includes 10 built-in generators, of which 6 are active by default. You can activate/deactivate them in the [Configuration](#configuration). +**Note**: Word-based password generators return passwords with their component words joined by hyphens. These hyphens are not included in the calculation of the password strength, so removing them will leave you with a password of the stated strength. + ### Active generators ### @@ -141,7 +188,7 @@ These generators are active by default. #### ASCII Generator #### -Generates passwords based on all ASCII characters, minus a few punctuation marks that can be hard to type, such as `\\`, `\``, `~`. +Generates passwords based on all ASCII characters, minus a few punctuation marks that can be hard to type, such as \\\`~ (backslash, backtick, tilde). #### Alphanumeric Generator #### @@ -151,7 +198,7 @@ Generates passwords from ASCII letters and digits. No punctuation. #### Clear Alphanumeric Generator #### -Generates passwords from ASCII letters and digits, excluding easily-confused characters `1`, `l`, `O`, `0` (lowercase L, uppercase O, the digits 1 and 0). +Generates passwords from ASCII letters and digits, excluding the easily-confused characters lO01 (lowercase L, uppercase O, the digits 1 and 0). #### Numeric Generator #### @@ -183,12 +230,12 @@ Has slightly more entropy than the [Pronounceable Nonsense generator](#pronounce #### German Generator #### -Generate passwords using all ASCII characters (including punctuation), plus German characters (esset, umlauts). +Generate passwords using the German alphabet, digits and punctuation. #### German Alphanumeric Generator #### -Generate passwords using all ASCII characters (without punctuation), plus German characters (esset, umlauts). +Generate passwords using the German alphabet and digits without punctuation. #### German Pronounceable Markov Generator #### @@ -196,10 +243,94 @@ Generate passwords using all ASCII characters (without punctuation), plus German Generates semi-pronounceable passwords based on Markov chains and the start of *Buddenbrooks*. +## Custom generators ## + +You can easily add your own custom password generators. These must be placed in the directory `~/Library/Application Support/Alfred 2/Workflow Data/net.deanishe.alfred-pwgen/generators/`. If you put your custom generators in the workflow itself, they will be deleted when the workflow is updated. You can quickly open up the above directory by entering the Alfred-Workflow magic argument `workflow:opendata` as your query, e.g. `pwconf workflow:opendata`. + +**Note**: Your generators will be deactivated by default. You must manually activate them using `pwconf` to use them. + +Modules containing your custom generators *must* be named `gen_XXX.py`. + +Only files matching the pattern `gen_*.py` will be imported. + +Your generator classes *must* subclass `generators.PassGenBase` (or `generators.WordGenBase`), an abstract base class, and *must* have `id_`, `name`, `description` and `data` properties. These have the following purposes: + +| Property | Purpose | +|---------------|-------------------------------------------------------------------------------| +| `id_` | Short name for your generator, used internally. | +| `name` | The human-readable name, shown in the configuration. | +| `description` | The longer description shown beneath the generator name in the configuration. | +| `data` | Return a sequence of the characters to generate passwords from. | + +The `data` property is used by the `entropy` property and `password()` method on the `PassGenBase` base class. + +`PassGenBase` is designed for character-based passwords, i.e. `data` returns a string or other sequence of single characters. `WordGenBase` is for word-based passwords, i.e. `data` returns a sequence of multi-character strings. The main difference is in the implementation of length-based passwords. + +**Important:** `data` must return a sequence (`string`, `list` or `tuple`) *not* a generator or `set`. If `random.choice()` chokes on it, it's no good. + + +### Examples ### + +A generator to produce German passwords (i.e. possibly including letters like 'ü' or 'ä'): + +```python + +import string +from generators import PassGenBase, punctuation +# punctuation is `string.punctuation` minus a few +# problematic/hard-to-type symbols (e.g. backslash) + +class GermanGenerator(PassGenBase): + """Generate passwords containing umlauts.""" + + @property + def id_(self): + return 'deutsch' + + @property + def name(self): + return 'German' + + @property + def description(self): + return 'German alphabet, digits and punctuation' + + @property + def data(self): + return string.ascii_letters + string.digits + punctuation + 'üäöÜÄÖß' +``` + +A word-based generator to produce Swedish passwords might look like this: + +```python +from generators import WordGenBase + +class BorkGenerator(WordGenBase): + """Bork-bork-bork""" + + @property + def id_(self): + return 'bork' + + @property + def name(self): + return 'Bork' + + @property + def description(self): + return 'Borked password generator' + + @property + def data(self): + return ['bork', 'bork', 'bork'] +``` + ## Licensing, thanks ## This workflow is released under the [MIT Licence](http://opensource.org/licenses/MIT), which is included as the LICENCE file. +The code for the Markov chain comes from [a SimonSapin snippet][markov], and the gibberish-generating code is from a [Greg Haskins StackOverflow answer][gibberish]. + It is heavily based on the [Alfred-Workflow](https://github.com/deanishe/alfred-workflow) library, also released under the MIT Licence. The workflow icon is from the [IcoMoon](https://icomoon.io/) webfont \([licence](https://icomoon.io/#termsofuse)\). @@ -224,7 +355,16 @@ Initial release - Add strength bar toggle to configuration - Improve filtering in configuration +### Version 1.2 (2015-07-31) ### + +- Add separate base class for word-based generators +- Add custom (user) generator support +- Refactor built-in generators + [demo]: https://github.com/deanishe/alfred-pwgen/raw/master/demo.gif "Alfred Password Generator Demo" [gh-releases]: https://github.com/deanishe/alfred-pwgen/releases [packal]: http://www.packal.org/workflow/secure-password-generator +[markov]: https://github.com/SimonSapin/snippets/blob/master/markov_passwords.py +[gibberish]: http://stackoverflow.com/a/5502875/356942 +[entropy]: https://en.wikipedia.org/wiki/Entropy_(computing) diff --git a/src/generators/__init__.py b/src/generators/__init__.py index 1680180..3df2d83 100644 --- a/src/generators/__init__.py +++ b/src/generators/__init__.py @@ -10,37 +10,207 @@ """ Package containing password generators. + +This module contains the machinery for loading generators and +the base class for generators. + +The default generators are contained in (and loaded from) the +other modules in this package matching the pattern ``gen_*.py``. + +All generators must subclass ``PassGenBase`` in order to +be recognised by the workflow. + """ -from __future__ import print_function, unicode_literals, absolute_import +from __future__ import ( + print_function, + unicode_literals, + absolute_import, + division +) +import abc import logging +import math import os +import random +import sys -from generators.base import PassGenBase, ENTROPY_PER_LEVEL - -__all__ = ['get_subclasses', 'get_generators', 'ENTROPY_PER_LEVEL'] +__all__ = [ + 'get_subclasses', + 'get_generators', + 'import_generators', + 'ENTROPY_PER_LEVEL', + 'PassGenBase', + 'punctuation', +] -_import_done = False +imported_dirs = set() log = logging.getLogger('workflow.generators') +ENTROPY_PER_LEVEL = 32 + +# string.punctuation contains a few characters we don't want +# like backslash and tilde +punctuation = """!"#$%&'()*+,-./:;<=>?@[]^_{|}""" + + +class PassGenBase(object): + """Base class for generators + + Generators *must* subclass this abstract base class. + + If you just use ``PassGenBase.register()``, the workflow + will not find the generator. + + Subclasses must override the ``id_``, ``name``, ``description`` + and ``data`` properties to be valid generators. + + A very simple generator can just return an interable of + characters from ``data``. The ``password`` method of this + base class will then generate a random password of the + required length/strength from the interable's contents. + + """ + __metaclass__ = abc.ABCMeta + + def password(self, strength=None, length=None): + """Method to generate and return password. + + Either ``strength`` or ``length`` must be specified. + + Returns tuple: (password, entropy) + + """ + + if strength is not None: + length = int(math.ceil(strength / self.entropy)) + + chars = self.data + pw = [chars[ord(c) % len(chars)] for c in os.urandom(length)] + return ''.join(pw), self.entropy * length + + @property + def entropy(self): + return math.log(len(self.data), 2) + + @abc.abstractproperty + def id_(self): + """Short name of the generator. + + Used in settings to identify the generator. + + """ + + return + + @abc.abstractproperty + def name(self): + """Human-readable name of the generator.""" + return + + @abc.abstractproperty + def description(self): + """Longer description of the generator.""" + return + + @abc.abstractproperty + def data(self): + """List of data to choose from.""" + return + + +def _get_generator_modules(dirpath): + """Return list of files in dirpath matching ``gen_*.py``""" + + modnames = [] -def _import_generators(): - """Import all generator modules within this package.""" - global _import_done - # Import all modules in this directory that match `gen_*.py` - dirpath = os.path.dirname(__file__) for filename in os.listdir(dirpath): if not filename.endswith('.py') or not filename.startswith('gen_'): continue - modname = filename[:-3] # Remove extension - log.debug('Importing generators from : %s ...', modname) + + modnames.append(os.path.splitext(filename)[0]) + + return modnames + + +class WordGenBase(PassGenBase): + """""" + + def _password_by_iterations(self, iterations): + """Return password using ``iterations`` iterations.""" + words = [] + rand = random.SystemRandom() + words = [rand.choice(self.data) for i in range(iterations)] + return '-'.join(words), self.entropy * iterations + + def _password_by_length(self, length): + """Return password of length ``length``.""" + words = [] + pw_length = 0 + rand = random.SystemRandom() + while pw_length < length: + word = rand.choice(self.data) + words.append(word) + pw_length += len(word) + 1 + + pw = '-'.join(words) + + return pw, self.entropy * len(words) + + def password(self, strength=None, length=None): + """Method to generate and return password. + + Either ``strength`` or ``length`` must be specified. + + Returns tuple: (password, entropy) + + """ + + if strength is not None: + iterations = int(math.ceil(strength / self.entropy)) + return self._password_by_iterations(iterations) + + else: + return self._password_by_length(length) + + +def import_generators(dirpath): + """Import all ``gen_*.py`` modules within directory ``dirpath``. + + Modules will be imported under ``generators.user_``. + + As a result, user modules may override built-ins. + + """ + + dirpath = os.path.abspath(dirpath) + + if dirpath in imported_dirs: + log.debug('Directory already imported : `%s`', dirpath) + return + + imported_dirs.add(dirpath) + + # Is ``dirpath`` this package? + builtin = dirpath == os.path.abspath(os.path.dirname(__file__)) + + if not builtin and dirpath not in sys.path: + sys.path.append(dirpath) + + kind = ('user', 'built-in')[builtin] + + for modname in _get_generator_modules(dirpath): + + if builtin: + modname = 'generators.%s' % modname + try: - __import__('generators.{0}'.format(modname)) + __import__(modname) + log.debug('Imported %s generators from `%s`', kind, modname) except Exception as err: - log.error("Couldn't import `%s` : %s", modname, err) - _import_done = True + log.error('Error importing `%s` : %s', modname, err) def get_subclasses(klass): @@ -51,22 +221,34 @@ def get_subclasses(klass): """ subclasses = [] - for klass in klass.__subclasses__(): - subclasses.append(klass) - subclasses += get_subclasses(klass) + + for cls in klass.__subclasses__(): + subclasses.append(cls) + subclasses += get_subclasses(cls) + return subclasses def get_generators(): - """Return a list containing an instance of each available generator.""" + """Return a list containing an instance of each available generator. + + It would be preferable to return the class (not all generators are + needed), but abstract base classes use properties, not attributes, + to enforce interface compliance :( + + """ + generators = [] + builtin_dir = os.path.abspath(os.path.dirname(__file__)) - if not _import_done: - _import_generators() + # Import the built-ins only once + if builtin_dir not in imported_dirs: + import_generators(builtin_dir) for klass in get_subclasses(PassGenBase): try: inst = klass() + log.debug('Loaded generator : `%s`', inst.name) except Exception as err: log.error(err) else: diff --git a/src/generators/base.py b/src/generators/base.py deleted file mode 100644 index 9a12308..0000000 --- a/src/generators/base.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# -# Copyright © 2015 deanishe@deanishe.net -# -# MIT Licence. See http://opensource.org/licenses/MIT -# -# Created on 2015-07-27 -# - -""" -""" - -from __future__ import ( - print_function, - unicode_literals, - absolute_import, - division -) - -import abc -import math -import os - -ENTROPY_PER_LEVEL = 32 - -# string.punctuation contains a few characters we don't want -# like backslash and tilde -punctuation = """!"#$%&'()*+,-./:;<=>?@[]^_{|}""" - - -class PassGenBase(object): - """Base class for generators""" - __metaclass__ = abc.ABCMeta - - def password(self, strength=None, length=None): - """Method to generate and return password. - - Either ``strength`` or ``length`` must be specified. - - Returns tuple: (password, entropy) - - """ - - if strength is not None: - length = int(math.ceil(strength / self.entropy)) - - chars = self.data - pw = [chars[ord(c) % len(chars)] for c in os.urandom(length)] - return ''.join(pw), self.entropy * length - - @property - def entropy(self): - return math.log(len(self.data), 2) - - @abc.abstractproperty - def id_(self): - """Short name of the generator. - - Used in settings to identify the generator. - - """ - - return - - @abc.abstractproperty - def name(self): - """Human-readable name of the generator.""" - return - - @abc.abstractproperty - def description(self): - """Longer description of the generator.""" - return - - @abc.abstractproperty - def data(self): - """List of data to choose from.""" - return diff --git a/src/generators/gen_basic.py b/src/generators/gen_basic.py index de0277d..0725154 100644 --- a/src/generators/gen_basic.py +++ b/src/generators/gen_basic.py @@ -9,14 +9,19 @@ # """ -Basic generators. +Basic password generators. + +These generators just return a list of characters from their +``data`` properties and rely on the ``password()`` method +of ``PassGenBase`` to generate the passwords. + """ from __future__ import print_function, unicode_literals, absolute_import import string -from generators.base import PassGenBase, punctuation +from generators import PassGenBase, punctuation class AsciiGenerator(PassGenBase): diff --git a/src/generators/gen_dictionary.py b/src/generators/gen_dictionary.py index 62ab219..7ed0a37 100644 --- a/src/generators/gen_dictionary.py +++ b/src/generators/gen_dictionary.py @@ -9,6 +9,8 @@ # """ +A password generator based on the contents of ``/usr/share/dict/words`` + """ from __future__ import print_function, unicode_literals, absolute_import @@ -16,10 +18,19 @@ import math import random -from generators.base import PassGenBase +from generators import WordGenBase + + +class WordlistGenerator(WordGenBase): + """Generate passwords based on the ``words`` file included with OS X. + + There's not a huge amount of entropy, so the passwords need to be + rather long. But they are easier to remember than most of the others. + The words in the passwords are joined with hyphens, but these are + not included in the calculation of password strength. -class WordlistGenerator(PassGenBase): + """ _filepath = '/usr/share/dict/words' _maxlen = 6 # Ignore words longer than this @@ -52,40 +63,3 @@ def name(self): @property def description(self): return 'Dictionary words' - - def _password_by_iterations(self, iterations): - """Return password using ``iterations`` iterations.""" - words = [] - rand = random.SystemRandom() - words = [rand.choice(self.data) for i in range(iterations)] - return '-'.join(words), self.entropy * iterations - - def _password_by_length(self, length): - """Return password of length ``length``.""" - words = [] - pw_length = 0 - rand = random.SystemRandom() - while pw_length < length: - word = rand.choice(self.data) - words.append(word) - pw_length += len(word) + 1 - - pw = '-'.join(words) - - return pw, self.entropy * len(words) - - def password(self, strength=None, length=None): - """Method to generate and return password. - - Either ``strength`` or ``length`` must be specified. - - Returns tuple: (password, entropy) - - """ - - if strength is not None: - iterations = int(math.ceil(strength / self.entropy)) - return self._password_by_iterations(iterations) - - else: - return self._password_by_length(length) diff --git a/src/generators/gen_german.py b/src/generators/gen_german.py index df8d46c..7a6372d 100644 --- a/src/generators/gen_german.py +++ b/src/generators/gen_german.py @@ -10,6 +10,10 @@ """ Password genrators based on the German alphabet. + +These are variations on the default generators using an alphabet +extended with German letters. + """ from __future__ import print_function, unicode_literals, absolute_import @@ -18,6 +22,10 @@ from .gen_pronounceable_markov import PronounceableMarkovGenerator +# Umlauts, lovely umlauts +german_chars = 'ÄäÖöÜüß' + + class GermanGenerator(AsciiGenerator): """ASCII + German characters.""" @@ -35,7 +43,6 @@ def description(self): @property def data(self): - german_chars = 'ÄäÖöÜüß' return super(GermanGenerator, self).data + german_chars @@ -56,7 +63,6 @@ def description(self): @property def data(self): - german_chars = 'ÄäÖöÜüß' return super(GermanAlphanumericGenerator, self).data + german_chars diff --git a/src/generators/gen_pronounceable.py b/src/generators/gen_pronounceable.py index 10d66dd..2cffaec 100644 --- a/src/generators/gen_pronounceable.py +++ b/src/generators/gen_pronounceable.py @@ -9,7 +9,7 @@ # """ -Generate gibberish words. +Generate password from (mostly) gibberish words. http://stackoverflow.com/a/5502875/356942 """ @@ -17,11 +17,9 @@ from __future__ import print_function, unicode_literals, absolute_import import itertools -import math -import random import string -from generators.base import PassGenBase +from generators import WordGenBase initial_consonants = ( @@ -47,7 +45,17 @@ vowels = 'aeiou' -class PronounceableGenerator(PassGenBase): +class PronounceableGenerator(WordGenBase): + """Generate passwords based on (mostly) gibberish words. + + Better entropy (so stronger passwords for the same bits) than + the dictionary-based generator (``WordlistGenerator``), but + a bit harder to remember. + + The words in the passwords are joined with hyphens, but these are + not included in the calculation of password strength. + + """ def __init__(self): self._syllables = None @@ -74,41 +82,7 @@ def name(self): def description(self): return 'Pronounceable, (mostly) nonsense words' - def _password_by_iterations(self, iterations): - """Return password using ``iterations`` iterations.""" - - rand = random.SystemRandom() - words = [rand.choice(self.data) for i in range(iterations)] - return '-'.join(words), self.entropy * iterations - - def _password_by_length(self, length): - """Return password of length ``length``.""" - - words = [] - pw_length = 0 - rand = random.SystemRandom() - while pw_length < length: - word = rand.choice(self.data) - words.append(word) - pw_length += len(word) + 1 - pw = '-'.join(words) - if len(pw) > length: - pw = pw[:length] - pw.rstrip('-') - - return pw, self.entropy * len(words) - - def password(self, strength=None, length=None): - """Generate and return password.""" - - if strength is not None: - iterations = int(math.ceil(strength / self.entropy)) - return self._password_by_iterations(iterations) - - else: - return self._password_by_length(length) - if __name__ == '__main__': - gen = PronounceableAltGenerator() + gen = PronounceableGenerator() print(gen.password(length=30)) diff --git a/src/generators/gen_pronounceable_markov.py b/src/generators/gen_pronounceable_markov.py index a562ebd..a3193c1 100644 --- a/src/generators/gen_pronounceable_markov.py +++ b/src/generators/gen_pronounceable_markov.py @@ -9,6 +9,11 @@ # """ +Generate English-sounding passwords using Markov chains. + +The Markov chain is based on the first few paragraphs of +*A Tale of Two Cities*. + """ from __future__ import ( @@ -20,12 +25,11 @@ from collections import defaultdict import itertools -import math import os import string import random -from generators.base import PassGenBase, ENTROPY_PER_LEVEL +from generators import WordGenBase # Markov chain code from @@ -130,13 +134,21 @@ def __iter__(self): yield ''.join(itertools.islice(chain, length)) -class PronounceableMarkovGenerator(PassGenBase): - """Pronounceable passwords based on Markov chains.""" +class PronounceableMarkovGenerator(WordGenBase): + """Pronounceable passwords based on Markov chains. + + The gibberish-based generator (``PronounceableGenerator``) generally + provides more pronounceable passwords. + + The words in the passwords are joined with hyphens, but these are + not included in the calculation of password strength. + + """ _sample_file = 'english.txt' def __init__(self): - self._sample = None + self._data = None self._generator = None @property @@ -145,15 +157,11 @@ def id_(self): @property def name(self): - return 'Pronounceable Markov' + return 'Pronounceable Markov chain' @property def description(self): - return 'Pronounceable, English & Markov' - - @property - def data(self): - return None + return 'Pronounceable, Markov-chain-generated English' @property def entropy(self): @@ -163,27 +171,26 @@ def entropy(self): # return self.generator.entropy @property - def sample(self): - if not self._sample: + def data(self): + if not self._data: path = os.path.join(os.path.dirname(__file__), self._sample_file) with open(path, 'rb') as fp: - self._sample = fp.read().decode('utf-8') + self._data = fp.read().decode('utf-8') - return self._sample + return self._data @property def generator(self): if not self._generator: - self._generator = WordGenerator(self.sample) + self._generator = WordGenerator(self.data) return self._generator def _password_by_iterations(self, iterations): """Return password using ``iterations`` iterations.""" words = [] - gen = WordGenerator(self.sample) - words = itertools.islice(gen, iterations) + words = itertools.islice(self.generator, iterations) return '-'.join(words), self.entropy * iterations def _password_by_length(self, length): @@ -201,22 +208,6 @@ def _password_by_length(self, length): return pw, self.entropy * len(words) - def password(self, strength=None, length=None): - """Method to generate and return password. - - Either ``strength`` or ``length`` must be specified. - - Returns tuple: (password, entropy) - - """ - - if strength is not None: - iterations = int(math.ceil(strength / self.entropy)) - return self._password_by_iterations(iterations) - - else: - return self._password_by_length(length) - if __name__ == '__main__': # gen = PronounceableGenerator() diff --git a/src/pwgen.py b/src/pwgen.py index e7007d6..19193f4 100644 --- a/src/pwgen.py +++ b/src/pwgen.py @@ -32,13 +32,14 @@ from __future__ import print_function, unicode_literals, absolute_import import logging +import os import subprocess import sys from docopt import docopt from workflow import Workflow, ICON_WARNING, ICON_HELP, ICON_SETTINGS -from generators import get_generators, ENTROPY_PER_LEVEL +from generators import get_generators, ENTROPY_PER_LEVEL, import_generators log = None @@ -177,6 +178,19 @@ def run(self): elif args.get('set'): return self.do_set() + def load_user_generators(self): + """Ensure any user generators are loaded""" + + user_generator_dir = wf.datafile('generators') + + # Create user generator directory + if not os.path.exists(user_generator_dir): + os.makedirs(user_generator_dir, 0700) + else: # Import user generators + # log.debug('Importing user generators from `%s` ...', + # user_generator_dir) + import_generators(user_generator_dir) + def do_generate(self): """Generate and display passwords from active generators.""" wf = self.wf @@ -233,6 +247,7 @@ def do_generate(self): log.info('Password strength: %d bits', pw_strength) + self.load_user_generators() generators = get_generators() # Filter out inactive generators @@ -354,7 +369,7 @@ def do_conf(self): ) # Generators - + self.load_user_generators() generators = get_generators() active_generators = wf.settings.get('generators', []) diff --git a/src/version b/src/version index b123147..ea710ab 100644 --- a/src/version +++ b/src/version @@ -1 +1 @@ -1.1 \ No newline at end of file +1.2 \ No newline at end of file