The Advanced Encryption Standard and its modes of operation
José Luis Gómez Pardo
Departamento de Álxebra, Universidade de Santiago
15782 Santiago de Compostela, Spain
Carlos Gómez-Rodríguez
Departamento de Computación, Universidade da Coruña
15071 A Coruña, Spain
e-mail: cgomezr@udc.es
Introduction
This worksheet contains an implementation (and also a brief explanation) of the Advanced Encryption Standard (AES) block cipher and its modes of operation (which are not exclusive of AES and apply to other block ciphers as well). AES was selected as successor of DES in an open competition to this effect staged by the United States National Institute of Standards and Technology (NIST). The winner of this competition was the Rijndael algorithm, designed by Joan Daemen and Vincent Rijmen, which was specified as a Federal Information Processing Standard (FIPS) under the AES name by NIST in 2001 [FIPS197]. AES is a symmetric block cipher with a 128-bit block size and three possible keylengths, namely, 128, 192, and 256 bits. The encryption and decryption functions act in a number of rounds which depends on the size of the key: there are 10 rounds for a 128-bit key, 12 rounds for a 192-bit key and 14 rounds for a 256-bit key. Apart from the already mentioned FIPS publication, AES is described in detail in many books and web pages. Among the books we can mention [Stinson], [Oppliger] and [Daemen-Rijmen2], where the latter gives a first hand account of the design choices that led to the final form of the algorithm. Among the web pages, apart from the already cited NIST page, we mention Wikipedia's page, the MSDN magazine Encrypt It page, the AES Lounge which contains a lot of additional information, and also H.W. Lenstra's page, where a one-page description of Rijndael is given for people with sufficient algebraic background.
AES can be used to build up encryption or authentication schemes (or schemes that combine both aspects) and for that the so called "modes of operation" are required. The modes of operation allow the encryption of messages of arbitrary length and are a necessary ingredient of AES-based encryption schemes but AES itself is not such a scheme. The security properties of these encryption schemes depend heavily on the mode of operation used. Even if AES behaves like an ideal block cipher, i.e., as a pseudo-random permutation on the 128-bit blocks on which the AES encryption function (also called the forward cipher function) acts -which would imply that AES is highly secure-, ECB mode, for example, is considered insecure. In this mode, if a message or block is repeatedly encrypted with the same key, this fact can be easily detected by an attacker. This is a significant leakage of information that could even allow an active attacker to change the order of the blocks in a message or to replace new messages by old ones without the manipulation being noticed by the legitimate receiver of the information. We refer to the [Bellare-Rogaway] notes and to [Katz-Lindell] for rigorous and detailed discussion of the issues related to the use of the modes of operation for AES and other block ciphers (for more elementary introductions, the reader may also check [Buchmann] and [Trappe-Washington]).
In view of the preceding remarks, one design goal for this project was to cover most of the modes of operation currently in use. In fact, all the modes of operation specified by NIST so far are implemented here: the encryption modes ECB, CBC, CFB (the CFB128 version only), OFB and CTR, the CMAC authentication mode, and the CCM and GCM/GMAC modes for authenticated encryption (another mode, called XTS, is currently proposed for NIST approval and yet more modes will probably be approved in the future). A second design goal was to build up an implementation able to handle plaintexts and ciphertexts in a variety of formats and having the capability of encrypting/decrypting/authenticating real messages and not just short text strings. We feel that, even if the primary purpose of an implementation like this one is to let users to experiment and learn, these goals are best achieved if it can handle these messages. Thus the implementation uses, whenever possible, lookup tables for efficiency and is able to encrypt both text strings and binary files of moderate size, up to several megabytes.
Another aspect worthy of mention is the fact that the implementation contains detailed explanations of the procedures used, including the lower level ones and discussing both the programming and the cryptographic aspects involved in the design choices made. However, with a few exceptions, we have not included line-by-line comments in the code itself. Since the code for each function is not too long (with the possible exception of some of the test functions at the end, which are essentially variants of other previously defined functions), we feel that the required explanations are better presented in the text comments surrounding the code, where they are not limited to technical implementation issues but deal with broader aspects related to design choices, to the algorithms, and so on. On the other hand, we have not tried to give a very detailed description of the AES algorithm itself because we feel that there are many excellent presentations in the literature and, in particular, in the references below (several of these presentations can be easily accessed from here just by clicking on the appropriate hyperlink). Nevertheless, the attentive reader can also extract a complete description of the algorithm from our comments and from the code. In the case of the more recent modes such as CCM or GCM/GMAC, we have tried to include some more detail in the comments although, of course, the reader is encouraged to look at the original sources and, in particular, to the NIST publications where these modes are specified.
Apart from the above mentioned features, we have also included a collection of tests. Among them there are tests that check compliance with NIST's specifications and other tests devoted to analyze some basic properties that are cryptographically relevant, such as diffusion, which is studied by collecting, displaying (in both text and graphic format) and analyzing statistical data.
Before starting to describe the contents of the worksheet, a remark on terminology is in order. In Maple, an expression of type "function" is what might be better called a "function call", i.e., citing Maple's help,
"an application of a function or procedure to arguments"
. Here we will use the term "function", apart from some specific cases in which it refers to the mathematical concept, in the sense it is used in the text of the previous hyperlink, i.e., more or less as a synonym of "procedure".
The broad structure of the worksheet may be sketched as follows. The Initialization section builds most of the lower level functions that will be used through the worksheet. These include, in addition to conversion functions between different kinds of data and functions to generate pseudo-random data such as keys, an implementation of the arithmetic of the 256-element field GF(28) which, in turn, is used to construct the round constants required by the Key Schedule process -the process that generates the round subkeys from an AES key- and the SBox, a permutation of GF(28) that underlies one of the AES operations. These operations, as well as the Key Schedule are defined in the next section (AES operations). Then there is a section, Low-level AES functions, devoted to functions that do low-level AES encryption and decryption by processing either one state (an AES block formatted as a 4x4 Array of bytes) or a list of bytes. These functions may operate in any of the five confidentiality modes and, in this section, functions are also provided for generating initialization vectors and for padding. The next section (AES encryption and decryption functions) builds up the higher level functions that are used for encrypting/decrypting data in the usual formats. Among them there are key-generating functions, functions to encrypt and decrypt binary files, functions to encrypt and decrypt text strings, and functions to encrypt and decrypt text files (line by line). These functions can encrypt or decrypt data in any of the five confidentiality modes. The next section, Authentication, is devoted to CMAC authentication mode and has functions for generation and verification of the MAC associated to a message. In the Authenticated Encryption section, the two modes for authenticated encryption approved by NIST (CCM and GCM/GMAC) are implemented. Then there are two sections devoted to provide tests for the implementation. First, Validation tests, where tests that check compliance with the algorithms approved by NIST are given. These include functions to display on screen several constant and tables used by AES, some random tests and Known Answer Tests that check whether the implementation of AES encryption (or authentication) in the different modes behaves as expected by comparing the results with the "example vectors" provided in NIST publications. The output of these functions is formatted as in NIST publications to facilitate the comparison. Then there is a section on Diffusion tests, in which functions are given to collect and display statistical data and to perform some simple statistical tests related to the diffusion properties of AES. The final section contains the references, with hyperlinks to the appropriate web pages in case they are on-line (in fact, all of them are except the books).
Finally, a few remarks about function naming and type checking. In general, we have tried to use meaningful names for the functions we define. These names tend to be long but they are easier to remember and to identify than shorter names. In the case of lower level procedures that are intended to be called from other functions and not directly by the user, we often use lower case letters only. For functions that are to be called by the user or that have a definite cryptographic meaning we frequently use a concatenation of the words that describe the function with the initial letter of each word in upper case. Also, to allow for more compact code when calling functions from Maple packages, one could use the with command in the Initialization section to make the short names of these functions available. However, there are some cases in which two of the packages we use export the same name (for example, LengthSplit, Reverse and Rotate are common to both ListTools and StringTools, Generate is common to StringTools and RandomTools, Random is common to StringTools and LinearAlgebra:-Modular ...). We could still use the short form of these names in many situations but this would require keeping track of the order in which the names of the different packages were exported, creating a potential source of confusion. To prevent this and also to make the code usable in different contexts without further modification, we have decided to use the long function names even in case these names are not repeated in different packages. Regarding type checking, it is minimal or even non-existent in low level functions but we have tried to make extensive use of it in the higher level ones. Also, for many of high level functions and for the convenience of the user, we have provided default values for some of the parameters, whenever it makes sense.
Version, requirements and copyright
Version: 1.1
Requirements: Maple v11 or higher
Copyright Notice: © (2008-2010) José Luis Gómez Pardo, Carlos Gómez-Rodríguez
Legal Notice: The copyright for this application is owned by the authors. Neither Maplesoft nor the authors are responsible for any errors contained within and are not liable for any damages resulting from the use of this material. This application is intended for non-commercial, non-profit use only. Contact the authors for permission if you wish to use this application in for-profit activities.
Initialization
| > |
GF(28)
AES, like other symmetric block ciphers, is byte-oriented in the sense that the basic processing unit is a byte, i.e., a sequence of 8 bits which can also be thought as an 8-bit string or list, whichever is more convenient (often, in the literature, the perhaps more precise term octet is used instead of byte). Each byte in turn can be identified with the integer it represents in binary and, for this, we shall generally adopt the big-endian convention, meaning that the most significant bit is the leftmost one (here, it is convenient to point out that Maple's function convert/base uses the little-endian convention). Thus we shall often identify the bytes with the (decimal) integers in the 0..255 range. Several of AES operations are based on the arithmetic of the 256-element field GF(28) defined by the irreducible polynomial x8 + x4+ x3 + x +1 (a good reference for finite fields and their applications is [Lidl-Niederreiter]). The elements of this field are binary polynomials of degree less than or equal to 7, and the field operations are just polynomial addition (in ℤ2[X], where ℤ2 = GF(2) = {0,1} is the prime field of characteristic 2) and polynomial multiplication modulo the polynomial specified above (this just means that multiplication in ℤ2[X] is followed by taking the remainder of division by the irreducible polynomial). The elements of the field can be identified with the binary strings defined by their coefficients and hence with the bytes. In order to make the implementation able to encrypt files of moderate size, the field arithmetic is going to be implemented by means of lookup tables (in the form of Maple's tables or Arrays). These tables are constructed here during the initialization process by using the basics of finite field arithmetic and, in particular, Maple's package GF . The field GF(28) is defined as follows:
| > |
We will need some conversion functions to convert the field elements to the different formats. In particular, we want to be able to convert integers in the 0..255 range (i.e., bytes) to lists of 8 bits and vice versa. The next function is used to pass from bytes to lists of bits and, as mentioned, we will use the big-endian convention in which the most significant bit comes first.
| > |
The following tables will be helpful to speed up the process of converting between integers in the 0..255 range and 8-bit lists.
| > |
Using these tables we define a very efficient conversion between bit lists and bytes regarded as decimal numbers in the 0..255 range (thus, for efficiency, we will use bytetobits instead of byte2bits in the sequel; these functions will be used mainly to apply then to a list by means of map, in other cases we will use the preceding tables directly).
| > |
For example, we have:
| > |
Another usual and more compact form to represent the bytes and hence also the elements of GF(28) is by means of hex digits, each two-digit hex string corresponding to one byte. More generally, we can represent any list of bytes (as integers in the 0..255 range) by the even-length hex string obtained by concatenating the hex representations of the bytes in the list. We are going to give conversions between bytes or lists of bytes and hex strings that will be used in the encryption/decryption/authentication functions. In particular, these functions can be used to convert between keys given as lists of bytes and keys given as hex strings. To make these conversions faster, we will use tables. Note that Maple's convert/hex function outputs the alphabetic hex digits as upper case letters but we will use lower case letters instead as it is more usual in cryptographic texts and code. We start by building a table (in Array format) with the correspondences between bytes and hex digits.
| > |
Based on the above table, the following is a function to convert a list of bytes to a hex string.
| > |
Next, we "invert" the preceding table, this time to convert strings formed by two hex digits to the corresponding bytes in integer form and use this table to give the associated function that converts hex strings to lists of bytes:
| > |
A quick check that these functions behave as intended is the following:
| > |
We will use AES to encrypt and decrypt files. For this purpose, the natural way to proceed is to view a file as a list of bytes, i.e., a list of elements of GF(28). The
FileTools[Binary]
package has functions to read and write binary files by using a hardware integer type and the closest to what we want, which is a list of bytes as integers in the 0..255 range, is obtained by using the integer[1] type. However, these integers range from -128 to 127 and so we must make the conversion after reading the file and before writing it to disk. To read a file to a list of bytes, we use the integer[1] type and convert to bytes by reducing modulo 256:
| > |
For the inverse process we want to check, before writing a list of bytes to a file, if a file of the same name already exists and, if this is the case, if it can be overwritten. The following function checks this and prompts the user about the action to take if the file already exists. In this case the user can choose between overwriting the existing file, abort the process or choosing a new file name. The output of the function is either an error message if the operation is aborted or a file name:
| > |
Now, to write a list of bytes to a file we have to convert bytes (integers in the 0..255 range) to integers of type integer[1]. One would like to use the function mods(-, 256) but this function does not convert the 0..255 range to the -128..127 range but to the -127..128 range instead. This problem is easily solved as, if one applies mods(-,256) one only needs to take care of converting the 128 byte to -128 instead of keeping its original value. We will do it by using a slight modification of this function, namely the function x -> mods(x+1,256)-1. In order to speed this process up, we build a table to convert bytes to integers in the integer[1] range:
| > |
Then the function that writes the byte list to a file is the following one. The input is the list of bytes to be written to the file, the file name (given as a string), a boolean-valued parameter called filecheck and a parameter called logoutput. If the value true is passed to filecheck, the function calls checkfile to check whether a file of the same name exists in the current directory and eventually prompts the user about which action to take. On the other hand, the value of logoutput can be none, file or terminal, and it specifies whether log information (indicating the number of bytes written and the name of the file) is provided and, in this case, whether it is written to a file or to the terminal (i.e., printed on the screen).
| > |
To test these functions, let's generate a random list of size slightly less than 16 Kibibytes. We use Mersenne Twister (called from RandomTools:-Generate) instead of Blum-Blum-Shub; the latter is much less efficient and there is no point in using it here:
| > |
Let's now write this list of bytes to a file named "testfile" in the current directory:
| > |
| 16381 bytes saved to testfile |
|
|
To check the correct behavior of the file writing and reading functions, we read the file to a list and compare it to the original list:
| > |
Next, using the previous conversion functions and bearing in mind that, later on, we will give encryption/authentication functions that accept as input several types of messages, we give functions to convert messages which can be either ordinary text strings or hex strings or files, to lists of bytes or, in other words, to lists of elements of GF(28) (note that a hex string is different from ordinary text in that it translates to the list of bytes given by hexstringtobytes instead of the ASCII codes given by convert/bytes which are used in the ordinary text case). As mentioned above, we use lower case for the alphabetic hex digits appearing in hex strings but we want that the functions that take hex strings as input also accept the equivalent version that uses upper case letters. For this reason, in the next function, StringTools:-LowerCase is called before applying hexstringtobytes.
| > |
To go in the opposite direction, we also give a function to convert a list of bytes to a message in the specified format. Note that, when using this function, one must be careful if the 0 byte belongs to the list to be converted because in that case the conversion will be truncated at this point (this aspect will be discussed in more detail below, in the section dealing with text encryption and decryption).
| > |
Finally, by combining the file/bytes and bytes/string conversions, we give functions to write a (hex or text) string to a (binary) file or to read a file as a string.
| > |
| > |
To check these functions we start with a string containing a quote from Octavio Paz with a lot of punctuation and diacritical marks:
| > |
We write this string to a file named "paz.txt", then we read the file to a string which is compared to the original string:
| > |
| 1100 bytes saved to paz.txt
|
Next, we consider a hex string:
| > |
| 64 bytes saved to hex64
|
The next function will play an auxiliary role in several functions where a key or a seed that can be either given as a hexadecimal string or an integer is used. It checks whether the input is a string and, if so, whether it is a hexadecimal string and, in that case, it converts it to an integer.
| > |
The BitXor operation
When the elements of GF(28) are regarded as integers in the 0..255 range, the sum in GF(28) is just the bitwise Xor or BitXor. This operation is frequently used in the AES algorithm and hence it is convenient to have it efficiently implemented. As Maple v11 does not have BitXor natively implemented for integers, the easier solution is just to use the package GF and define the sum in the field F = GF(28) as:
However, to make the operation faster, we will implement it by means of a lookup table. This table can be constructed with the help of the preceding function but, to speed the process up, we will use the previously constructed tables that convert between bytes and bit lists, to define the BitXor operation for integers in the 0..255 range. First, we give a procedure to carry out the BitXor operation between lists of bits of the same length:
| > |
Then, the lookup table for the BitXor operation can be constructed as a symmetric array (since the operation is commutative) as follows.
![]()
![]()
![]()
![]()
![]()
Using a symmetric Array cuts in half both the time to build the table and the storage required in comparison with the full table. However, it also has some drawbacks derived from the fact that, for the array to be symmetric, the indexing must start at 1 instead of at 0. This forces us to carry out a few extra operations when computing a BitXor by looking up the table and, since this operation will be applied a lot of times, it produces a non negligible loss of speed. Moreover, accessing a symmetric array is slower than accessing an array with rectangular storage. Thus we will use an array with rectangular storage containing the full table (with 2562 = 65536 entries, which is not too much for today's computers).
| > |
Then the BitXor operation with two integers in the 0..255 range as arguments is the following:
| > |
We will also use the following version of BitXor, which computes the BitXor of a 0..3,0..3 Array a with a list l of no more than 16 terms, where the array is passed as the first argument and the list as the second. If n is the the length of l, then the output is the list of length n obtained by BitXor-ing the elements of l with the first n elements of a with respect to
Fortran_order
. The function ArrayTools:-Alias is used here to view the 2-dimensional Array as a 1-dimensional Array with this order.
| > |
The multiplication in GF(28)
In the MixColumns operation of AES we will have to make use of the multiplication in the field GF(28). The multiplication, for elements as integers in the 0..255 range is given by:
| > |
However, we will not use this function directly in MixColumns because we will have to multiply 4x4 matrices over GF(28) and this means a lot of multiplications of field elements. To optimize speed, we will instead use a lookup table constructed by means of this function. In fact, we will build two tables. The reason is that only a handful of the field elements (specifically, 1, 2, 3, 9, 11, 13, 14) are required for MixColumns and its inverse and so we only need to know the result of multiplying each of these bytes by any other byte. If one uses tables, then the indices need not be consecutive and so one can build up a table whose rows correspond to all the elements of the field (bytes in the 0..255 range) and whose columns correspond to the elements of the field mentioned above (excepting 1, as multiplication by 1 is trivial), namely, 2,3 (required by MixColumns) and 9,11,13,14 (for the inverse). This would give a table that is much smaller than the full multiplication table -which would require much more time to build and much more space to store- and, since the indices of a table need not be integers, it would also permit to deal with the elements in the form of 8-bit lists instead of bytes. But this approach will not be pursued here as it has other drawbacks. We will use arrays, which must be indexed by integer ranges and so, to economize both space and time, we will build up two Arrays for the multiplication table, one for the multiplication by 2 and 3 and the other for the multiplication by the elements in the 9..14 range:
| > |
The round constants
Here, we define the "round constants" which are used during the key schedule that builds the round subkeys. These constants are 14 4-byte words (herein represented as 4-byte lists) which are obtained by successive multiplication by 2 in GF(28) starting with the word [1,0,0,0].
| > |
The list of round constants, Rcon, is then:
| > |
The SBox
The SubBytes operation is based on a permutation of GF(28) that associates to each nonzero byte the result of computing the inverse byte in GF(28) —the 0 byte is mapped to itself— and then applies an affine map to the corresponding bit vector. To obtain an efficient implementation of SubBytes we will construct a table called the SBox. To build the table we start with the following function that gives the inverse of a byte in GF(28):
| > |
The affine map used by SubBytes is given by an 8x8 matrix of bits and a bit vector of dimension 8. These are the following:
| > |
![]() |
Now, the affine map itself, with input and output an 8-bit list:
| > |
With these ingredients, we give a function (ByteSub) that implements the permutation of GF(28) underlying SubBytes (which will be defined in the next section).
| > |
We now construct the SBox table and its inverse (InvSBox). Since in this case all the required indices are integers, we will use Array as data structure instead of table:
| > |
Based on these tables, we can define more efficient versions of ByteSub and its inverse permutation. The SB function will also be used during the Key Schedule process.
| > |
The pseudo-random generator
An important role is played in this worksheet (and, more generally, in cryptography) by the pseudo-random number generators (PRNG's) used to generate keys, initialization vectors, etc. An important feature of PRNG's is that they must be seeded by an entropy source. In this worksheet we are going to use Maple's PRNG's often and, since they are deterministic algorithms, if directly called without previous seeding they would produce the same output sequence each time. For some specific purposes this could be acceptable but it is clear that keys, for example, should not be generated this way. From version 10 onwards, the default PRNG in Maple (the one that is called by the function rand()) is the
Mersenne Twister
PRNG (described in detail in [Matsumoto-Nishimura], where it was introduced). Although this is not the PRNG that we will use to generate keys -for reasons that will be explained later on- it will still play a role for some purposes such as testing, so that we will now set the initial state of this PRNG or, rather, let Maple do so by seeding it with values taken from the system (this initialization should always be carried out before using the PRNG). The next function sets the initial state of Mersenne Twister:
| > |
The Mersenne Twister PRNG is very fast and passes many statistical randomness tests (so it is often the PRNG of choice for statistical simulations) but is considered unsuitable for cryptographic use due to the fact that knowledge of only 624 consecutive outputs is sufficient to predict further outputs. For cryptographic applications a "cryptographically secure" PRNG is required, meaning that it should pass the "next bit" test, in the sense that knowing the first s bits of a sequence output one should not be able to predict the value of the (s+1)-bit with probability significantly greater than 1/2. A PRNG that has been proven to be cryptographically secure under certain assumptions -the most important being the assumed difficulty of the integer factorization problem- is the Blum-Blum-Shub (BBS) generator (see, for example, [Stinson] or [Geisler-Kroigard-Danielsen] for a security proof, and also [Monagan-Fee] which, in addition, gives a description of the BBS Maple implementation). The BBS generator is implemented in Maple in
RandomTools:-BlumBlumShub
and we will use this PRNG to generate pseudo-random keys and other objects. A problem inherent to the way PRNG's work is that they must be seeded by an entropy source which, in order to provide the true random bits required to obtain sufficient security, should involve some kind of physical process (see [SP800-90]). In our case we shall either use externally generated random seeds -this is the preferred method and the only one that, if correctly used, produces secure results- or seeds taken from the system, which are not random but may be used for demonstration purposes. As we shall see in the following sections, we will deal with keys and initialization vectors that are lists of integers in the 0..255 range, which we will refer to as bytes. The PRNG that we will use to generate pseudo-random bytes is BBS and, in particular, its implementation in Maple's function
RandomTools:-BlumBlumShub:-NewBitGenerator
. This function has a required input parameter for the seed and the BBS PRNG works by taking squares modulo and integer which is a product of two large primes congruent to 3 (mod 4). For the security of BBS it is crucial that an adversary be unable to factor this integer and recover the prime factors. In the case of Maple's implementation, there is a choice among three integers which are stored in three variables local to the module
called n512, n768 and n1024. The latter evaluates to an integer of 616 decimal digits that is a product of two 1024-bit primes and, similarly, n512 and n768 evaluate, respectively, to a 308-digit product of two 512-bit primes and a 462-digit product of two 768-bit primes and
RandomTools:-BlumBlumShub:-NewBitGenerator
lets us choose which of these integers to use through the input parameter called primes, in which any of the three values 512, 768 and 1024 may be specified. The next function BBSByteGen relies on NewBitGenerator to generate a list of pseudo-random bytes of specified length. The required input parameters are length for the length of the list of string or bytes to be generated and seed for the seed (which, as mentioned above, should be randomly generated). Actually, the function will also work if no argument is passed to the parameter seed, in which case the seed will be generated by means of Maple's function RandomTools:-MersenneTwister:-GenerateInteger() (see the comments about this usage in the discussion of the function PseudoRandomKeyGen below). There are two optional keyword parameters, format, used to specify whether the output will be a list of bytes or a hexadecimal string (with list as default) and primeslength, which specifies the length of the primes to be used by BBS among the three possible values 512, 768, 1024, with the latter being the default.
| > |
This function does not have type checking nor default values for the parameters because it is not intended to be called directly by the user but rather by other functions that will build lists of pseudo-random bytes. The seed can be any positive integer which is increased by 1 inside the function because Maple's NewBitGenerator procedure enters an infinite loop when seed = 1 which, anyway, will only happen with negligible probability if the seed is randomly chosen. The integer values of n512, n768, and n1024 can be obtained from Maple's source code and are also given by the authors of Maple's BBS implementation in [Monagan-Fee]. According to the publicly available knowledge, it seems that factoring these numbers is not currently feasible, although it is foreseeable that n512 could be factored within a few years. Of course, these numbers were constructed by finding the prime factors first and then multiplying them, and this means that if one uses Maple's Blum-Blum-Shub implementation, one has to trust Maple's assertion (in NewBitGenerator's help ) that it does not have these factorizations. If Alice does not want to trust Maple, then she can modify Maple's code by replacing the values of n512, n768 and n1024 (or, at least, of some of them) by different integers of similar size which are known only to her and satisfy the requirements of the BBS generator. This is easy to do following the method indicated in [Monagan-Fee] but it requires a certain amount of computation because the primes are chosen so as to make easy the computation of a seed which will produce a BBS output sequence with long period. However, the fact that these primes will be known only to Alice can be used to reduce the computational cost of computing them as this makes easier to find an appropriate seed. But, on the other hand, Alice might as well have to reveal these primes to Bob if she wants to convince him that the key she is offering to share cannot be easily guessed by Eve ...
AES operations
In this section, the four AES operations are defined. These operations are, following standard terminology: SubBytes, ShiftRows, MixColumns, and AddRoundKey. Furthermore, we also set up the Key Schedule which is the process that generates the round subkeys through a function called KeyExpansion. Observe that all AES operations operate on 128-bit plaintext or ciphertext blocks each of which is organized as a 4x4 matrix of bytes called the state. In our implementation, the state will be a 2-dimensional Array with 0..3, 0..3 indexing. This is slightly more efficient than the 1..4,1..4 matrix indexing as it simplifies the operations done when manipulating its entries. A perhaps simpler alternative would be to use a list for the state. However, working with lists is generally less efficient than using Arrays and, moreover, Arrays can be modified "in place", which often makes it unnecessary to build a different copy and gives additional efficiency gains.
SubBytes and its inverse
For the SubBytes operation and its inverse we could just use LinearAlgebra:-Map to map the SB function defined above to the state Array. However, to increase speed, we will use the following functions that replace the state entries by looking directly at the SBox and InvSBox tables:
| > |
| > |
ShiftRows and its inverse
The ShiftRows operation consists in shifting the rows of the state Array by 0, 1, 2, 3 positions to the left, i.e., each row is shifted by the same amount as its index. The inverse operation just shifts rows right by the same offsets. These operations are not implemented "in place" as we need to use a copy of the state Array inside the corresponding function.
| > |
| > |
MixColumns and its inverse
The MixColumns operation acts on the state matrix by multiplying it by a 4x4 matrix with entries in GF(28). It replaces state by the matrix M
state, where M =
(so that the multiplication of these matrices uses the addition and the multiplication in GF(28)). This operation may also be regarded as replacing each column of the state matrix by the result of multiplying M by it, hence the name. To improve speed, the function calls directly bitxortable and multtable1 to do multiplications by 2 and 3 in GF(28). This is important because MixColumns requires, for each plaintext block, 64 multiplications of bytes per round, which translates into a minimum of 576 multiplications per block during encryption.
| > |
The inverse operation consists in multiplying by the inverse matrix of M which is
. To implement it we use the second multiplication table, multtable2:
| > |
The Key Schedule
The only AES operation remaining to be defined is AddRoundKey. For this operation it is necessary to generate the "round subkeys" using the so called "Key Schedule" process. This is done by the next function which takes as input the AES key, given as a list of 16, 24 or 32 bytes (we will give functions to generate keys later on). The output is a list of 0..3-0..3 Arrays whose entries are bytes; each one of these Arrays is a round subkey. The first 4, 6 or 8 columns of the first two Arrays contain the key, which has been mapped to them columnwise, starting on the top of the leftmost column. The functions RotWord and SubWord mentioned in [FIPS197] are not defined here because the first is simply a version of Rotate from the ListTools package and the second consists in applying SubBytes to a 4-byte word, which is done here by means of the previously defined function SB.
| > |
For example, let's look at the expanded key for a 128-bit key consisting entirely of zero bytes. The results may be compared to those in the web page Key Schedule bearing in mind that here each round key is a 4x4 matrix and that the string form of the subkey defined by each of these matrices is obtained by going through the matrix entries columnwise.
| > |
![]() ![]() ![]() |
AddRoundKey
The AddRoundKey operation is simply the result of BitXor-ing the bytes in the state matrix with those in the corresponding round subkey of the expanded key (in other words, the new state matrix is obtained by adding over GF(28) the round subkey matrix to the old state matrix). The state Array is changed "in place" by modifying it and this can be done without returning anything. However, for convenience, our function returns the new value of the state. Note that in this case we do not define the inverse operation as AddRoundKey is its own inverse.
| > |
Low-level AES functions
In this section we define two levels of AES encryption/decryption functions and also some auxiliary functions to provide them with the input in the required format. The lower level consists of the "forward cipher function" and its inverse (herein called Encrypt and Decrypt, respectively). These functions accept as input a state matrix (i.e., a plaintext or ciphertext block) and the expanded key and give as output the corresponding plaintext/ciphertext state matrix. The higher level functions deal with lists of bytes of arbitrary length (assumed to be a multiple of the block length for some modes) and they incorporate the five confidentiality modes of operation specified by NIST. We start with the lower of the two levels just mentioned.
The Rijndael rounds and the cipher and inverse cipher functions
First we define an AES round. The input is a state Array and a round subkey (also a 0..3,0..3 Array) and the output is a modified state.
| > |
The next function is the forward cipher function which, given a state matrix and a expanded key, produces the ciphertext corresponding to this state.
| > |
Similar to the Round function, the inverse round is now:
| > |
Finally, the inverse cipher function:
| > |
Encrypting and decrypting lists of bytes
Now, we give higher level encryption and decryption functions, EncryptBytes and DecryptBytes. In this case, the input is a list of bytes (like the one obtained by reading a file from disk), a key (given also as a list of 16, 24 or 32 bytes), a mode of operation (one of the following strings: "ecb", "cbc", "cfb", "ofb", "ctr", which we use to represent ECB, CBC, CFB, OFB and CTR modes, respectively) and an "initialization vector" (IV) except in the case of ECB mode which does not require it. We take as default mode "cbc" and, for the IV, we use a keyword parameter iv with default value the list of 16 0-bytes. The output is a list of bytes containing the corresponding ciphertext (with the IV added in case the used mode requires it) or the plaintext (in the case of the decryption function). For ECB, CBC and CFB modes, the length of the input list of bytes is supposed to be a multiple of the block length (i.e., the byte length of this list should be a multiple of 16). We will see later how every plaintext can be converted to such a list by using padding. The list of bytes -together with the IV- is partitioned into 16-byte sublists (the blocks) and each of these blocks is converted to a state 0..3,0..3 Array. In the case of OFB and CTR modes, which do not require padding, the list of bytes has arbitrary length and is partitioned again into 16-byte sublists, except the last one which may have less than 16 bytes and is regarded as a partial block. Because of this we do not make the states into Arrays in these cases and we keep them as lists instead (in this case, the length of the ciphertext -minus the IV- must be exactly equal to the length of the plaintext because otherwise you would not know after decryption where the plaintext ends). In these cases we will use the function BitXorAL defined above which allows us to compute the BitXor of a 0..3,0..3 Array of bytes and a list of bytes of no more than 16 elements. In all cases, the list of all the states is converted to a 1-dimensional Array -this is necessary because we want to modify the states and this cannot be made by direct assignment if they are the members of a list of more than 100 entries.
These functions are still pretty low level and they are not supposed to be directly called by the user, except perhaps for testing or learning purposes. Instead, they will be called from other functions that are the ones that actually encrypt files or text strings. Because of this we do not include any error-checking here, as the higher level functions are supposed to pass correctly formatted arguments to these ones. Note also that the output of the EncryptBytes function for the modes that require an IV is not just the ciphertext but the IV concatenated to the ciphertext. This has the advantage that the decryption function does not require the IV as a separate input and is perfectly OK since the IV need not be secret and, moreover, its length is fixed and publicly known. Observe that in these functions, in contrast with the previous ones, the key (as a list of bytes) is used instead of the expanded key. There is the possibility of gaining some speed by using the expanded key instead of the key in all the encryption/decryption functions (here and in the higher level ones). This way, the key expansion function would be invoked only once for each key, just after generating it, and the expanded key thus obtained would be the one used by all the functions. Since the time taken by KeyExpansion is not too long and, moreover, one should change keys often, we will not implement this.
The implementation of the modes of operation consists in running a loop over the Array containing the states replacing each state with the corresponding encrypted or decrypted one. These loops just use the functions Encrypt/Decrypt, BitXor (where BitXor applied to two state matrices is just the AddRoundKey operation) and BitXorAL in the way described in the specification for the modes of operation [NIST 800-38A]. The only thing worthy of mention here is that, in CTR mode, a series of different blocks called counters (one for each plaintext block) must be generated. To generate the counters we need two ingredients. One of them is an algorithm to choose the initial counter block corresponding to each encryption; this initial counter will be the IV and we will later give functions to generate it. The other ingredient is a method to generate the subsequent counters in such a way that their values are never repeated across all the encryptions done with the same key. To achieve this we use an "incrementing function" which is called after encrypting each block in order to obtain the counter for the next block from the preceding counter. We will use the so-called 32-bit incrementing function which consists in adding 1 to the counter modulo 232, when the counter is viewed as a 16-digit number in base 256, in big-endian order, i.e., with the most significant byte first. Therefore, the 32-bit incrementing function only modifies the last four bytes of the counter -that initially are all zero- and cycles among all the possible values of these 4 bytes. The function could also have been defined modulo 2128 -for example-, so that the counter values would cycle among 2128 possible ones but this is not really necessary. The 32-bit version provides for 232 different counter values. Since each of these values is used to encrypt one 24-byte block, this means that the plaintext can have up to 236 bytes without any counter value being repeated during encryption. This is more than enough (as 236 bytes = 64 gigabytes) and, in fact, the implementation cannot handle plaintexts so large because Maple's list size limit imposes here a maximum plaintext size of 226-4 bytes, which is close to 64 MB. It is possible to modify the implementation to allow for larger plaintexts but, given other realistic space-time constraints, there would be little point in doing so.
During encryption, the counter blocks will take the form of 0..3,0..3 Arrays and the increasing function that acts on them is the following (note that the function just increases the counter value without returning the new value; it just returns the last modified byte and can be easily modified to return NULL).
| > |
In the EncryptBytes and DecryptBytes functions we shall need to compute the BitXor of two states. The AddRoundKey function does just this. To distinguish the use of BitXor in CBC and CFB modes from the AddRoundKey operation, we define an alias of the latter and call it BitXor:
| > |
The encryption function for lists of bytes is then:
| > |
The decryption function that acts on lists of bytes is very similar to the preceding one. Here the IV is not required because it consists of the first 16 bytes of the (ciphertext) list.
| > |
Later, we will give tests to check the behavior of these functions against known values. For now we give, as a very simple example, the result of encrypting and decrypting the example vectors in Example C1 (AES-128) of [FIPS197]:
| > |
| > |
When calling the preceding functions from higher level encryption/decryption functions, we will use the following procedure to check if the supplied mode name is correct. Valid modes are
| > |
Initialization vectors
For all modes except ECB, an initialization vector (IV) must be generated. The IV need not be secret but, for CBC and CFB modes, it must be unpredictable so that, for any given plaintext, it must not be possible to predict the IV that will be associated to it. One of the best ways to generate such an IV is by using a PRNG and so we will use Blum-Blum-Shub through the BBSByteGen function to generate a pseudo-random IV of specified length. For the confidentiality modes already mentioned we will only use IV's of byte length 16 but IV's of different length will also be used in some of the authentication modes to be implemented later on in this worksheet.
For OFB mode, the IV need not be unpredictable and hence it need not be random but it must be a nonce (in the cryptographic sense) that is unique to each execution of the encryption operation (with the same key). The usual techniques to obtain a nonce are based on using a timestamp or a PRNG of sufficient quality to ensure a probabilistically negligible chance of repeating a previously generated value. In this case, we are going to use a combination of both techniques. The reason for not using the timestamp alone is that Maple will only retrieve the current date/time up to the seconds and so, to prevent a repetition if several quick encryptions are done within the same second, we will add the pseudo-random values. Moreover, since unpredictability is not a concern here, we shall use the Mersenne Twister PRNG because of its high efficiency. On the other hand, we will not use a purely pseudo-random nonce to prevent the very small chance of getting a repeated value. Thus, a nonce of length n (a list of n bytes) will be generated as follows. Using StringTools:-FormatTime with the appropriate format string and then using sscanf, we get a list of 6 bytes which correspond to the last two digits of the year, the month of the year, the day of the month, the hour (in 24-hour clock format), the minute and the second. This sequence of 6 bytes is then completed with n-6 pseudo-randomly generated bytes. Note that it would be a mistake to initialize the PRNG each time that nonce() is invoked because, as the seed is taken from the system clock, this would allow the possibility of obtaining two consecutive equal values when calling this function, although it would be an unlikely event if an actual encryption is being carried out. On the other hand, there is no need to initialize the PRNG here, for this has already been done at the beginning of the session. Note that, in fact, for the purpose of this function it would even be unnecessary to seed the PRNG since the purpose here is not so much obtaining random values as the fact that these values are not repeated (an alternative approach would consist in taking some of the bytes from the CPU time spent).
One rather trivial difficulty that might, in theory, arise is that using the date/time stamp to generate the nonce as we do here might lead to a repetition if the system time is changed in a non-regular way, for example at the winter daylight saving time or because of a malfunction of the system clock. This could allow the value to reach a repetition. However, since half of the bytes are pseudo-randomly generated, such a repetition is very unlikely and it can also be prevented with some precautionary measures, so that we shall not worry about this either.
| > |
One of the uses of the previous function will be to set the initial value of the counter in CTR mode. The procedure we are going to follow for this is similar to one suggested in [SP800-38A]. At the start of each CTR encryption, the initial counter will be obtained by calling nonce(12) and completing it with four zeros on the right to make a 16-byte block. We have already mentioned the use of the incrementing function to generate the subsequent counter values but, on the other hand, since the counter is initialized by using the nonce each time the CTR encryption function is called, one has to ensure not only that the value returned by the nonce function never repeats -as already discussed- and that the counter value never repeats during one encryption but also that no repeated counter values arise during different encryptions with the same key. Assuming that the nonce behaves as expected, since the counter works by successively increasing the initial nonce during each encryption, the only thing that must be satisfied to ensure that the counter values never repeat is that the value returned by calling the nonce function increases at greater speed than the counter itself. Of course, the speed with which the counter increases depends on the speed of the encryption operation, given that the counter increases by 1 for each 16 bytes that are encrypted. On the other hand, the nonce advances (assuming than at least one second has passed) more than 280-264-1 units per second. This is because the byte corresponding to the seconds is the sixth from the left and an increase of a unit in this byte corresponds to an increase of 280 in the value of the block if we view it as a 16-digit base-256 number. Assuming that the pseudo-random part is at its maximum value before increasing the sixth byte and at its minimum value after increasing it, the difference between both values is the one between the largest 8-byte number -that may correspond to the pseudo-random part of the nonce/counter- and 0, namely 264-1, so that an increase of 280-264-1 is obtained. Each unit corresponds to 16 bytes and hence for the counter to reach the new nonce value (or a larger one) after an encryption -discounting the case in which two encryptions are started within the same second which, apart from being rare, is dealt with by the pseudo-random part of the nonce-, one should encrypt, at least, (280-264-1)
24 bytes per second. This is more than 283 bytes/second or more than 8 yobibytes/second, were 1 yobibyte (YiB) = 280 bytes is currently the largest information storage unit, so we do not have to worry about it.
We are now ready to give the function that selects the nonce, or the initial counter or, more generally, the IV appropriate for each mode (except for ECB mode where it is not used). The encryption functions below accept the IV as an input, so that the IV may be externally generated if desired but, in case no IV value is passed to the functions, then it will be generated by the following function.
| > |
As an example, we select an IV (i.e., an initial counter in this case) for use in CTR mode. In this case, the last four bytes are 0-bytes:
| > |
The next function will be used by the encryption functions to check that the IV passed to the function is valid (i.e., a list of bytes or a hexadecimal string of valid length). The function returns an error in case the IV is not valid, otherwise it returns the IV as a list of bytes, which is the format used by the low-level encryption and decryption functions. Note also that, if the key is given as a hex string, both lower case and upper case letters will be accepted as hex digits. If, on the other hand, no argument is passed to the function, then the output is the NULL sequence, which is useful for ECB mode, where no IV is used.
| > |
Padding
The encryption function that acts on lists of bytes requires, for ECB, CBC and CFB modes, that the bit length of the plaintext be a multiple of 128 (the block size). If this is not the case then it is necessary to pad the plaintext (in order to complete a certain number of blocks) and to unpad it (once decrypted, to recover the original plaintext). The usual padding method (10i padding) consists in adding a "1" bit to mark the beginning of the padding and then completing it with as many "0" bits as needed to make its length a multiple of 128 (and, in order to avoid ambiguity, a full 128-bit block is added if the plaintext bit length is already a multiple of 128). Using 10i padding means that, since we work at the byte level and the binary expansion of 128 is 1 followed by seven zeros, we will use a byte 128 to mark the beginning of the padding and then all bytes 0 until completing the desired length (until the byte length is a multiple of 16).
| > |
The function that removes the padding of a decrypted plaintext is the following:
| > |
AES encryption and decryption functions
In this section we set up the encryption and decryption functions that will be used to encrypt/decrypt files, text, etc., as well as some auxiliary functions. We start with the functions devoted to generating keys.
Key generation
We will give a couple of functions to generate AES keys in different formats. Before defining these functions we want to mention an alternative possibility that produces highly insecure keys. This method converts a text string (which can be input through the keyboard) to a key given as a list of bytes and we mention it only to illustrate why it should never be used. The required keylength (in bits) is supplied through the second argument of the function, which will only accept one of the three allowed keylengths. If the byte keylength is n and the number of characters of the input text string is greater than or equal to n, then the bytes corresponding to the first n characters are taken, otherwise the string appended to itself as many times as necessary.
| > |
For example, we can generate a 256-bit key as follows:
| > |
This method allows the generation of keys that are easy to remember but, because of this very fact, it is not very recommendable and, as already mentioned, it should not be used. If the text string is passed to the function by typing it at the keyboard then the number of different bytes that may appear in the key is severely restricted (there are many bytes that will never appear in a string produced this way). On the other hand, if the text string is a meaningful sentence in a natural language, then the effective size of the key space is severely reduced and even more so if a string of less characters than the required byte keylength is used. This would violate a basic cryptographic principle which postulates that all keys should be equally likely and hence they should be chosen uniformly at random [Bellare-Rogaway, Katz-Lindell]. If we observe the key obtained in the previous example, we see that all its bytes (with the exception of 32 that corresponds to the space) are in the 84..116 range and, moreover, the last 12 bytes are the same as the first 12. Thus not only the key is not random but it hardly looks random!
The requirement that AES keys be selected uniformly at random is very difficult to meet (see, e.g., [Viega] for an in-depth analysis of the many difficulties that arise in practice when trying to generate secure keys by means of PRNG's). As an alternative, we will use the BBS PRNG (just by calling BBSByteGen) to generate a sequence of pseudo-random bytes of the required length. This is done by the function PseudoRandomKeyGen below, whose required input parameters are keylength (which admits only the three possible values 128, 192 and 256, corresponding to the possible bit sizes of an AES key) and seed, for the seed given either as a positive integer or a hexadecimal string. There are also two optional keyword parameters, format and primeslength, which play the same role and have the same default values as in the function BBSByteGen. As already mentioned, for security the seed should be randomly chosen among all possible seeds of sufficient length. The seed length should be large enough to prevent brute-force attacks; for example, if randomly chosen 32-bit seeds are used, an adversary could mount a successful attack by carrying out an exhaustive search over all the 232 possible seeds, which is feasible. Therefore, in order to prevent such an attack, it is advisable to choose the seed at random among all the strings of length at least 128 bits. This cannot be done with Maple but operating systems have reasonably good methods to generate random seeds or even, with patience, one can always resort to the slow method of coin tossing and use Von Neumann's trick just in case the coin is biased. Maple also has methods to generate seeds, such as those given by the functions randomize() and RandomTools:-MersenneTwister:-SetState(), but seeds taken from the system clock are hardly random and, moreover, the 32-bit seeds produced by these functions are not large enough to prevent brute-force attacks. However, we include the possibility of using Maple's automatic seed in the function PseudoRandomKeyGen, which will also work if no value is passed to the parameter seed, in which case the seed will be generated by BBSByteGen by calling RandomTools:-MersenneTwister:-SetState(). This is done only for convenience and for demonstration purposes but we insist that this method is not secure and that, for security, true random seeds (or true random AES keys) should be used. If one is going to use true random seeds, then one might as well generate a true random AES key by the same method because, anyway, the seed should have at least 128 bits as remarked above, so only in the case of 192- and 256-bit keys we would have to generate fewer random bits by using the PRNG.
| > |
For example, a 256-bit hex key is the following:
| > |
A 256-bit key generated with automatic seeding is:
| > |
The next procedure will be used by the encryption/decryption functions to check that the key passed to the function is valid (i.e., a list of bytes or a hexadecimal string of valid length). The function, which is similar to checkiv, takes as input at an AES key given either as a list of bytes or as a hexadecimal string, checks whether the key is correctly formatted and, if this is the case, it returns the key as a hexadecimal hex string.
| > |
Let's check a hexadecimal key;
| > |
File encryption and decryption
Next, we are going to give higher level functions to encrypt and decrypt files. For this we will use the previously defined functions filetobytes and bytestofile, to read a file to a list of bytes and to write a list of bytes to a file, respectively. Observe that the size of the files that can be handled by these functions is limited by the maximum list size which is 226-4. This limitation will also affect other kinds of plaintexts (like strings) and, in the case of files means that, since 220 bytes equals 1 MB, the maximum size that can be read by filetobytes is just below 64 MB. On the other hand, files close to 64 MB in size would take quite a long time to encrypt or decrypt but the implementation can easily deal, within reasonable time, with the typical files produced by word processors. The input for these functions is the name of the file to be encrypted or decrypted (as a string delimited by double quotes), the name of the file where the result of the encryption/decryption operation is to be written, the key (as a hex string or a list of bytes), the mode of operation (as a lower case string with default value "cbc"), and some keyword parameters. The keyword parameter iv (used only by the EncryptFile function) is for the IV given as a list of 16 bytes and, if no argument is passed to this parameter, then the default value makes the function to generate the IV by means of the previous function selectiv. This way, the IV may be, for example, an externally generated random IV but it can also be generated inside the function, although the automatic seeding used makes this second method insecure. The keyword parameter filecheck is used for file checking and logoutput specifies (with the same format as in bytestofile and with default value terminal) where the output log is to be written. The functions check if the file exists by default but in the subsequent tests we override this check by specifying filecheck = false in order to facilitate procedure timing.
| > |
| > |
To test these functions we use the file "testfile" that was previously generated, and the corresponding list of bytes called testlist. We encrypt "testfile" in CBC mode (the default) by using as key the list of bytes in the 0..15 range and we write the ciphertext to a file named "ctestfile". Afterwards, we decrypt this file to a file named "dtestfile". Finally, we read "dtestfile" to a list and compare this list to the original testlist. The comparison should reveal that both lists are the same and hence that the encryption/decryption process worked correctly.
| > |