A symmetric block cipher such as AES (or Triple DES) operates on blocks of fixed size (128 bits for AES and 64 bits for TDES). It is possible, however, to convert a block cipher into a stream cipher using one of the three following modes: cipher feedback (CFB), output feedback (OFB), and counter (CTR). A stream cipher eliminates the need to pad a message to be an integral number of blocks and, for this reason, can operate in real time, making it the natural choice for encrypting streaming data (e.g. voice).
The Python code shown below implements the encryption and decryption operations for CFB-8 and CFB-128 modes. These functions rely on the “basic” AES mode (ECB) services provided by sundAES, an AES implementation in Python presented in a previous blog.
#!/usr/bin/env python3
#
# Author: Joao H de A Franco (jhafranco@gmail.com)
#
# Description: Implementation of AES_CFB8 and AES_CFB128
# implementation modes in Python
#
# Date: 2013-06-05
#
# Repository: https://github.com/jhafranco/Crypto
#
# License: Attribution-NonCommercial-ShareAlike 3.0 Unported
# (CC BY-NC-SA 3.0)
#===========================================================
import sys
from functools import reduce
import AES
# Auxiliary functions
def xor(x,y):
"""Returns the xor between two lists"""
return bytes(i^j for i,j in zip(x,y))
def bytesToInt(b):
"""Converts a bytes string into an integer"""
return listToInt(list(b))
def listToInt(lst):
"""Convert a byte list into a number"""
return reduce(lambda x,y:(x<>= 8
return lst[::-1]
def intToBytes(number):
"""Converts an integer into a bytes list"""
return bytes(intToList(number))
def intToList2(number,length=None):
"""Converts an integer into an integer list with
16, 24 or 32 elements"""
lst = []
while number:
lst.append(number&0xff)
number >>= 8
L = len(lst)
if length:
pZero = length-L
assert pZero >= 0
else:
if L <= 16:
pZero = 16-L
elif L <= 24:
pZero = 24-L
elif L <= 32:
pZero = 32-L
else:
raise ValueError
return list(bytes(pZero)) + lst[::-1]
def intToBytes2(number,length=None):
"""Converts an integer into a bytes list with
16, 24 or 32 elements"""
return bytes(intToList2(number,length))
# Real crypto stuff starts here...
def encryptCFB8(keysize,key,iv,input):
"""Encrypts single bytes of input block (CFB8 mode)"""
inputBuffer = intToBytes2(iv)
if type(input) is int:
ptext = intToBytes(input)
else:
ptext = bytes(map(ord,input))
obj = AES.AES("MODE_ECB")
obj.setKey(keysize,key,iv)
ctext = bytes()
for i in range(0,len(ptext),1):
AESoutput = obj.encrypt(inputBuffer)
cbyte = bytes([AESoutput[0] ^ ptext[i]])
inputBuffer = inputBuffer[1:] + cbyte
ctext += cbyte
if type(input) is int:
ctext = bytesToInt(ctext)
else:
ctext = list(ctext)
return ctext
def decryptCFB8(keysize,key,iv,input):
"""Decrypts single bytes of input block (CFB8 mode)"""
inputBuffer = intToBytes2(iv)
if type(input) is int:
ctext = intToBytes(input)
else:
ctext = bytes(map(ord,input))
obj = AES.AES("MODE_ECB")
obj.setKey(keysize,key,iv)
ptext = bytes()
for i in range(0,len(ctext),1):
AESoutput = obj.encrypt(inputBuffer)
pbyte = bytes([AESoutput[0] ^ ctext[i]])
inputBuffer = inputBuffer[1:] + bytes(ctext[i])
ptext += pbyte
if type(input) is int:
ptext = bytesToInt(ptext)
else:
ptext = "".join(chr(e) for e in ptext)
return ptext
# ... and goes on here
def encryptCFB128(keysize,key,iv,input):
"""Encrypts a 16-byte input block (CFB128 mode)"""
inputBuffer = intToBytes2(iv)
if type(input) is int:
ptext = intToList2(input)
else:
ptext = list(map(ord,input))
L = 16-len(ptext)
ptext = list(bytes(L)) + ptext
obj = AES.AES("MODE_ECB")
obj.setKey(keysize,key,iv)
ctext = bytes()
L3 = len(ptext)
for i in range(0,L3,16):
AESoutput = obj.encrypt(inputBuffer)
inputBuffer = xor(AESoutput,ptext[i:min(L3,i+16)])
ctext += bytes(inputBuffer)
if type(input) is int:
ctext = bytesToInt(ctext)
else:
ctext = list(ctext)
return ctext
def decryptCFB128(keysize,key,iv,input):
"""Decrypts a 16-byte input block (CFB128 mode)"""
inputBuffer = intToBytes2(iv)
if type(input) is int:
ctext = intToList2(input)
else:
ctext = list(map(ord,input))
L = 16-len(ctext)
ctext = list(bytes(L)) + ctext
obj = AES.AES("MODE_ECB")
obj.setKey(keysize,key,iv)
ptext = bytes()
L3 = len(ctext)
for i in range(0,L3,16):
AESoutput = obj.encrypt(inputBuffer)
inputBuffer = ctext[i:min(L3,i+16)]
ptextBlock = xor(AESoutput,inputBuffer)
ptext += ptextBlock
if type(input) is int:
ptext = bytesToInt(ptext)
else:
ptext = "".join(chr(e) for e in ptext)
return ptext
CFB8 encrypts (or decrypts) a single byte while CFB128 operates on a 16-byte block. Like the other stream modes (OFB and CTR), and differently from Electronic codebook (ECB) and Cipher block chaining (CBC) “block modes”, CFB dispenses padding and uses only the ECB encryption operation. This latter feature is very convenient for AES, since AES encryption and decryption operations are somewhat different.
The US National Institute of Standards and Technology (NIST) defines four types of Known Answer Test (KAT): GFSbox, KeySbox, Variable Key and Variable Text. The contents of file CFB8GFSbox128.rsp (see below) describe the GFSbox encryption and decryption tests cases for CFB8 using 128-bits AES keys.
# CAVS 11.1 # Config info for aes_values # AESVS GFSbox test data for CFB8 # State : Encrypt and Decrypt # Key Length : 128 # Generated on Fri Apr 22 15:11:46 2011 [ENCRYPT] COUNT = 0 KEY = 00000000000000000000000000000000 IV = f34481ec3cc627bacd5dc3fb08f273e6 PLAINTEXT = 00 CIPHERTEXT = 03 ... 5 test cases omitted [DECRYPT] COUNT = 0 KEY = 00000000000000000000000000000000 IV = f34481ec3cc627bacd5dc3fb08f273e6 CIPHERTEXT = 03 PLAINTEXT = 00 ... 5 test cases omitted
As a second example, the contents of file CFB128VarKey192.rsp file that follow describe the Variable Key encryption and decryption tests cases for the CFB128 mode using 192-bits AES keys.
# CAVS 11.1 # Config info for aes_values # AESVS VarKey test data for CFB128 # State : Encrypt and Decrypt # Key Length : 192 # Generated on Fri Apr 22 15:11:55 2011 [ENCRYPT] COUNT = 0 KEY = 800000000000000000000000000000000000000000000000 IV = 00000000000000000000000000000000 PLAINTEXT = 00000000000000000000000000000000 CIPHERTEXT = de885dc87f5a92594082d02cc1e1b42c ... 190 test cases omitted [DECRYPT] COUNT = 0 KEY = 800000000000000000000000000000000000000000000000 IV = 00000000000000000000000000000000 CIPHERTEXT = de885dc87f5a92594082d02cc1e1b42c PLAINTEXT = 00000000000000000000000000000000 ... 190 test cases omitted
Next is presented the Python program that extracts data from the files contained in the KAT_AES directory which names start with “CFB8” or “CFB128”, then builds the test cases and finally executes them.
#!/usr/bin/python3
#
# Author: Joao H de A Franco (jhafranco@acm.org)
#
# Description: Validation of AES-CFB8 and AES-CFB128
# implementations in Python
#
# Date: 2013-06-05
#
# License: Attribution-NonCommercial-ShareAlike 3.0 Unported
# (CC BY-NC-SA 3.0)
#===========================================================
import os,sys,re
from functools import reduce
from glob import glob
import AES_CFB
# Global counters
noFilesTested = noFilesSkipped = 0
counterOK = counterNOK = 0
class AEStester:
def buildTestCases(self,filename):
"""Build test cases described in a given file"""
global noFilesTested,noFilesSkipped
self.basename = os.path.basename(filename)
if self.basename.startswith('CFB'):
if self.basename.startswith('CFB8'):
self.mode = "MODE_CFB8"
result = re.search("CFB8(\D{6,})\d{3}",self.basename)
self.typeTest = result.group(1)
elif self.basename.startswith('CFB128'):
self.mode = "MODE_CFB128"
result = re.search("CFB128(\D{6,})\d{3}",self.basename)
self.typeTest = result.group(1)
else: # CFB1 files not considered
noFilesSkipped += 1
return
else: # not CFB files
noFilesSkipped += 1
return
noFilesTested += 1
digits = re.search("(\d{3})\.",self.basename)
self.keysize = 'SIZE_' + digits.group(1)
self.iv = None
for line in open(filename):
line = line.strip()
if (line == "") or line.startswith('#'):
continue
elif line == '[ENCRYPT]':
self.operation = 'encrypt'
continue
elif line == '[DECRYPT]':
self.operation = 'decrypt'
continue
param,_,value = line.split(' ',2)
if param == "COUNT":
self.count = int(value)
continue
else:
self.__setattr__(param.lower(),int(value,16))
if (self.operation == 'encrypt') and (param == "CIPHERTEXT") or \
(self.operation == 'decrypt' and param == "PLAINTEXT"):
self.runTestCase()
def runTestCase(self):
"""Execute test case and report result"""
global counterOK,counterNOK
def printTestCase(result):
print("Type={0:s} Mode={1:s} Keysize={2:s} Function={3:s} Count={4:03d} {5:s}"\
.format(self.typeTest,self.mode[5:],\
self.keysize[5:],self.operation.upper(),\
self.count,result))
if self.operation == 'encrypt':
if self.mode == "MODE_CFB8":
CIPHERTEXT = AES_CFB5.encryptCFB8(self.keysize,self.key,self.iv,self.plaintext)
else:
CIPHERTEXT = AES_CFB5.encryptCFB128(self.keysize,self.key,self.iv,self.plaintext)
try:
assert self.ciphertext == CIPHERTEXT
counterOK += 1
printTestCase("OK")
except AssertionError:
counterNOK +=1
print(self.basename,end=" ")
printTestCase("failed")
print("Expected ciphertext={0:0x}".format(self.ciphertext))
print("Returned ciphertext={0:0x}".format(CIPHERTEXT))
else:
if self.mode == "MODE_CFB8":
PLAINTEXT = AES_CFB5.decryptCFB8(self.keysize,self.key,self.iv,self.ciphertext)
else:
PLAINTEXT = AES_CFB5.decryptCFB128(self.keysize,self.key,self.iv,self.ciphertext)
try:
assert self.plaintext == PLAINTEXT
counterOK += 1
printTestCase("OK")
except AssertionError:
counterNOK +=1
print(self.basename,end=" ")
printTestCase("failed")
print("Expected plaintext={0:0x}".format(self.plaintext))
print("Returned plaintext={0:0x}".format(PLAINTEXT))
if __name__ == '__main__':
path = os.path.dirname(__file__)
files = sys.argv[1:]
if not files:
files = glob(os.path.join(path,'KAT_AES','*.rsp'))
files.sort()
for file in files:
AEStester().buildTestCases(file)
print("Files tested={0:d}".format(noFilesTested))
print("Files skipped={0:d}".format(noFilesSkipped))
print("Test cases OK={0:d}".format(counterOK))
print("Test cases NOK={0:d}".format(counterNOK))
This program, when executed without arguments, will generate the following report:
Type=GFSbox Mode=CFB128 Keysize=128 Function=ENCRYPT Count=000 OK Type=GFSbox Mode=CFB128 Keysize=128 Function=ENCRYPT Count=001 OK Type=GFSbox Mode=CFB128 Keysize=128 Function=ENCRYPT Count=002 OK Type=GFSbox Mode=CFB128 Keysize=128 Function=ENCRYPT Count=003 OK Type=GFSbox Mode=CFB128 Keysize=128 Function=ENCRYPT Count=004 OK ... 4,146 test cases omitted Type=VarTxt Mode=CFB8 Keysize=256 Function=DECRYPT Count=123 OK Type=VarTxt Mode=CFB8 Keysize=256 Function=DECRYPT Count=124 OK Type=VarTxt Mode=CFB8 Keysize=256 Function=DECRYPT Count=125 OK Type=VarTxt Mode=CFB8 Keysize=256 Function=DECRYPT Count=126 OK Type=VarTxt Mode=CFB8 Keysize=256 Function=DECRYPT Count=127 OK Files tested=24 Files skipped=48 Test cases OK=4156 Test cases NOK=0