~gagbo/diceware.py

ref: 91f221a218c6e70f10c0322913d1741485cbcc83 diceware.py/dice_rolls/diceware_result.py -rw-r--r-- 4.9 KiB
91f221a2 — Gerry Agbobada Add Ack for bundled list, lint README 2 years ago
                                                                                
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/usr/bin/env python3
# coding: utf-8
""" diceware_result : Utilities to generate dice rolls for diceware method.
    The count of rolls per word (currently 5 die per word), is hardcoded in
a few places and behaviour with non 5-throw lists (from '11111' to '66666'
with only 1-6 inclusive digits) is undefined.
"""
import random


class DicewareResult:
    """ DicewareResult contains all the data from the diceware protocol
        in number + bonus_separator form, so all this object can be used to
        check up the words database.
    """

    def __init__(self, wordsCount=5, systemRand=True, bonusRoll=True):
        """ Constructor of a Diceware Result
        wordsCount is the number of words in the passphrase
        systemRand asks to use system true random instead of pseudo-random
        bonusRoll asks for one more roll to salt the passphrase with a symbol
        """
        self.wordsCount = wordsCount
        self.systemRand = systemRand
        self.bonusRoll = bonusRoll

    def make_rolls(self):
        """ make_rolls fills self.rolls with dice throws
        These 'rolls' which are really 5-uples are the basis for generating
        the true passphrase using a dictionary
        """
        self.rolls = self.generate_rolls()
        self.ensure_random_generator()

        if self.bonusRoll:
            self.salt = roll_5_dice(self.random_generator)
            while self.salt[0] > self.wordsCount:
                self.salt = roll_5_dice(self.random_generator)
        else:
            self.salt = None

    def generate_rolls(self):
        """ generate_rolls generates a rolls-uple of 5-uple of [1,6] integers
        """
        self.ensure_random_generator()

        result = []
        for i in range(self.wordsCount):
            result.append(roll_5_dice(self.random_generator))
        return tuple(result)

    def ensure_random_generator(self):
        """ Create a random generator attribute if not created yet."""
        if hasattr(self, "random_generator"):
            return

        if self.systemRand:
            self.random_generator = random.SystemRandom()
        else:
            self.random_generator = random.Random()

    def __str__(self):
        """ Method called for str(self) and print(self)
        Useful for debugging purposes
        """
        string = "Diceware Result : {} words with {} generator\n".format(
            self.wordsCount, "system" if self.systemRand else "pseudo")
        for i in range(self.wordsCount):
            string += "Word {} : {}\n".format(i + 1, self.rolls[i])

        if self.bonusRoll:
            string += "Salt : {}\n".format(self.salt)

        return string[:-1]

    def key_from_word(self, i):
        """ Transforms self.rolls[i] in an integer.
        It should be used to obtain the key for
        the words dictionary (Diceware list)
        """
        roll = self.rolls[i]
        result = (roll[0] * 10000 + roll[1] * 1000 +
                  roll[2] * 100 + roll[3] * 10 + roll[4])
        return result

    def password_from_dict(self, diceware_dict):
        """ Returns the password as a string
        The string is generated with this instance and diceware_dict param
        diceware_dict must be a dictionary indexed with integers representing
           the rolls concatenated
        """
        result = ''
        # Prepare salting the correct word if relevant
        if self.bonusRoll:
            target_word_index = self.salt[0] - 1

        for i in range(self.wordsCount):
            new_word = list(diceware_dict[self.key_from_word(i)])
            # Salting
            if self.bonusRoll and i == target_word_index:
                target_char = min(self.salt[1], len(new_word))
                replace_char = get_salt_char(self.salt[2], self.salt[3])
                new_word[target_char - 1] = replace_char

            result += "".join(new_word)
            result += ' '
        result = result[:-1]
        return result


def roll_5_dice(gen):
    """ roll_5_dice generates a 5-uple of integers between 1 and 6 inclusive
    """
    return (gen.randint(1, 6),
            gen.randint(1, 6),
            gen.randint(1, 6),
            gen.randint(1, 6),
            gen.randint(1, 6))


def get_salt_char(digitThree=0, digitFour=0):
    """ get_salt_char returns a char to use as additional char from salt
    It takes 2 digits in the range [1,6] as parameters digitThree digitFour.
    If these parameters are not given, then space is returned
    """
    if digitThree < 1 or digitFour < 1 or digitThree > 6 or digitFour > 6:
        return ' '

    symbol_table = (('~', '!', '#', '$', '%', '^'),
                    ('&', '*', '(', ')', '-', '='),
                    ('+', '[', ']', '\\', '{', '}'),
                    (':', ';', '"', '\'', '<', '>'),
                    ('?', '/', '0', '1', '2', '3'),
                    ('4', '5', '6', '7', '8', '9'))

    return symbol_table[digitFour - 1][digitThree - 1]