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