diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..617b6ae --- /dev/null +++ b/LICENCE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 deanishe@deanishe.net + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Password Generator-1.0.alfredworkflow b/Password Generator-1.1.alfredworkflow similarity index 93% rename from Password Generator-1.0.alfredworkflow rename to Password Generator-1.1.alfredworkflow index a81fadd..11fb5d9 100644 Binary files a/Password Generator-1.0.alfredworkflow and b/Password Generator-1.1.alfredworkflow differ diff --git a/README.md b/README.md index 2ae54fa..e067f0b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Alfred Password Generator # -Generate secure random passwords from Alfred. +Generate secure random passwords from Alfred. Uses `/dev/urandom` as source of entropy. ## Features ## @@ -14,25 +14,32 @@ Generate secure random passwords from Alfred. ## Usage ## - `pwgen []` — Generate passwords of specified strength. Default is `3` (96 bits of entropy). See [Password strength](#password-strength) for details. + - `↩` or `⌘+C` — Copy the selected password to the clipboard. + - `⌘+↩` — Copy the selected password to the clipboard and paste it to the frontmost application. + - `⌘+L` — Show the selected password in Alfred's Large Text window. - `pwlen []` — Generate passwords of specified length. Default is `20`. See [Password strength](#password-strength) for details. -- `pwconf` — View and edit workflow settings. See [Configuration](configuration) for details. + - `↩` or `⌘+C` — Copy the selected password to the clipboard. + - `⌘+↩` — Copy the selected password to the clipboard and paste it to the frontmost application. + - `⌘+L` — Show the selected password in Alfred's Large Text window. +- `pwconf []` — View and edit workflow settings. See [Configuration](#configuration) for details. **Note:** Word-based generators may provide passwords that are slightly longer than ``. ## Password strength ## -Passwords can be specified either by strength or length. The default strength is `3`, which is approx. 90 bits of entropy (each level is 32 bits). You may also specify the desired number of bits by appending `b` to your input, e.g. `pwgen 128b` will provide at least 128 bits of entropy. +Passwords can be specified either by strength or length. The default strength is `3`, which is at least 96 bits of entropy (each level is 32 bits). You may also specify the desired number of bits by appending `b` to your input, e.g. `pwgen 128b` will provide at least 128 bits of entropy. Default length is 20 characters, which can provide ~50 to ~130 bits of entropy depending on generator. Each password has its strength in the result subtitle. This is shown either as a bar or in bits of entropy, depending on your settings. Each full block in the bar represents 30 bits of entropy. + ### How strong should my passwords be? ### That depends on what you're using it for and how long you want it to remain secure. As of 2015, custom password-guessing hardware (built from standard PC components) can guess **>45 billion passwords per second.** -The average number of guesses required to crack a password *n* bits is 2n-1, so 2,147,483,647 guesses for a 32-bit password. Or **0.048 seconds** with the above hardware. +The average number of guesses required to crack a password with *n* bits of entropy is 2n-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 number of possible passwords, so 64 bits is a good deal stronger: 6.5 **years** on average to guess on the same hardware. @@ -50,6 +57,13 @@ The default password strength level of 3 (96 bits) provides very secure password The default password length of 20 characters provides reasonably to very secure passwords, depending on the generator. +#### Displayed strength #### + +By default, the strength of generated passwords is shown as a bar in the result subtitle. Each full block represents 32 bits of entropy, so 2 blocks represents a pretty secure password, 3 or more a very secure password. + +You can have the precise number of bits displayed instead by toggling the "Strength Bar" setting in the [Configuration](#configuration) (keyword `pwconf`). + + #### How can passwords of the same strength have different levels of security? #### Passwords of the same length (or even the self-same password) generated using different techniques have different strengths because the strength is determined by the permutations in the algorithm and the password length. @@ -61,4 +75,144 @@ See [Password strength on Wikipedia](https://en.wikipedia.org/wiki/Password_stre ## Configuration ## -Not implemented yet. +Access the configuraton options with the `pwconf` keyword. You can use an optional query to filter the available options, e.g. use `pwconf gen` to show only the available generators. + +The following configuraton options are available: + + +### Open Help ### + +Action this item to open this README in your browser. + + +### An Update is Available / No Update is Available ### + +The workflow checks for a new version once a day. If one is available, "An Update is Available" will be shown. Action this item to install the update. + +If no update is available, "No Update is Available" will be shown. Action this item to force a check for an update. + + +### 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. + +Action this item to enter a new default strength. + + +### Default Password Length ### + +The default length in characters for passwords generated with `pwlen`. The default of `20` provides passwords that are reasonably to very secure, depending on the generator. + +Action this item to enter a new default length. + + +### Strength Bar ### + +By default, password strength is indicated by a number of blocks. Each full block represents 32 bits of entropy, so 3 blocks is secure, 4 is very secure. Less that 3 blocks should be avoided. + +Alternatively, strength can be shown as the number of bits of entropy. + +Action this item to toggle the strength bar on/off. + + +### Generators ### + +All the available generators are listed below the other options. + +Active generators have a checked green circle as their icon, inactive ones have an empty red circle icon. + +Action a generator to toggle it on or off. + +## Built-in generators ## + +The workflow includes 10 built-in generators, of which 6 are active by default. You can activate/deactivate them in the [Configuration](#configuration). + + +### Active generators ### + +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 `\\`, `\``, `~`. + + +#### Alphanumeric Generator #### + +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). + + +#### Numeric Generator #### + +Generates digit-only passwords. + + +#### Pronounceable Nonsense Generator #### + +Generates pronounceable passwords based on nonsense words. Based on [this GitHub comment](http://stackoverflow.com/a/5502875). + + +#### Dictionary Generator #### + +Generates passwords based on the words in `/usr/share/dict/words`. + + +### Inactive generators ### + +These generators are inactive by default. They can be turned on in the [Configuration](#configuration). + + +#### Pronounceable Markov Generator #### + +Generates semi-pronounceable passwords based on Markov chains and the start of *A Tale of Two Cities*. + +Has slightly more entropy than the [Pronounceable Nonsense generator](#pronounceable-nonsense-generator), but the passwords aren't quite as pronounceable. + + +#### German Generator #### + +Generate passwords using all ASCII characters (including punctuation), plus German characters (esset, umlauts). + + +#### German Alphanumeric Generator #### + +Generate passwords using all ASCII characters (without punctuation), plus German characters (esset, umlauts). + + +#### German Pronounceable Markov Generator #### + +Generates semi-pronounceable passwords based on Markov chains and the start of *Buddenbrooks*. + + +## Licensing, thanks ## + +This workflow is released under the [MIT Licence](http://opensource.org/licenses/MIT), which is included as the LICENCE file. + +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)\). + +The other icons are based on the [Font Awesome](http://fortawesome.github.io/Font-Awesome/) webfont \([licence](http://scripts.sil.org/OFL)\). + + +## Changelog ## + +### Version 1.0 (2015-07-28) ### + +Initial release + + +### Version 1.1 (2015-07-28) ### + +- Replace default Markov pronounceable generator with gibberish. +- Rename Dictionary generator module +- Add licence and licensing info +- Improve usage description in README +- Add generator descriptions to README +- Add strength bar toggle to configuration +- Improve filtering in configuration diff --git a/TODO b/TODO index 259cc73..597f696 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,9 @@ Features to add: + - Allow default strength to be a bit value, e.g. `64b` Optional features: ___________________ Archive: + - Make word-based passwords work better with specific lengths. @done(15-07-28 13:19) @project(Features to add) - Implement configuration @done(15-07-28 11:34) @project(Features to add) diff --git a/src/generators/gen_basic.py b/src/generators/gen_basic.py index 9ffccb4..de0277d 100644 --- a/src/generators/gen_basic.py +++ b/src/generators/gen_basic.py @@ -52,7 +52,7 @@ def name(self): @property def description(self): - return 'ASCII characters without punctuation' + return 'ASCII characters, no punctuation' @property def data(self): @@ -72,7 +72,7 @@ def name(self): @property def description(self): - return 'ASCII characters without confusing characters or punctuation' + return 'ASCII characters, no confusing characters or punctuation' @property def data(self): diff --git a/src/generators/gen_realwords.py b/src/generators/gen_dictionary.py similarity index 100% rename from src/generators/gen_realwords.py rename to src/generators/gen_dictionary.py diff --git a/src/generators/gen_german.py b/src/generators/gen_german.py index c3f1519..df8d46c 100644 --- a/src/generators/gen_german.py +++ b/src/generators/gen_german.py @@ -15,7 +15,7 @@ from __future__ import print_function, unicode_literals, absolute_import from .gen_basic import AsciiGenerator, AlphanumGenerator -from .gen_pronounceable import PronounceableGenerator +from .gen_pronounceable_markov import PronounceableMarkovGenerator class GermanGenerator(AsciiGenerator): @@ -60,7 +60,7 @@ def data(self): return super(GermanAlphanumericGenerator, self).data + german_chars -class GermanPronounceableGenerator(PronounceableGenerator): +class GermanPronounceableGenerator(PronounceableMarkovGenerator): """Pronounceable German passwords based on Markov chains.""" _sample_file = 'german.txt' @@ -71,7 +71,7 @@ def id_(self): @property def name(self): - return 'German Pronounceable' + return 'German Pronounceable Markov' @property def description(self): diff --git a/src/generators/gen_pronounceable.py b/src/generators/gen_pronounceable.py index c2f7003..10d66dd 100644 --- a/src/generators/gen_pronounceable.py +++ b/src/generators/gen_pronounceable.py @@ -9,135 +9,58 @@ # """ +Generate gibberish words. + +http://stackoverflow.com/a/5502875/356942 """ -from __future__ import ( - print_function, - unicode_literals, - absolute_import, - division -) +from __future__ import print_function, unicode_literals, absolute_import -from collections import defaultdict import itertools import math -import os -import string import random +import string -from generators.base import PassGenBase, ENTROPY_PER_LEVEL - - -# Markov chain code from -# https://github.com/SimonSapin/snippets/blob/master/markov_passwords.py - - -def pairwise(iterable): - """ - Yield pairs of consecutive elements in iterable. - - >>> list(pairwise('abcd')) - [('a', 'b'), ('b', 'c'), ('c', 'd')] - """ - iterator = iter(iterable) - try: - a = iterator.next() - except StopIteration: - return - for b in iterator: - yield a, b - a = b - - -class MarkovChain(object): - """Markov chain implementation. Initialise with text ``sample``. - - If a system transits from a state to another and the next state depends - only on the current state and not the past, it is said to be a - Markov chain. - - It is determined by the probability of each next state from any current - state. - - See http://en.wikipedia.org/wiki/Markov_chain - - The probabilities are built from the frequencies in the `sample` chain. - Elements of the sample that are not a valid state are ignored. - - """ - - def __init__(self, sample): - self._rand = random.SystemRandom() - self.counts = counts = defaultdict(lambda: defaultdict(int)) - - for word in sample.split(' '): - for current, nxt in pairwise(word): - counts[current][nxt] += 1 - - self.totals = dict( - (current, sum(next_counts.itervalues())) - for current, next_counts in counts.iteritems() - ) - - def next(self, state): - """Return random next state. - - Choose at random and return a next state from a current state, - according to the probabilities for this chain - - """ - - nexts = self.counts[state].iteritems() - # Like random.choice() but with a different weight for each element - rand = self._rand.randrange(0, self.totals[state]) - # Using bisection here could be faster, but simplicity prevailed. - # (Also it’s not that slow with 26 states or so.) - for next_state, weight in nexts: - if rand < weight: - return next_state - rand -= weight - - def __iter__(self): - """Return an infinite iterator of states.""" - - state = self._rand.choice(self.counts.keys()) - while True: - state = self.next(state) - yield state - - -class WordGenerator(object): - """Yield pronounceable words""" +from generators.base import PassGenBase - def __init__(self, sample, min_length=3, max_length=6): - self.min_length = min_length - self.max_length = max_length - # Remove punctuation, numbers and some whitespace from sample - bad_chars = string.punctuation + string.digits + '\n\t' - self.sample = ''.join([c for c in sample.lower() - if c not in bad_chars]) +initial_consonants = ( + set(string.ascii_lowercase) - set('aeiou') + # remove those easily confused with others + - set('qxc') + # add some crunchy clusters + | set(['bl', 'br', 'cl', 'cr', 'dr', 'fl', + 'fr', 'gl', 'gr', 'pl', 'pr', 'sk', + 'sl', 'sm', 'sn', 'sp', 'st', 'str', + 'sw', 'tr']) +) - # import math - # chain = MarkovChain(self.sample) - # self.entropy = math.log(sum(chain.totals.values()), 2) +final_consonants = ( + set(string.ascii_lowercase) - set('aeiou') + # confusable + - set('qxcsj') + # crunchy clusters + | set(['ct', 'ft', 'mp', 'nd', 'ng', 'nk', 'nt', + 'pt', 'sk', 'sp', 'ss', 'st']) +) - def __iter__(self): - rand = random.SystemRandom() - while True: - chain = MarkovChain(self.sample) - length = rand.randrange(self.min_length, self.max_length) - yield ''.join(itertools.islice(chain, length)) +vowels = 'aeiou' class PronounceableGenerator(PassGenBase): - """Pronounceable passwords based on Markov chains.""" - - _sample_file = 'english.txt' def __init__(self): - self._sample = None - self._generator = None + self._syllables = None + + @property + def data(self): + if not self._syllables: + # each syllable is consonant-vowel-consonant "pronounceable" + self._syllables = map(''.join, + itertools.product(initial_consonants, + vowels, + final_consonants)) + return self._syllables @property def id_(self): @@ -145,45 +68,17 @@ def id_(self): @property def name(self): - return 'Pronounceable' + return 'Pronounceable Nonsense' @property def description(self): - return 'Pronounceable, based on English' - - @property - def data(self): - return None - - @property - def entropy(self): - # Conservative estimate based on the number - # of elements in a chain based on `english.txt` - return 13.43 - # return self.generator.entropy - - @property - def sample(self): - if not self._sample: - path = os.path.join(os.path.dirname(__file__), - self._sample_file) - with open(path, 'rb') as fp: - self._sample = fp.read().decode('utf-8') - - return self._sample - - @property - def generator(self): - if not self._generator: - self._generator = WordGenerator(self.sample) - - return self._generator + return 'Pronounceable, (mostly) nonsense words' def _password_by_iterations(self, iterations): """Return password using ``iterations`` iterations.""" - words = [] - gen = WordGenerator(self.sample) - words = itertools.islice(gen, 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): @@ -191,24 +86,20 @@ def _password_by_length(self, length): words = [] pw_length = 0 - for word in self.generator: + rand = random.SystemRandom() + while pw_length < length: + word = rand.choice(self.data) words.append(word) pw_length += len(word) + 1 - if pw_length >= length: - break - 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): - """Method to generate and return password. - - Either ``strength`` or ``length`` must be specified. - - Returns tuple: (password, entropy) - - """ + """Generate and return password.""" if strength is not None: iterations = int(math.ceil(strength / self.entropy)) @@ -219,24 +110,5 @@ def password(self, strength=None, length=None): if __name__ == '__main__': - # gen = PronounceableGenerator() - # print(gen.password(30)) - words = set() - words_in_words = 0 - i = 1 - path = os.path.join(os.path.dirname(__file__), 'english.txt') - with open(path, 'rb') as fp: - sample = fp.read().decode('utf-8') - gen = WordGenerator(sample) - for word in gen: - if word in words: - words_in_words += 1 - else: - words.add(word) - words_in_words = 0 - if words_in_words == 10: - break - if i % 1000 == 0: - print('%d/%d unique words' % (len(words), i)) - i += 1 - print('%d words' % len(words)) + gen = PronounceableAltGenerator() + print(gen.password(length=30)) diff --git a/src/generators/gen_pronounceable_markov.py b/src/generators/gen_pronounceable_markov.py new file mode 100644 index 0000000..a562ebd --- /dev/null +++ b/src/generators/gen_pronounceable_markov.py @@ -0,0 +1,242 @@ +#!/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 +) + +from collections import defaultdict +import itertools +import math +import os +import string +import random + +from generators.base import PassGenBase, ENTROPY_PER_LEVEL + + +# Markov chain code from +# https://github.com/SimonSapin/snippets/blob/master/markov_passwords.py + + +def pairwise(iterable): + """ + Yield pairs of consecutive elements in iterable. + + >>> list(pairwise('abcd')) + [('a', 'b'), ('b', 'c'), ('c', 'd')] + """ + iterator = iter(iterable) + try: + a = iterator.next() + except StopIteration: + return + for b in iterator: + yield a, b + a = b + + +class MarkovChain(object): + """Markov chain implementation. Initialise with text ``sample``. + + If a system transits from a state to another and the next state depends + only on the current state and not the past, it is said to be a + Markov chain. + + It is determined by the probability of each next state from any current + state. + + See http://en.wikipedia.org/wiki/Markov_chain + + The probabilities are built from the frequencies in the `sample` chain. + Elements of the sample that are not a valid state are ignored. + + """ + + def __init__(self, sample): + self._rand = random.SystemRandom() + self.counts = counts = defaultdict(lambda: defaultdict(int)) + + for word in sample.split(' '): + for current, nxt in pairwise(word): + counts[current][nxt] += 1 + + self.totals = dict( + (current, sum(next_counts.itervalues())) + for current, next_counts in counts.iteritems() + ) + + def next(self, state): + """Return random next state. + + Choose at random and return a next state from a current state, + according to the probabilities for this chain + + """ + + nexts = self.counts[state].iteritems() + # Like random.choice() but with a different weight for each element + rand = self._rand.randrange(0, self.totals[state]) + # Using bisection here could be faster, but simplicity prevailed. + # (Also it’s not that slow with 26 states or so.) + for next_state, weight in nexts: + if rand < weight: + return next_state + rand -= weight + + def __iter__(self): + """Return an infinite iterator of states.""" + + state = self._rand.choice(self.counts.keys()) + while True: + state = self.next(state) + yield state + + +class WordGenerator(object): + """Yield pronounceable words""" + + def __init__(self, sample, min_length=3, max_length=6): + self.min_length = min_length + self.max_length = max_length + + # Remove punctuation, numbers and some whitespace from sample + bad_chars = string.punctuation + string.digits + '\n\t' + self.sample = ''.join([c for c in sample.lower() + if c not in bad_chars]) + + # import math + # chain = MarkovChain(self.sample) + # self.entropy = math.log(sum(chain.totals.values()), 2) + + def __iter__(self): + rand = random.SystemRandom() + while True: + chain = MarkovChain(self.sample) + length = rand.randrange(self.min_length, self.max_length) + yield ''.join(itertools.islice(chain, length)) + + +class PronounceableMarkovGenerator(PassGenBase): + """Pronounceable passwords based on Markov chains.""" + + _sample_file = 'english.txt' + + def __init__(self): + self._sample = None + self._generator = None + + @property + def id_(self): + return 'pronounceable-markov' + + @property + def name(self): + return 'Pronounceable Markov' + + @property + def description(self): + return 'Pronounceable, English & Markov' + + @property + def data(self): + return None + + @property + def entropy(self): + # Conservative estimate based on the number + # of elements in a chain based on `english.txt` + return 13.43 + # return self.generator.entropy + + @property + def sample(self): + if not self._sample: + path = os.path.join(os.path.dirname(__file__), + self._sample_file) + with open(path, 'rb') as fp: + self._sample = fp.read().decode('utf-8') + + return self._sample + + @property + def generator(self): + if not self._generator: + self._generator = WordGenerator(self.sample) + + return self._generator + + def _password_by_iterations(self, iterations): + """Return password using ``iterations`` iterations.""" + words = [] + gen = WordGenerator(self.sample) + words = itertools.islice(gen, iterations) + return '-'.join(words), self.entropy * iterations + + def _password_by_length(self, length): + """Return password of length ``length``.""" + + words = [] + pw_length = 0 + for word in self.generator: + words.append(word) + pw_length += len(word) + 1 + if pw_length >= length: + break + + 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) + + +if __name__ == '__main__': + # gen = PronounceableGenerator() + # print(gen.password(30)) + words = set() + words_in_words = 0 + i = 1 + path = os.path.join(os.path.dirname(__file__), 'english.txt') + with open(path, 'rb') as fp: + sample = fp.read().decode('utf-8') + gen = WordGenerator(sample) + for word in gen: + if word in words: + words_in_words += 1 + else: + words.add(word) + words_in_words = 0 + if words_in_words == 10: + break + if i % 1000 == 0: + print('%d/%d unique words' % (len(words), i)) + i += 1 + print('%d words' % len(words)) diff --git a/src/generators/pronounceable_test.py b/src/generators/pronounceable_test.py deleted file mode 100644 index 7e1f520..0000000 --- a/src/generators/pronounceable_test.py +++ /dev/null @@ -1,68 +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 -# - -""" -Generate gibberish words. - -http://stackoverflow.com/a/5502875/356942 -""" - -from __future__ import print_function, unicode_literals, absolute_import - -import itertools -import random -import string - -initial_consonants = ( - set(string.ascii_lowercase) - set('aeiou') - # remove those easily confused with others - - set('qxc') - # add some crunchy clusters - | set(['bl', 'br', 'cl', 'cr', 'dr', 'fl', - 'fr', 'gl', 'gr', 'pl', 'pr', 'sk', - 'sl', 'sm', 'sn', 'sp', 'st', 'str', - 'sw', 'tr']) -) - -final_consonants = ( - set(string.ascii_lowercase) - set('aeiou') - # confusable - - set('qxcsj') - # crunchy clusters - | set(['ct', 'ft', 'mp', 'nd', 'ng', 'nk', 'nt', - 'pt', 'sk', 'sp', 'ss', 'st']) -) - -vowels = 'aeiou' - -# each syllable is consonant-vowel-consonant "pronounceable" -syllables = map(''.join, itertools.product(initial_consonants, - vowels, - final_consonants)) - - -def password(length, wordlist=syllables): - words = [] - pw_length = 0 - rand = random.SystemRandom() - while pw_length < length: - word = rand.choice(wordlist) - words.append(word) - pw_length += len(word) + 1 - pw = '-'.join(words) - if len(pw) > length: - pw = pw[:length] - pw.rstrip('-') - - return pw - -if __name__ == '__main__': - print('%d syllables' % len(syllables)) - print(password(30)) diff --git a/src/pwgen.py b/src/pwgen.py index 49ff643..e7007d6 100644 --- a/src/pwgen.py +++ b/src/pwgen.py @@ -338,6 +338,21 @@ def do_conf(self): } ) + # Strength bar + if wf.settings.get('strength_bar'): + icon = 'icons/toggle_on.icns' + else: + icon = 'icons/toggle_off.icns' + options.append( + { + 'title': 'Strength Bar', + 'subtitle': 'Show password strength as a bar, not bits', + 'arg': 'toggle strength_bar', + 'valid': True, + 'icon': icon, + } + ) + # Generators generators = get_generators() @@ -359,7 +374,8 @@ def do_conf(self): ) if query: - options = wf.filter(query, options, key=lambda d: d.get('title')) + options = wf.filter(query, options, key=lambda d: d.get('title'), + min_score=30) if not options: wf.add_item('No matching items', @@ -405,28 +421,42 @@ def do_toggle(self): wf = self.wf args = self.args gen_id = args.get('') - gen = None - for g in get_generators(): - if g.id_ == gen_id: - gen = g - break - if not gen: - log.critical('Unknown generator : %s', gen_id) - return 1 - active_generators = wf.settings.get('generators', []) - if gen_id in active_generators: - log.debug('Turning generator `%s` off...', gen.name) - active_generators.remove(gen_id) - mode = 'off' - else: - log.debug('Turning generator `%s` on...', gen.name) - active_generators.append(gen_id) - mode = 'on' - log.debug('Active generators : %s', active_generators) - wf.settings['generators'] = active_generators + # Strength bar toggle + if gen_id == 'strength_bar': + if wf.settings.get('strength_bar'): + log.debug('Turning strength bar off...') + wf.settings['strength_bar'] = False + mode = 'off' + else: + log.debug('Turning strength bar on...') + wf.settings['strength_bar'] = True + mode = 'on' + print('Turned strength bar {0}'.format(mode)) + + else: # Generator toggles + gen = None + for g in get_generators(): + if g.id_ == gen_id: + gen = g + break + if not gen: + log.critical('Unknown generator : %s', gen_id) + return 1 + + active_generators = wf.settings.get('generators', []) + if gen_id in active_generators: + log.debug('Turning generator `%s` off...', gen.name) + active_generators.remove(gen_id) + mode = 'off' + else: + log.debug('Turning generator `%s` on...', gen.name) + active_generators.append(gen_id) + mode = 'on' + log.debug('Active generators : %s', active_generators) + wf.settings['generators'] = active_generators - print("Turned generator '{0}' {1}".format(gen.name, mode)) + print("Turned generator '{0}' {1}".format(gen.name, mode)) call_alfred_search(KEYWORD_CONF + ' ') return 0 diff --git a/src/version b/src/version index 9f8e9b6..b123147 100644 --- a/src/version +++ b/src/version @@ -1 +1 @@ -1.0 \ No newline at end of file +1.1 \ No newline at end of file