You want to do encryption or decryption without the hassle of worrying about choosing an encryption algorithm, performing an integrity check, managing a nonce, and so on.
Use the following "Encryption Queue" implementation, which relies on the reference CWC mode implementation (discussed in Recipe 5.10) and the key derivation function from Recipe 4.11.
Be sure to take into account the fact that functions in this API can fail, particularly the decryption functions. If a decryption function fails, you need to fail gracefully. In Recipe 9.12, we discuss many issues that help ensure robust network communication that we don't cover here.
This recipe provides an easy-to-use interface to symmetric encryption. The two ends of communication must set up cipher queues in exactly the same configuration. Thereafter, they can exchange messages easily until the queues are destroyed.
This code relies on the reference CWC implementation discussed in Recipe 5.10. We use CWC mode because it gives us both encryption and integrity checking using a single key with a minimum of fuss.
We add a new data type,
SPC_CIPHERQ
, which is responsible for keeping track of
queue state. Here's the declaration of the
SPC_CIPHERQ
data type:
typedef struct { cwc_t ctx; unsigned char nonce[SPC_BLOCK_SZ]; } SPC_CIPHERQ;
SPC_CIPHERQ
objects are initialized by calling
spc_cipherq_setup(
)
, which requires the code from Recipe 5.5, as
well as an implementation of the randomness API discussed in Recipe
11.2:
#include <stdlib.h> #include <string.h> #include <cwc.h> #define MAX_KEY_LEN (32) /* 256 bits */ size_t spc_cipherq_setup(SPC_CIPHERQ *q, unsigned char *basekey, size_t keylen, size_t keyuses) { unsigned char dk[MAX_KEY_LEN]; unsigned char salt[5]; spc_rand(salt, 5); spc_make_derived_key(basekey, keylen, salt, 5, 1, dk, keylen); if (!cwc_init(&(q->ctx), dk, keylen * 8)) return 0; memcpy(q->nonce, salt, 5); spc_memset(basekey, 0, keylen); return keyuses + 1; }
The function has the following arguments:
q
SPC_CIPHERQ
context object.
basekey
Shared key used by both ends of communication (the "base key" that will be used to derive session keys).
keylen
Length of the shared key in bytes, which must be 16, 24, or 32.
keyuses
Indicates how many times the current key has been used to initialize
a SPC_CIPHERQ
object. If you are going to reuse
keys, it is important that this argument be used properly.
On error, spc_cipherq_setup()
returns 0.
Otherwise, it returns the next value it would expect to receive for
the keyuses
argument. Be sure to save this value
if you ever plan to reuse keys.
Note also that basekey
is erased upon successful
initialization.
Every time you initialize an SPC_CIPHERQ
object, a
key specifically for use with that queue instance is generated, using
the basekey
and the keyuses
arguments. To derive the key, we use the key derivation function
discussed in Recipe 4.11. Note that this is useful when two parties
share a long-term key that they wish to keep reusing. However, if you
exchange a session key at connection establishment (i.e., using one
of the techniques from Chapter 8), the key
derivation step is unnecessary, because reusing {key, nonce} pairs is
already incredibly unlikely in such a situation.
Both communicating parties must initialize their queue with identical parameters.
When you're done with a queue, you should deallocate
internally allocated memory by calling spc_cipherq_cleanup(
)
:
void spc_cipherq_cleanup(SPC_CIPHERQ *q) { spc_memset(q, 0, sizeof(SPC_CIPHERQ)); }
Here are implementations of the encryption and decryption operations (including a helper function), both of which return a newly allocated buffer containing the results of the appropriate operation:
static void increment_counter(SPC_CIPHERQ *q) { if (!++q->nonce[10]) if (!++q->nonce[9]) if (!++q->nonce[8]) if (!++q->nonce[7]) if (!++q->nonce[6]) ++q->nonce[5]; } unsigned char *spc_cipherq_encrypt(SPC_CIPHERQ *q, unsigned char *m, size_t mlen, size_t *ol) { unsigned char *ret; if (!(ret = (unsigned char *)malloc(mlen + 16))) { if (ol) *ol = 0; return 0; } cwc_encrypt(&(q->ctx), 0, 0, m, mlen, q->nonce, ret); increment_counter(q); if (ol) *ol = mlen + 16; return ret; } unsigned char *spc_cipherq_decrypt(SPC_CIPHERQ *q, unsigned char *m, size_t mlen, size_t *ol) { unsigned char *ret; if (!(ret = (unsigned char *)malloc(mlen - 16))) { if (ol) *ol = 0; return 0; } if (!cwc_decrypt(&(q->ctx), 0, 0, m, mlen, q->nonce, ret)) { free(ret); if (ol) *ol = 0; return 0; } increment_counter(q); if (ol) *ol = mlen - 16; return ret; }
The functions spc_cipherq_encrypt(
)
and spc_cipherq_decrypt(
)
each take four arguments:
q
SPC_CIPHERQ
object to use for encryption or
decryption.
m
Message to be encrypted or decrypted.
mlen
Length of the message to be encrypted or decrypted, in bytes.
ol
The number of bytes returned from the encryption or decryption
operation is stored in this integer pointer. This may be
NULL
if you don't need the
information. The number of bytes returned will always be the message
length plus 16 bytes for encryption, or the message length minus 16
bytes for decryption.
These functions don't check for counter rollover because you can use this API to send over 250 trillion messages with a single key, which should be adequate for any use.
Instead of using such a large counter, it is a good idea to use only five bytes for the counter and initialize the rest with a random salt value. The random salt helps prevent against a class of problems in which the attacker amortizes the cost of an attack by targeting a large number of possible keys at once. In Recipe 9.12, we show a similar construction that uses both a salt and a counter in the nonce.
If you do think you might send more messages under a single key, be sure to rekey in time. (This scheme is set up to handle at least four trillion keyings with a single base key.)
In the previous code, the nonces are separately managed by both parties in the communication. They each increment by one when appropriate, and will fail to decrypt a message with the wrong nonce. Thus, this solution prevents capture replay attacks and detects message drops or message reordering, all as a result of implicit message numbering. Some people like explicit message numbering and would send at least a message number, if not the entire nonce, with each message (though you should always compare against the previous nonce to make sure it's increasing). In addition, if there's a random portion to the nonce as we suggested above, the random portion needs to be communicated to both parties. In Recipe 9.12, we send the nonce explicitly with each message, which helps communicate the portion randomly selected at connection setup time.
It's possible to mix and match calls to
spc_cipherq_encrypt(
)
and spc_cipherq_decrypt(
)
using a single context. However, if you want
to use this API in this manner, do so only if the communicating
parties send messages in lockstep. If parties can communicate
asynchronously (that is, without taking turns), there is the
possibility for a race condition in which the
SPC_CIPHERQ
states on each side of the
communication get out of sync, which will needlessly cause decryption
operations to fail.
If you need to perform asynchronous communication with an
infrastructure like this, you could use two
SPC_CIPHERQ
instances, one where the client
encrypts messages for the server to decrypt, and another where the
server encrypts messages for the client to decrypt.
The choice you need to make is whether each
SPC_CIPHERQ
object should be keyed separately or
should share the same key. Sharing the same key is possible, as long
as you ensure that the same {key, nonce} pair is never reused. The
way to do this is to manage two sets of nonces that can never
collide. Generally, you do this by setting the high bit of the nonce
buffer to 1 in one context and 0 in another context.
Here's a function that takes an existing context that has been set up, but not otherwise used, and turns it into two contexts with the same key:
void spc_cipherq_async_setup(SPC_CIPHERQ *q1, SPC_CIPHERQ *q2) { memcpy(q2, q1, sizeof(SPC_CIPHERQ)); q1->nonce[0] &= 0x7f; /* The upper bit of q1's nonce is always 0. */ q2->nonce[0] |= 0x80; /* The upper bit of q2's nonce is always 1. */ }
We show a similar trick in which we use only one abstraction in Recipe 9.12.