Handling UDP Traffic with Counter Mode

158 later. If you are using an earlier version of OpenSSL, you can define this macro to the value 64, which is the largest possible block size in 0.9.6c and earlier. Example 6-6. Using the example encryption and decryption functions int mainint argc, char argv[] { EVP_CIPHER_CTX ctx; char key[EVP_MAX_KEY_LENGTH]; char iv[EVP_MAX_IV_LENGTH]; char ct, out; char final[EVP_MAX_BLOCK_LENGTH]; char str[] = 123456789abcdef; int i; if seed_prng { printfFatal Error Unable to seed the PRNG\n; abort; } select_random_keykey, EVP_MAX_KEY_LENGTH; select_random_iviv, EVP_MAX_IV_LENGTH; EVP_EncryptInitctx, EVP_bf_cbc, key, iv; ct = encrypt_examplectx, str, strlenstr, i; printfCiphertext is d bytes.\n, i; EVP_DecryptInitctx, EVP_bf_cbc, key, iv; out = decrypt_examplectx, ct, 8; printfDecrypted: s\n, out; out = decrypt_examplectx, ct + 8, 8; printfDecrypted: s\n, out; if EVP_DecryptFinalctx, final, i { printfPadding incorrect.\n; abort; } final[i] = 0; printfDecrypted: s\n, final; } If we run this example, the first time we try to output decrypted plaintext, we will see nothing, even though we fed the decryption routine a full block of data. Note that when we feed the remainder of the data, we are passing in eight bytes, because the encryption routine padded the data to a block-aligned length. At that point, one block will be output, and the second block will be held in reserve until there is more data, or until EVP_DecryptFinal is called. If we were to change the cipher to RC4, the above example would compile, but give slightly incorrect output. The length of the encrypted text would be 15 bytes, not 16, due to the lack of padding. As a result, passing in 16 bytes of ciphertext to the decrypt routine will cause a block of garbage to be decrypted. Changing the third argument in the second call to decrypt_example to seven fixes the problem.

6.2.6 Handling UDP Traffic with Counter Mode

It is almost never desirable to use ECB mode for encryption. Schneier recommends it for encrypting other keys or other situations in which the data is short and random. However, this 159 advice applies only when the key length is no larger than the cipher block length. Additionally, if you wish to include an integrity check alongside your data which is almost always a good idea, ECB again becomes undesirable. Another occasion when people think to use ECB is when encrypting datagrams to be sent over a UDP connection. The problem is that packets may show up out of order, or not at all. All basic cipher modes besides ECB require an ordered, reliable stream of data. CBC mode is much better suited to handling UDP traffic. A single key can be used to encrypt all data, but each packet gets initialized with a randomly chosen IV, which can be sent alongside the encrypted data. Counter mode is slightly better suited than CBC for encrypting UDP traffic. One advantage of counter mode over OFB and other modes that simulate stream ciphers is that it can inherently survive data loss—the current state of the counter can be passed in the clear each time a packet is sent. Another major advantage of counter mode is that it allows for parallelization, which is not supported by any of the default modes, except for ECB a CBC-based approach could parallelize at the packet level, but could not parallelize the processing of data within a single packet. Another feature unique to OFB is that most of the work can be done offline. That is, you can generate a keystream before there is even data available to encrypt, while you have spare CPU cycles. Additionally, because counter mode essentially supports arbitrarily jumping around a data stream, it can enable file encryption where random access to the data is still possible. Moreover, in theory, counter mode should be able to handle the UDP encryption problem without a rekey for every packet. However, current limitations of the OpenSSL library make that goal difficult to achieve, although the forthcoming 0.9.7 release will fix the problem. OpenSSL currently doesnt support counter mode, but it is simple to implement yourself. Counter mode effectively turns a block cipher into a stream cipher. For each block of plaintext, we encrypt a counter of exactly one block in length, which gets XORd with the corresponding block of plaintext. Then, the counter is incremented in some way. Increasing the counter by one or using a PRNG to increment the counter both work. The PRNG doesnt even need to be cryptographically secure, though it must not repeat items before enumerating all possible values. We can prevent the attacker from seeing the counter if need be. Lets say that, in addition to an agreed-upon key, we share a second secret of the same length. Additionally, a sequence number is sent in the clear with each packet. To generate the counter used to encrypt the first data block in that packet, we concatenate the second shared secret with the sequence number, and cryptographically hash that. We show how to use cryptographic hashes in Chapter 7 . Counter mode is easily implemented by keeping track of a counter that is encrypted in ECB mode once for each block. In our UDP example, each packet would have several blocks. Only one counter should need to be sent per packet, because the receiving end should be able to recreate the counter values for the subsequent blocks. Example 6-7 shows an implementation of counter mode that will work for any cipher in ECB mode. There is just one function, counter_encrypt_or_decrypt , as encryption and decryption are identical in counter mode. int counter_encrypt_or_decryptEVP_CIPHER_CTX ctx, char pt, char ct, int len, unsigned char counter; ctx The cipher context to use. TE AM FL Y Team-Fly ® 160 pt A buffer containing the data to be encrypted or decrypted. ct A buffer that will contain the encrypted or decrypted data. len The number of bytes from the input buffer, pt , to process. counter The counter. In this example, the counter must be the same size as the block size of the cipher were using we query the ciphers block size to specify that value. Cipher block sizes are usually bigger than 32 bits, so it is best to represent the counter as an array of unsigned bytes. This function also modifies the counter in place as it processes blocks. To increment the counter, we simply increment the leftmost byte until it rolls over, at which point we increment the byte next to it, and so on. Note that its extremely important not to reuse counter values with a single key, not even across a reboot. If there is any chance of reusing a counter, be sure to change the key. The next function will return -1 if it determines that the cipher is not in ECB mode we need to use this mode in order to implement counter mode; do not take this example as an endorsement of ECB in general. This is accomplished by calling EVP_CIPHER_CTX_mode , which returns a number. If that number is equal to the constant EVP_CIPH_ECB_MODE , then we know the cipher was initialized improperly. See the documentation on the books web site for a list of other valid mode constants. Note that the code in Example 6-7 should not be used without also using a MAC to provide data integrity, as discussed in Chapter 7 . Example 6-7. Encryption and decryption using counter mode int counter_encrypt_or_decryptEVP_CIPHER_CTX ctx, char pt, char ct, int len, unsigned char counter { int i, j, where = 0, num, bl = EVP_CIPHER_CTX_block_sizectx; char encr_ctrs[len + bl]; Encrypted counters. if EVP_CIPHER_CTX_modectx = EVP_CIPH_ECB_MODE return -1; = is correct, so that we handle any possible non-aligned data. for i = 0; i = len bl; i++ { Encrypt the current counter. EVP_EncryptUpdatectx, encr_ctrs[where], num, counter, bl; where += num; Increment the counter. Remember its an array of single characters for j = 0; j bl sizeofchar; j++ { if ++counter[j] 161 break; } } XOR the key stream with the first buffer, placing the results in the second buffer. for i = 0; i len; i++ ct[i] = pt[i] encr_ctrs[i]; return 1; Success. } As we discussed, the above example requires the state of the counter to be kept externally. Another option is to make a COUNTER_CTX data type that could hold a pointer to the underlying cipher context and the current state of the counter. However, this less-abstract API makes it easier to use in a situation in which the counter may need to be reset explicitly after desynchronization, such as when dealing with UDP traffic.

6.3 General Recommendations

So far, we have looked at how to use the EVP API to perform encryption and decryption. While weve examined some basic examples, we havent looked at real-world examples. The primary reason is that we dont recommend using symmetric key encryption without a MAC; in cases where an attacker has read access to data, you should be worried about her also gaining write access. Therefore, we give real-world examples of using encryption along with MACs in Chapter 7 . When you do use MACs, use them with independent keys that is, do not MAC with your encryption keys and use them to validate all of the data, including anything sent in the clear. In particular, when using counter mode, make sure to include the counter value in the data that you include in the MAC calculation. Extending this recommendation, whenever you design protocols based on encryption, avoid any communication in plaintext at all, and MAC anything that does need to be plaintext. In particular, if you do plaintext protocol negotiation before a key exchange, you should MAC the payloads of each message in the negotiation, so that after a key is agreed upon, both sides can validate the negotiation. For example, lets say that a client connects to the server and immediately asks to speak Version 2 of protocol X, and receives a response saying the server speaks only the insecure Version 1. If it turns out that a man in the middle told the server the client wanted Version 1, and fakes the response from the server, then neither the client nor the server would notice, and would wind up speaking an insecure version of the protocol. Another recommendation is to design your protocol to be fault-tolerant. In particular, when using MACs to validate messages, be prepared to perform reasonable error handling if the data doesnt authenticate on the receiving end. If your protocol fails in such a situation, denial of service attacks will be quite easy. Finally, be sure to protect yourself against dictionary and capture-replay type attacks. One thing you can do is add sequence numbers to the beginning of each message. Its also a good idea to place unique information per-user or per-connection near the beginning of each message.