123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- #
- # 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 *
- 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 = 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, 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(range(len(msgblocks)), msgblocks):
- # base64 adds a trailing newline
- print ' %3d' % i,
- 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!'
|