aboutsummaryrefslogtreecommitdiff
path: root/frozen_deps/Crypto/Protocol/AllOrNothing.py
blob: dd20536fca797436d50a10932d8d631ba488e71c (plain) (blame)
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
#
#  AllOrNothing.py : all-or-nothing package transformations
#
# Part of the Python Cryptography Toolkit
#
# Written by Andrew M. Kuchling and others
#
# ===================================================================
# The contents of this file are dedicated to the public domain.  To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# 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.
# ===================================================================

"""This file implements all-or-nothing package transformations.

An all-or-nothing package transformation is one in which some text is
transformed into message blocks, such that all blocks must be obtained before
the reverse transformation can be applied.  Thus, if any blocks are corrupted
or lost, the original message cannot be reproduced.

An all-or-nothing package transformation is not encryption, although a block
cipher algorithm is used.  The encryption key is randomly generated and is
extractable from the message blocks.

This class implements the All-Or-Nothing package transformation algorithm
described in:

Ronald L. Rivest.  "All-Or-Nothing Encryption and The Package Transform"
http://theory.lcs.mit.edu/~rivest/fusion.pdf

"""

__revision__ = "$Id$"

import operator
import sys
from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.Util.py3compat import *
from functools import reduce

def isInt(x):
    test = 0
    try:
        test += x
    except TypeError:
        return 0
    return 1

class AllOrNothing:
    """Class implementing the All-or-Nothing package transform.

    Methods for subclassing:

        _inventkey(key_size):
            Returns a randomly generated key.  Subclasses can use this to
            implement better random key generating algorithms.  The default
            algorithm is probably not very cryptographically secure.

    """

    def __init__(self, ciphermodule, mode=None, IV=None):
        """AllOrNothing(ciphermodule, mode=None, IV=None)

        ciphermodule is a module implementing the cipher algorithm to
        use.  It must provide the PEP272 interface.

        Note that the encryption key is randomly generated
        automatically when needed.  Optional arguments mode and IV are
        passed directly through to the ciphermodule.new() method; they
        are the feedback mode and initialization vector to use.  All
        three arguments must be the same for the object used to create
        the digest, and to undigest'ify the message blocks.
        """

        self.__ciphermodule = ciphermodule
        self.__mode = mode
        self.__IV = IV
        self.__key_size = ciphermodule.key_size
        if not isInt(self.__key_size) or self.__key_size==0:
            self.__key_size = 16

    __K0digit = bchr(0x69)

    def digest(self, text):
        """digest(text:string) : [string]

        Perform the All-or-Nothing package transform on the given
        string.  Output is a list of message blocks describing the
        transformed text, where each block is a string of bit length equal
        to the ciphermodule's block_size.
        """

        # generate a random session key and K0, the key used to encrypt the
        # hash blocks.  Rivest calls this a fixed, publically-known encryption
        # key, but says nothing about the security implications of this key or
        # how to choose it.
        key = self._inventkey(self.__key_size)
        K0 = self.__K0digit * self.__key_size

        # we need two cipher objects here, one that is used to encrypt the
        # message blocks and one that is used to encrypt the hashes.  The
        # former uses the randomly generated key, while the latter uses the
        # well-known key.
        mcipher = self.__newcipher(key)
        hcipher = self.__newcipher(K0)

        # Pad the text so that its length is a multiple of the cipher's
        # block_size.  Pad with trailing spaces, which will be eliminated in
        # the undigest() step.
        block_size = self.__ciphermodule.block_size
        padbytes = block_size - (len(text) % block_size)
        text = text + b(' ') * padbytes

        # Run through the algorithm:
        # s: number of message blocks (size of text / block_size)
        # input sequence: m1, m2, ... ms
        # random key K' (`key' in the code)
        # Compute output sequence: m'1, m'2, ... m's' for s' = s + 1
        # Let m'i = mi ^ E(K', i) for i = 1, 2, 3, ..., s
        # Let m's' = K' ^ h1 ^ h2 ^ ... hs
        # where hi = E(K0, m'i ^ i) for i = 1, 2, ... s
        #
        # The one complication I add is that the last message block is hard
        # coded to the number of padbytes added, so that these can be stripped
        # during the undigest() step
        s = divmod(len(text), block_size)[0]
        blocks = []
        hashes = []
        for i in range(1, s+1):
            start = (i-1) * block_size
            end = start + block_size
            mi = text[start:end]
            assert len(mi) == block_size
            cipherblock = mcipher.encrypt(long_to_bytes(i, block_size))
            mticki = bytes_to_long(mi) ^ bytes_to_long(cipherblock)
            blocks.append(mticki)
            # calculate the hash block for this block
            hi = hcipher.encrypt(long_to_bytes(mticki ^ i, block_size))
            hashes.append(bytes_to_long(hi))

        # Add the padbytes length as a message block
        i = i + 1
        cipherblock = mcipher.encrypt(long_to_bytes(i, block_size))
        mticki = padbytes ^ bytes_to_long(cipherblock)
        blocks.append(mticki)

        # calculate this block's hash
        hi = hcipher.encrypt(long_to_bytes(mticki ^ i, block_size))
        hashes.append(bytes_to_long(hi))

        # Now calculate the last message block of the sequence 1..s'.  This
        # will contain the random session key XOR'd with all the hash blocks,
        # so that for undigest(), once all the hash blocks are calculated, the
        # session key can be trivially extracted.  Calculating all the hash
        # blocks requires that all the message blocks be received, thus the
        # All-or-Nothing algorithm succeeds.
        mtick_stick = bytes_to_long(key) ^ reduce(operator.xor, hashes)
        blocks.append(mtick_stick)

        # we convert the blocks to strings since in Python, byte sequences are
        # always represented as strings.  This is more consistent with the
        # model that encryption and hash algorithms always operate on strings.
        return [long_to_bytes(i,self.__ciphermodule.block_size) for i in blocks]


    def undigest(self, blocks):
        """undigest(blocks : [string]) : string

        Perform the reverse package transformation on a list of message
        blocks.  Note that the ciphermodule used for both transformations
        must be the same.  blocks is a list of strings of bit length
        equal to the ciphermodule's block_size.
        """

        # better have at least 2 blocks, for the padbytes package and the hash
        # block accumulator
        if len(blocks) < 2:
            raise ValueError("List must be at least length 2.")

        # blocks is a list of strings.  We need to deal with them as long
        # integers
        blocks = list(map(bytes_to_long, blocks))

        # Calculate the well-known key, to which the hash blocks are
        # encrypted, and create the hash cipher.
        K0 = self.__K0digit * self.__key_size
        hcipher = self.__newcipher(K0)
        block_size = self.__ciphermodule.block_size

        # Since we have all the blocks (or this method would have been called
        # prematurely), we can calculate all the hash blocks.
        hashes = []
        for i in range(1, len(blocks)):
            mticki = blocks[i-1] ^ i
            hi = hcipher.encrypt(long_to_bytes(mticki, block_size))
            hashes.append(bytes_to_long(hi))

        # now we can calculate K' (key).  remember the last block contains
        # m's' which we don't include here
        key = blocks[-1] ^ reduce(operator.xor, hashes)

        # and now we can create the cipher object
        mcipher = self.__newcipher(long_to_bytes(key, self.__key_size))

        # And we can now decode the original message blocks
        parts = []
        for i in range(1, len(blocks)):
            cipherblock = mcipher.encrypt(long_to_bytes(i, block_size))
            mi = blocks[i-1] ^ bytes_to_long(cipherblock)
            parts.append(mi)

        # The last message block contains the number of pad bytes appended to
        # the original text string, such that its length was an even multiple
        # of the cipher's block_size.  This number should be small enough that
        # the conversion from long integer to integer should never overflow
        padbytes = int(parts[-1])
        text = b('').join(map(long_to_bytes, parts[:-1]))
        return text[:-padbytes]

    def _inventkey(self, key_size):
        # Return key_size random bytes
        from Crypto import Random
        return Random.new().read(key_size)

    def __newcipher(self, key):
        if self.__mode is None and self.__IV is None:
            return self.__ciphermodule.new(key)
        elif self.__IV is None:
            return self.__ciphermodule.new(key, self.__mode)
        else:
            return self.__ciphermodule.new(key, self.__mode, self.__IV)



if __name__ == '__main__':
    import sys
    import getopt
    import base64

    usagemsg = '''\
Test module usage: %(program)s [-c cipher] [-l] [-h]

Where:
    --cipher module
    -c module
        Cipher module to use.  Default: %(ciphermodule)s

    --aslong
    -l
        Print the encoded message blocks as long integers instead of base64
        encoded strings

    --help
    -h
        Print this help message
'''

    ciphermodule = 'AES'
    aslong = 0

    def usage(code, msg=None):
        if msg:
            print(msg)
        print(usagemsg % {'program': sys.argv[0],
                          'ciphermodule': ciphermodule})
        sys.exit(code)

    try:
        opts, args = getopt.getopt(sys.argv[1:],
                                   'c:l', ['cipher=', 'aslong'])
    except getopt.error as msg:
        usage(1, msg)

    if args:
        usage(1, 'Too many arguments')

    for opt, arg in opts:
        if opt in ('-h', '--help'):
            usage(0)
        elif opt in ('-c', '--cipher'):
            ciphermodule = arg
        elif opt in ('-l', '--aslong'):
            aslong = 1

    # ugly hack to force __import__ to give us the end-path module
    module = __import__('Crypto.Cipher.'+ciphermodule, None, None, ['new'])

    x = AllOrNothing(module)
    print('Original text:\n==========')
    print(__doc__)
    print('==========')
    msgblocks = x.digest(b(__doc__))
    print('message blocks:')
    for i, blk in zip(list(range(len(msgblocks))), msgblocks):
        # base64 adds a trailing newline
        print('    %3d' % i, end=' ')
        if aslong:
            print(bytes_to_long(blk))
        else:
            print(base64.encodestring(blk)[:-1])
    #
    # get a new undigest-only object so there's no leakage
    y = AllOrNothing(module)
    text = y.undigest(msgblocks)
    if text == b(__doc__):
        print('They match!')
    else:
        print('They differ!')