Skip to content

Commit

Permalink
Improve README
Browse files Browse the repository at this point in the history
Specify password strength in bits of entropy
  • Loading branch information
deanishe committed Jul 28, 2015
1 parent f8a8416 commit a2f7c19
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 40 deletions.
50 changes: 39 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,62 @@

Generate secure random passwords from Alfred.

Offers multiple generators, including based on real words and pronounceable pseudo-words generated with Markov chains.

Shows the strength of each generated password.
## Features ##

- Passwords can be generated based on strength or length.
- Offers multiple generators, including based on real words and pronounceable pseudo-words generated with Markov chains.
- Shows the strength of each generated password.


## Usage ##

- `pwgen [<length>]` — Generate passwords of length `<length>`. Default length is 30 characters.
- `pwconf` — View and edit workflow settings.
- `pwgen [<strength>]` — Generate passwords of specified strength. Default is `3` (96 bits of entropy). See [Password strength](#password-strength) for details.
- `pwlen [<length>]` — 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.

**Note:** Word-based generators may provide passwords that are slightly longer than `<length>`.


## Password strength ##

Each password has its strength in the result subtitle. This is in bits of entropy.
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.

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 **&gt;45 billion passwords per second.**

The average number of guesses required to crack a password *n* bits is 2<sup>n-1</sup>, 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.

As a rule of thumb, your passwords should have the following strengths for the following applications:
| Level | Min. entropy | Time to guess&nbsp;\* | Application |
| ------: | --------------------: | --------------: | --------------------------- |
| 1 | 32&nbsp;bits | 0.048 seconds | Stuff you want to be super easy to crack? |
| 2 | 64&nbsp;bits | 6.5 years | Web accounts, WiFi passwords |
| 3 | 96&nbsp;bits | 280 million years | Almost anything |
| 4 | 128&nbsp;bits | billions of times the age of the universe | Encryption keys |

| Application | Strength (bits of entropy) |
|-----------------|----------------------------|
| Online password | 30 |
| Encrpytion keys | 128 |
\* = based on 45 billions guesses per second.

The default password length of 30 characters should provide very secure passwords, though you may need to increase this if you're making a new 1Password master key using one of the word-based generators.
The default password strength level of 3 (96 bits) provides very secure passwords.

The default password length of 20 characters provides reasonably to very secure passwords, depending on the generator.


#### 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.

For example, the single-digit password `1` has an entropy of 3.32 bits when generated by the Numeric algorithm because it is one of only 10 possible one-digit passwords. The same password generated by the ASCII algorithm has an entropy of 5.95 bits because it is one of 62 possible one-character passwords.

See [Password strength on Wikipedia](https://en.wikipedia.org/wiki/Password_strength#Random_passwords) for more information.


## Configuration ##

Not implemented yet.
5 changes: 2 additions & 3 deletions src/generators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import math
import os

ENTROPY_PER_LEVEL = 30
ENTROPY_PER_LEVEL = 32

# string.punctuation contains a few characters we don't want
# like backslash and tilde
Expand All @@ -43,8 +43,7 @@ def password(self, strength=None, length=None):
"""

if strength is not None:
target_entropy = strength * ENTROPY_PER_LEVEL
length = int(math.ceil(target_entropy / self.entropy))
length = int(math.ceil(strength / self.entropy))

chars = self.data
pw = [chars[ord(c) % len(chars)] for c in os.urandom(length)]
Expand Down
3 changes: 1 addition & 2 deletions src/generators/gen_pronounceable.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,7 @@ def password(self, strength=None, length=None):
"""

if strength is not None:
target_entropy = strength * ENTROPY_PER_LEVEL
iterations = int(math.ceil(target_entropy / self.entropy))
iterations = int(math.ceil(strength / self.entropy))
return self._password_by_iterations(iterations)

else:
Expand Down
5 changes: 2 additions & 3 deletions src/generators/gen_realwords.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import math
import random

from generators.base import PassGenBase, ENTROPY_PER_LEVEL
from generators.base import PassGenBase


class WordlistGenerator(PassGenBase):
Expand Down Expand Up @@ -84,8 +84,7 @@ def password(self, strength=None, length=None):
"""

if strength is not None:
target_entropy = strength * ENTROPY_PER_LEVEL
iterations = int(math.ceil(target_entropy / self.entropy))
iterations = int(math.ceil(strength / self.entropy))
return self._password_by_iterations(iterations)

else:
Expand Down
4 changes: 2 additions & 2 deletions src/info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
<key>queuedelaymode</key>
<integer>0</integer>
<key>queuemode</key>
<integer>1</integer>
<integer>2</integer>
<key>runningsubtext</key>
<string>Crunching random bits…</string>
<key>script</key>
Expand Down Expand Up @@ -170,7 +170,7 @@
<key>queuedelaymode</key>
<integer>0</integer>
<key>queuemode</key>
<integer>1</integer>
<integer>2</integer>
<key>runningsubtext</key>
<string>Crunching random bits…</string>
<key>script</key>
Expand Down
64 changes: 45 additions & 19 deletions src/pwgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
# Created on 2015-07-27
#

"""pwgen.py command [options]
"""pwgen.py <command> [options]
Generate secure passwords.
Usage:
pwgen.py generate [-v|-q|-d] [<strength>]
pwgen.py generate [-v|-q|-d] --length [<length>]
pwgen.py conf [-v|-q|-d] [<query>]
pwgen.py conf [-v|-q|-d]
pwgen.py set [-v|-q|-d] <key> <value>
pwget.py reset [-v|-q|-d] <key>
pwgen.py (-h|--version)
Options:
Expand Down Expand Up @@ -84,6 +88,26 @@ def pw_strength_meter(entropy):
return bar


def entropy_from_strength(strength):
"""Return bits of entropy for ``strength``.
If ``strength`` ends in 'b', treat as bits, else
treat as level and multiply by ``ENTROPY_PER_LEVEL``.
"""

if not isinstance(strength, basestring):
strength = str(strength)
strength = strength.strip()
if not strength:
return None

if strength.endswith('b'):
return int(strength[:-1])

return int(strength) * ENTROPY_PER_LEVEL


class PasswordApp(object):
"""Workflow application"""

Expand Down Expand Up @@ -123,6 +147,7 @@ def do_generate(self):
"""Generate and display passwords from active generators."""
wf = self.wf
args = self.args
query = ''
mode = 'strength'
pw_length = None
pw_strength = None
Expand All @@ -132,6 +157,7 @@ def do_generate(self):
mode = 'length'
pw_length = args.get('<length>') or ''
pw_length = pw_length.strip()
query = pw_length

if pw_length:
if not pw_length.isdigit():
Expand All @@ -150,22 +176,22 @@ def do_generate(self):

else: # Default strength mode
pw_strength = args.get('<strength>') or ''
pw_strength = pw_strength.strip()

if pw_strength:
if not pw_strength.isdigit():
wf.add_item('`{0}` is not a number'.format(pw_strength),
'Usage: pwgen [length]',
icon=ICON_WARNING)
wf.send_feedback()
return 0

pw_strength = int(pw_strength)

pw_strength = pw_strength or wf.settings.get('pw_strength',
DEFAULT_PW_STRENGTH)

log.info('Password length: %d', pw_length)
query = pw_strength
try:
pw_strength = entropy_from_strength(pw_strength)
except ValueError:
wf.add_item('`{0}` is not a number'.format(pw_strength),
'Usage: pwgen [length]',
icon=ICON_WARNING)
wf.send_feedback()
return 0

pw_strength = (
pw_strength or
entropy_from_strength(wf.settings.get('pw_strength',
DEFAULT_PW_STRENGTH)))

log.info('Password strength: %d bits', pw_strength)

generators = get_generators()

Expand Down Expand Up @@ -204,7 +230,7 @@ def do_generate(self):
wf.add_item(pw,
subtitle,
arg=pw, uid=g.id_,
autocomplete='{0}'.format(pw_length),
autocomplete=query,
valid=True,
copytext=pw,
largetext=pw)
Expand Down
52 changes: 52 additions & 0 deletions time_to_guess.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python
# encoding: utf-8
#
# Copyright © 2015 [email protected]
#
# MIT Licence. See https://opensource.org/licenses/MIT
#
# Created on 2015-07-28
#

"""
"""

from __future__ import print_function, unicode_literals, absolute_import

import os
import sys

# Guesses/second
gps = 45000000000

bits = 32


def human_time(seconds):
if seconds < 60:
return '%0.3f seconds' % seconds
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
weeks, days = divmod(days, 7)
years, weeks = divmod(weeks, 52)
centuries, years = divmod(years, 100)
millenia, centuries = divmod(centuries, 10)
universe_ages, millenia = divmod(millenia, 14000000)
if universe_ages:
return '%d ages of the universe' % universe_ages
if millenia:
return '%d millenia, %d centuries' % (millenia, centuries)
if centuries:
return '%d centuries, %d years' % (centuries, years)
if years:
return '%d years, %d weeks' % (years, weeks)
elif weeks:
return '%d weeks, %d days' % (weeks, days)

while bits <= 256:
# guesses = int('0b' + '1' * (bits - 1), 2)
guesses = 2 ** (bits - 1)
time = float(guesses) / gps
print('%3d bits : %s' % (bits, human_time(time)))
bits += 32

0 comments on commit a2f7c19

Please sign in to comment.