File: passphrase.py

package info (click to toggle)
python-pyutil 3.3.2-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 884 kB
  • sloc: python: 7,198; makefile: 6
file content (77 lines) | stat: -rw-r--r-- 2,957 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/usr/bin/env python
# -*- coding: utf-8-with-signature-unix; fill-column: 77 -*-
# -*- indent-tabs-mode: nil -*-

#  This file is part of pyutil; see README.rst for licensing terms.

import argparse, math, random, sys

from pyutil.mathutil import div_ceil

from pkg_resources import resource_stream

def recursive_subset_sum(entropy_needed, wordlists):
    # Pick a minimalish set of numbers which sum to at least
    # entropy_needed.

    # Okay now what's the smallest number of words which will give us
    # at least this much entropy?
    entropy_of_biggest_wordlist = wordlists[-1][0]
    assert isinstance(entropy_of_biggest_wordlist, float), wordlists[-1]
    needed_words = div_ceil(entropy_needed, entropy_of_biggest_wordlist)
    # How much entropy do we need from each word?
    needed_entropy_per_word = entropy_needed / needed_words
    # What's the smallest wordlist that offers at least this much
    # entropy per word?
    for (wlentropy, wl) in wordlists:
        if wlentropy >= needed_entropy_per_word:
            break
    assert wlentropy >= needed_entropy_per_word, (wlentropy, needed_entropy_per_word)

    result = [(wlentropy, wl)]
    # If we need more, recurse...
    if wlentropy < entropy_needed:
        rest = recursive_subset_sum(entropy_needed - wlentropy, wordlists)
        result.extend(rest)
    return result

def gen_passphrase(entropy, allwords):
    maxlenwords = []
    i = 2 # The smallest set is words of length 1 or 2.
    words = [x for x in allwords if len(x) <= i]
    maxlenwords.append((math.log(len(words), 2), words))
    while len(maxlenwords[-1][1]) < len(allwords):
        i += 1
        words = [x for x in allwords if len(x) <= i]
        maxlenwords.append((math.log(len(words), 2), words))

    sr = random.SystemRandom()
    passphrase = []

    wordlists_to_use = recursive_subset_sum(entropy, maxlenwords)

    passphraseentropy = 0.0
    for (wle, wl) in wordlists_to_use:
        passphrase.append(sr.choice(wl))
        passphraseentropy += wle
            
    return (u".".join(passphrase), passphraseentropy)

def main():
    parser = argparse.ArgumentParser(prog="passphrase", description="Create a random passphrase by picking a few random words.")

    parser.add_argument('-d', '--dictionary', help="what file to read a list of words from (or omit this option to use passphrase's bundled dictionary)", type=argparse.FileType('rU'), metavar="DICT")
    parser.add_argument('bits', help="how many bits of entropy minimum", type=float, metavar="BITS")
    args = parser.parse_args()

    dicti = args.dictionary
    if not dicti:
        dicti = resource_stream('pyutil', 'data/wordlist.txt')
    allwords = set([x.decode('utf-8').strip().lower() for x in dicti.readlines()])

    passphrase, bits = gen_passphrase(args.bits, allwords)

    sys.stdout.write(passphrase)
    sys.stdout.write('\n')

    sys.stderr.write(u"This passphrase encodes about {:.0f} bits.\n".format(bits))