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 [0]
    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[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 = 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[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 = 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
#
# 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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s