AllOrNothing.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. #
  2. # AllOrNothing.py : all-or-nothing package transformations
  3. #
  4. # Part of the Python Cryptography Toolkit
  5. #
  6. # Written by Andrew M. Kuchling and others
  7. #
  8. # ===================================================================
  9. # The contents of this file are dedicated to the public domain. To
  10. # the extent that dedication to the public domain is not available,
  11. # everyone is granted a worldwide, perpetual, royalty-free,
  12. # non-exclusive license to exercise all rights associated with the
  13. # contents of this file for any purpose whatsoever.
  14. # No rights are reserved.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
  20. # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
  21. # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  22. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. # SOFTWARE.
  24. # ===================================================================
  25. """This file implements all-or-nothing package transformations.
  26. An all-or-nothing package transformation is one in which some text is
  27. transformed into message blocks, such that all blocks must be obtained before
  28. the reverse transformation can be applied. Thus, if any blocks are corrupted
  29. or lost, the original message cannot be reproduced.
  30. An all-or-nothing package transformation is not encryption, although a block
  31. cipher algorithm is used. The encryption key is randomly generated and is
  32. extractable from the message blocks.
  33. This class implements the All-Or-Nothing package transformation algorithm
  34. described in:
  35. Ronald L. Rivest. "All-Or-Nothing Encryption and The Package Transform"
  36. http://theory.lcs.mit.edu/~rivest/fusion.pdf
  37. """
  38. __revision__ = "$Id$"
  39. import operator
  40. import sys
  41. from Crypto.Util.number import bytes_to_long, long_to_bytes
  42. from Crypto.Util.py3compat import *
  43. def isInt(x):
  44. test = 0
  45. try:
  46. test += x
  47. except TypeError:
  48. return 0
  49. return 1
  50. class AllOrNothing:
  51. """Class implementing the All-or-Nothing package transform.
  52. Methods for subclassing:
  53. _inventkey(key_size):
  54. Returns a randomly generated key. Subclasses can use this to
  55. implement better random key generating algorithms. The default
  56. algorithm is probably not very cryptographically secure.
  57. """
  58. def __init__(self, ciphermodule, mode=None, IV=None):
  59. """AllOrNothing(ciphermodule, mode=None, IV=None)
  60. ciphermodule is a module implementing the cipher algorithm to
  61. use. It must provide the PEP272 interface.
  62. Note that the encryption key is randomly generated
  63. automatically when needed. Optional arguments mode and IV are
  64. passed directly through to the ciphermodule.new() method; they
  65. are the feedback mode and initialization vector to use. All
  66. three arguments must be the same for the object used to create
  67. the digest, and to undigest'ify the message blocks.
  68. """
  69. self.__ciphermodule = ciphermodule
  70. self.__mode = mode
  71. self.__IV = IV
  72. self.__key_size = ciphermodule.key_size
  73. if not isInt(self.__key_size) or self.__key_size==0:
  74. self.__key_size = 16
  75. __K0digit = bchr(0x69)
  76. def digest(self, text):
  77. """digest(text:string) : [string]
  78. Perform the All-or-Nothing package transform on the given
  79. string. Output is a list of message blocks describing the
  80. transformed text, where each block is a string of bit length equal
  81. to the ciphermodule's block_size.
  82. """
  83. # generate a random session key and K0, the key used to encrypt the
  84. # hash blocks. Rivest calls this a fixed, publically-known encryption
  85. # key, but says nothing about the security implications of this key or
  86. # how to choose it.
  87. key = self._inventkey(self.__key_size)
  88. K0 = self.__K0digit * self.__key_size
  89. # we need two cipher objects here, one that is used to encrypt the
  90. # message blocks and one that is used to encrypt the hashes. The
  91. # former uses the randomly generated key, while the latter uses the
  92. # well-known key.
  93. mcipher = self.__newcipher(key)
  94. hcipher = self.__newcipher(K0)
  95. # Pad the text so that its length is a multiple of the cipher's
  96. # block_size. Pad with trailing spaces, which will be eliminated in
  97. # the undigest() step.
  98. block_size = self.__ciphermodule.block_size
  99. padbytes = block_size - (len(text) % block_size)
  100. text = text + b(' ') * padbytes
  101. # Run through the algorithm:
  102. # s: number of message blocks (size of text / block_size)
  103. # input sequence: m1, m2, ... ms
  104. # random key K' (`key' in the code)
  105. # Compute output sequence: m'1, m'2, ... m's' for s' = s + 1
  106. # Let m'i = mi ^ E(K', i) for i = 1, 2, 3, ..., s
  107. # Let m's' = K' ^ h1 ^ h2 ^ ... hs
  108. # where hi = E(K0, m'i ^ i) for i = 1, 2, ... s
  109. #
  110. # The one complication I add is that the last message block is hard
  111. # coded to the number of padbytes added, so that these can be stripped
  112. # during the undigest() step
  113. s = divmod(len(text), block_size)[0]
  114. blocks = []
  115. hashes = []
  116. for i in range(1, s+1):
  117. start = (i-1) * block_size
  118. end = start + block_size
  119. mi = text[start:end]
  120. assert len(mi) == block_size
  121. cipherblock = mcipher.encrypt(long_to_bytes(i, block_size))
  122. mticki = bytes_to_long(mi) ^ bytes_to_long(cipherblock)
  123. blocks.append(mticki)
  124. # calculate the hash block for this block
  125. hi = hcipher.encrypt(long_to_bytes(mticki ^ i, block_size))
  126. hashes.append(bytes_to_long(hi))
  127. # Add the padbytes length as a message block
  128. i = i + 1
  129. cipherblock = mcipher.encrypt(long_to_bytes(i, block_size))
  130. mticki = padbytes ^ bytes_to_long(cipherblock)
  131. blocks.append(mticki)
  132. # calculate this block's hash
  133. hi = hcipher.encrypt(long_to_bytes(mticki ^ i, block_size))
  134. hashes.append(bytes_to_long(hi))
  135. # Now calculate the last message block of the sequence 1..s'. This
  136. # will contain the random session key XOR'd with all the hash blocks,
  137. # so that for undigest(), once all the hash blocks are calculated, the
  138. # session key can be trivially extracted. Calculating all the hash
  139. # blocks requires that all the message blocks be received, thus the
  140. # All-or-Nothing algorithm succeeds.
  141. mtick_stick = bytes_to_long(key) ^ reduce(operator.xor, hashes)
  142. blocks.append(mtick_stick)
  143. # we convert the blocks to strings since in Python, byte sequences are
  144. # always represented as strings. This is more consistent with the
  145. # model that encryption and hash algorithms always operate on strings.
  146. return [long_to_bytes(i,self.__ciphermodule.block_size) for i in blocks]
  147. def undigest(self, blocks):
  148. """undigest(blocks : [string]) : string
  149. Perform the reverse package transformation on a list of message
  150. blocks. Note that the ciphermodule used for both transformations
  151. must be the same. blocks is a list of strings of bit length
  152. equal to the ciphermodule's block_size.
  153. """
  154. # better have at least 2 blocks, for the padbytes package and the hash
  155. # block accumulator
  156. if len(blocks) < 2:
  157. raise ValueError, "List must be at least length 2."
  158. # blocks is a list of strings. We need to deal with them as long
  159. # integers
  160. blocks = map(bytes_to_long, blocks)
  161. # Calculate the well-known key, to which the hash blocks are
  162. # encrypted, and create the hash cipher.
  163. K0 = self.__K0digit * self.__key_size
  164. hcipher = self.__newcipher(K0)
  165. block_size = self.__ciphermodule.block_size
  166. # Since we have all the blocks (or this method would have been called
  167. # prematurely), we can calculate all the hash blocks.
  168. hashes = []
  169. for i in range(1, len(blocks)):
  170. mticki = blocks[i-1] ^ i
  171. hi = hcipher.encrypt(long_to_bytes(mticki, block_size))
  172. hashes.append(bytes_to_long(hi))
  173. # now we can calculate K' (key). remember the last block contains
  174. # m's' which we don't include here
  175. key = blocks[-1] ^ reduce(operator.xor, hashes)
  176. # and now we can create the cipher object
  177. mcipher = self.__newcipher(long_to_bytes(key, self.__key_size))
  178. # And we can now decode the original message blocks
  179. parts = []
  180. for i in range(1, len(blocks)):
  181. cipherblock = mcipher.encrypt(long_to_bytes(i, block_size))
  182. mi = blocks[i-1] ^ bytes_to_long(cipherblock)
  183. parts.append(mi)
  184. # The last message block contains the number of pad bytes appended to
  185. # the original text string, such that its length was an even multiple
  186. # of the cipher's block_size. This number should be small enough that
  187. # the conversion from long integer to integer should never overflow
  188. padbytes = int(parts[-1])
  189. text = b('').join(map(long_to_bytes, parts[:-1]))
  190. return text[:-padbytes]
  191. def _inventkey(self, key_size):
  192. # Return key_size random bytes
  193. from Crypto import Random
  194. return Random.new().read(key_size)
  195. def __newcipher(self, key):
  196. if self.__mode is None and self.__IV is None:
  197. return self.__ciphermodule.new(key)
  198. elif self.__IV is None:
  199. return self.__ciphermodule.new(key, self.__mode)
  200. else:
  201. return self.__ciphermodule.new(key, self.__mode, self.__IV)
  202. if __name__ == '__main__':
  203. import sys
  204. import getopt
  205. import base64
  206. usagemsg = '''\
  207. Test module usage: %(program)s [-c cipher] [-l] [-h]
  208. Where:
  209. --cipher module
  210. -c module
  211. Cipher module to use. Default: %(ciphermodule)s
  212. --aslong
  213. -l
  214. Print the encoded message blocks as long integers instead of base64
  215. encoded strings
  216. --help
  217. -h
  218. Print this help message
  219. '''
  220. ciphermodule = 'AES'
  221. aslong = 0
  222. def usage(code, msg=None):
  223. if msg:
  224. print msg
  225. print usagemsg % {'program': sys.argv[0],
  226. 'ciphermodule': ciphermodule}
  227. sys.exit(code)
  228. try:
  229. opts, args = getopt.getopt(sys.argv[1:],
  230. 'c:l', ['cipher=', 'aslong'])
  231. except getopt.error, msg:
  232. usage(1, msg)
  233. if args:
  234. usage(1, 'Too many arguments')
  235. for opt, arg in opts:
  236. if opt in ('-h', '--help'):
  237. usage(0)
  238. elif opt in ('-c', '--cipher'):
  239. ciphermodule = arg
  240. elif opt in ('-l', '--aslong'):
  241. aslong = 1
  242. # ugly hack to force __import__ to give us the end-path module
  243. module = __import__('Crypto.Cipher.'+ciphermodule, None, None, ['new'])
  244. x = AllOrNothing(module)
  245. print 'Original text:\n=========='
  246. print __doc__
  247. print '=========='
  248. msgblocks = x.digest(b(__doc__))
  249. print 'message blocks:'
  250. for i, blk in zip(range(len(msgblocks)), msgblocks):
  251. # base64 adds a trailing newline
  252. print ' %3d' % i,
  253. if aslong:
  254. print bytes_to_long(blk)
  255. else:
  256. print base64.encodestring(blk)[:-1]
  257. #
  258. # get a new undigest-only object so there's no leakage
  259. y = AllOrNothing(module)
  260. text = y.undigest(msgblocks)
  261. if text == b(__doc__):
  262. print 'They match!'
  263. else:
  264. print 'They differ!'