# Validation of an AES-CFB implementation in Python 3

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/python3

import sys
from functools import reduce
import sundAES

# 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)+y,lst)

def intToList(number):
"""Converts an integer into an integer list"""
if number == 0:
return 
lst = []
while number:
lst += [number&0xff]
number >>= 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 = sundAES.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 ^ 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 = sundAES.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 ^ 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 = sundAES.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 = sundAES.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
#
#          (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
```