Skip to content

Commit

Permalink
Added BlockFeeder and documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed May 13, 2014
1 parent bade3ab commit e68d52e
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 1 deletion.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ ciphertext = aes.encrypt(plaintext)
# 'L6\x95\x85\xe4\xd9\xf1\x8a\xfb\xe5\x94X\x80|\x19\xc3'
print repr(ciphertext)


# Since there is no state stored in this mode of operation, it
# is not necessary to create a new aes object for decryption.
#aes = pyaes.AESModeOfOperationECB(key)
Expand All @@ -159,6 +158,40 @@ print decrypted == plaintext
```


### BlockFeeder

Since most of the modes of operations require data in specific block-sized or segment-sized blocks, it can be difficult when working with large arbitrary streams or strings of data.

The BlockFeeder class is meant to make life easier for you, by buffering bytes across multiple calls and returning bytes as they are available, as well as padding or stripping the output when finished, if necessary.

```python
import pyaes

# Any mode of operation can be used; for this example CBC
key = "This_key_for_demo_purposes_only!"
iv = "InitializationVe"

ciphertext = ''

# We can encrypt one line at a time, regardles of length
encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv))
for line in file('/etc/passwd'):
ciphertext += encrypter.feed(line)

# Make a final call to flush any remaining bytes and add paddin
ciphertext += encrypter.feed()

# We can decrypt the cipher text in chunks (here we split it in half)
decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv))
decrypted = decrypter.feed(ciphertext[:len(ciphertext) / 2])
decrypted += decrypter.feed(ciphertext[len(ciphertext) / 2:])

# Again, make a final call to flush any remaining bytes and strip padding
decrypted += decrypter.feed()

print file('/etc/passwd').read() == decrypted
```

### AES block cipher

Generally you should use one of the modes of operation above. This may however be useful for experimenting with a custom mode of operation or dealing with encrypted blocks.
Expand Down
1 change: 1 addition & 0 deletions pyaes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@
VERSION = [1, 0, 0]

from pyaes.aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter
from pyaes.blockfeeder import Decrypter, Encrypter
151 changes: 151 additions & 0 deletions pyaes/blockfeeder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# The MIT License (MIT)
#
# Copyright (c) 2014 Richard Moore
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# 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.


from pyaes.aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation
from pyaes.util import append_PKCS7_padding, strip_PKCS7_padding

# First we add three functions to each of the modes of operations
#
# _block_an_consume(size)
# - Given a size determine how many bytes could be consumed in
# a single call to either the decrypt or encrypt method
#
# _block_final_encrypt(data)
# - call and return encrypt on this (last) chunk of data,
# padding as necessary; this will always be at least 16
# bytes unless the total incoming input was less than 16
# bytes
#
# _block_final_decrypt(data)
# - same as _blcok_final_encrypt except for decrypt, for
# stripping off padding
#


# ECB and CBC are block-only ciphers

def _block_can_consume(self, size):
if size >= 16: return 16
return 0

def _block_final_encrypt(self, data):
data = append_PKCS7_padding(data)
if len(data) == 32:
return self.encrypt(data[:16]) + self.encrypt(data[16:])
return self.encrypt(data)

def _block_final_decrypt(self, data):
return strip_PKCS7_padding(self.decrypt(data))

AESBlockModeOfOperation._can_consume = _block_can_consume
AESBlockModeOfOperation._final_encrypt = _block_final_encrypt
AESBlockModeOfOperation._final_decrypt = _block_final_decrypt



# CFB is a segment cipher

def _segment_can_consume(self, size):
return self.segment_bytes * int(size // self.segment_bytes)

# CFB can handle a non-segment-sized block at the end using the remaining cipherblock
def _segment_final_encrypt(self, data):
padded = data + (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes)))
return self.encrypt(padded)[:len(data)]

# CFB can handle a non-segment-sized block at the end using the remaining cipherblock
def _segment_final_decrypt(self, data):
padded = data + (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes)))
return self.decrypt(padded)[:len(data)]

AESSegmentModeOfOperation._can_consume = _segment_can_consume
AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt
AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt



# OFB and CTR are stream ciphers

def _stream_can_consume(self, size):
return size

def _stream_final_encrypt(self, data):
return self.encrypt(data)

def _stream_final_decrypt(self, data):
return self.decrypt(data)

AESStreamModeOfOperation._can_consume = _stream_can_consume
AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt
AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt



class BlockFeeder(object):
'''The super-class for objects to handle chunking a stream of bytes
into the appropriate block size for the underlying mode of operation
and applying (or stripping) padding, as necessary.'''

def __init__(self, mode, feed, final):
self._mode = mode
self._feed = feed
self._final = final
self._buffer = ""

def feed(self, data = None):
'''Provide bytes to encrypt (or decrypt), returning any bytes
possible from this or any previous calls to feed.
Call with None or an empty string to flush the mode of
operation and return any final bytes; no further calls to
feed may be made.'''

if self._buffer is None:
raise ValueError('already finished feeder')

if not data:
result = self._final(self._buffer)
self._buffer = None
return result

self._buffer += data

result = ''
while len(self._buffer) > 16:
can_consume = self._mode._can_consume(len(self._buffer) - 16)
if can_consume == 0: break
result += self._feed(self._buffer[:can_consume])
self._buffer = self._buffer[can_consume:]

return result


class Encrypter(BlockFeeder):
def __init__(self, mode):
BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt)


class Decrypter(BlockFeeder):
def __init__(self, mode):
BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt)

43 changes: 43 additions & 0 deletions pyaes/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# The MIT License (MIT)
#
# Copyright (c) 2014 Richard Moore
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# 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.


def append_PKCS7_padding(data):
pad = 16 - (len(data) % 16)
return data + pad * chr(pad)

def strip_PKCS7_padding(data):
if len(data) % 16 != 0:
raise ValueError("invalid length")

pad = ord(data[-1])

if pad > 16:
raise ValueError("invalid padding byte")

return data[:-pad]

if __name__ == '__main__':
for i in xrange(0, 17):
data = 'A' * i
padded = append_PKCS7_padding(data)
print repr(padded), strip_PKCS7_padding(padded) == data

0 comments on commit e68d52e

Please sign in to comment.