DEV Community

Moritz Höppner
Moritz Höppner

Posted on

The Counter Mode - manually!

The counter mode of operation (CTR) is defined in the NIST Special Publication 800-38A, pp. 15-16. I'd summarize it like this: The CTR mode transforms any block cipher into a stream cipher. Encryption and decryption are the same function. They encrypt a sequence of counter blocks and XOR the result with the plaintext (for encryption) or the ciphertext (for decryption).

My goal here is encrypting a plaintext with AES-256-CTR. And I want to do the CTR part manually, i.e. in my shell only with the usual tools like hexdump, basenc and dd. To encrypt the counter blocks, I'll need OpenSSL - but not in CTR mode!

The shell is probably not the best tool for something like this. Especially XORing data is a little awkward. Or maybe I just think so because I'm certainly not particularly good at shell programming. However, for now I'll stay with the shell and won't switch to a language like Ruby or Go. This way, you can follow without having to install an interpreter or compiler for my preferred language.

Preparation

Let's say, we want to encrypt the plaintext Lorem ipsum dolor sit amet with AES-256-CTR. The plaintext in ASCII encoding consist of two 128-bit blocks:

$ P="Lorem ipsum dolor sit amet"
$ echo -n $P | hexdump -C
00000000  4c 6f 72 65 6d 20 69 70  73 75 6d 20 64 6f 6c 6f  |Lorem ipsum dolo|
00000010  72 20 73 69 74 20 61 6d  65 74                    |r sit amet|
0000001a
Enter fullscreen mode Exit fullscreen mode

To encrypt them in CTR, we start with two counter blocks, which we save in shell variables:

$ T1=00000000000000000000000000000000
$ T2=00000000000000000000000000000001
Enter fullscreen mode Exit fullscreen mode

The next step is to encrypt them with AES-256. Since both T1 and T2 are exactly one block, we can apply the AES cipher function directly. To do this with OpenSSL, we choose the ECB mode, which simply applies AES block by block. Let's save a 256-bit key in a shell variable and then encrypt the counter blocks:

$ key=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
$ O1=$(echo -n $T1 | basenc --base16 -d | openssl enc -aes-256-ecb -K $key -nopad)
$ O2=$(echo -n $T2 | basenc --base16 -d | openssl enc -aes-256-ecb -K $key -nopad)
Enter fullscreen mode Exit fullscreen mode

We need the -nopad flag here, because otherwise OpenSSL will add a complete padding block to T1 and T2, respectively.

Now, we split the plaintext in blocks:

$ P1=$(echo -n $P | dd bs=16 skip=0 count=1 status=none)
$ P2=$(echo -n $P | dd bs=16 skip=1 count=1 status=none)
Enter fullscreen mode Exit fullscreen mode

Ok, so the ciphertext will be P1 XOR O1 and P2 XOR O2. To be precise, the second ciphertext block is actually not P2 XOR O2, because P2 has only 10 bytes and O2 has 16 bytes. The CTR specification states that the last ciphertext block (which may not be a complete block) Cn is calculated like this:

Cn = Pn XOR MSB_u(On)
Enter fullscreen mode Exit fullscreen mode

Here, u is the length of Pn and MSB_u(On) are the u most significant bytes of On, i.e. the u left-most bytes. In our case, u is 10, so we only need the first 10 bytes of O2:

$ O2_MSB=$(echo -n $O2 | dd bs=10 count=1 status=none)
Enter fullscreen mode Exit fullscreen mode

Okay. Now, we need to calculate P1 XOR O1 and P2 XOR O2_MSB. How do we do that? We could look at the hexdumps, calculate the binary representations and do the XORing by hand. But of course it'd be nice to do it programmatically. As indicated, to do that in the shell is a little awkward.

XOR as shell function

We can XOR to integers (base 10) like this:

$ echo $((6 ^ 5))
3
# That's right:
#     0110 (6)
#     0101 (5)
# --------
# XOR 0011 (3)
Enter fullscreen mode Exit fullscreen mode

To use this syntax, we first convert our data to a hexadecimal representation and then interpret this representation as one number and transform it to base 10:

$ a=a
# $a now contains the string 'a', i.e. 0x61
$ a_hex=$(echo -n $a | basenc --base16)
$ echo $a_hex
61
# Great. 0x61 in base 10 is 6*16 + 1 = 97.
$ echo $((16#$a_hex))
97
# Woohoo!
Enter fullscreen mode Exit fullscreen mode

This way, we can transform our data into base 10 integers and XOR them. Afterwards, we must transform the integer back to raw data. We can transform a base 10 integer into its hexadecimal representation with printf and then decode it with basenc:

$ input=97
$ input_hex=$(printf "%02x" $input)
$ echo $input_hex
61
$ input_raw=$(echo -n $input_hex | basenc --base16 -d)
$ echo $input_raw
a
Enter fullscreen mode Exit fullscreen mode

A side note: We can convert an integer to its hexadecimal representation with printf "%x" $input. However, this does not necessarily output complete bytes. For example:

$ printf "%x" 3
3
Enter fullscreen mode Exit fullscreen mode

But basenc expects complete bytes with two characters each, so we must pad the output with zeros:

$ printf "%02x" 3
03
Enter fullscreen mode Exit fullscreen mode

Okay, with these ingredients, we can implement an XOR function:

function xor() {
  op1_hex=$(echo -n $1 | basenc --base16)
  op1_dec=$(echo $((16#$op1_hex)))

  op2_hex=$(echo -n $2 | basenc --base16)
  op2_dec=$(echo $((16#$op2_hex)))

  xor_dec=$(echo $(($op1_dec ^ $op2_dec)))

  xor_hex=$(printf "%02x" $xor_dec)
  xor_raw=$(echo -n $xor_hex | basenc --base16 -d)

  echo -n $xor_raw
}
Enter fullscreen mode Exit fullscreen mode

When we test this function, we may not see an output because it has no printable characters. So we must pipe it to hexdump or basenc:

$ xor e f | basenc --base16
03
Enter fullscreen mode Exit fullscreen mode

The e character is encoded as 0x65, the f character is encoded as 0x66. 6 XOR 6 is 0, 5 XOR 6 is 3. It works!

Unfortunately, there is still a problem:

$ xor $P1 $O1
xor:2: number truncated after 17 digits: 4C6F72656D20697073756D20646F6C6F
xor:5: number truncated after 16 digits: F29000B62A499FD0A9F39A6ADD2E7780
Enter fullscreen mode Exit fullscreen mode

Apparently, $((16#...)) cannot handle numbers of the size we need here. A simple solution that works for us is to split the arguments into bytes and XOR them separately:

function xor() {
  op_length=$(echo -n $1 | wc -c)

  for i in $(seq 1 $op_length)
  do
    op1_byte=$(echo -n $1 | dd bs=1 count=1 skip=$((i-1)) status=none)
    op2_byte=$(echo -n $2 | dd bs=1 count=1 skip=$((i-1)) status=none)

    op1_hex=$(echo -n $op1_byte | basenc --base16)
    op1_dec=$(echo -n $((16#$op1_hex)))

    op2_hex=$(echo -n $op2_byte | basenc --base16)
    op2_dec=$(echo -n $((16#$op2_hex)))

    xor_dec=$(echo -n $(($op1_dec ^ $op2_dec)))

    xor_hex=$(printf "%02x" $xor_dec)
    xor_raw=$(echo -n $xor_hex | basenc --base16 -d)

    if [ $i = 1 ]; then
      result=$xor_raw
    else
      result="$result$xor_raw"
    fi
  done

  echo -n $result
}
Enter fullscreen mode Exit fullscreen mode

Not an elegant solution, but good enough for the CTR demonstration I'm doing here.

Back to CTR

Now we can calculate the ciphertext:

$ C1=$(xor $P1 $O1)
$ C2=$(xor $P2 $O2_MSB)
$ C="$C1$C2"
$ echo -n $C | hexdump -C
00000000  be ff 72 d3 47 69 f6 a0  da 86 f7 4a b9 41 1b ef  |..r.Gi.....J.A..|
00000010  82 7d 05 c7 3e 99 fe 88  c3 82                    |.}..>.....|
0000001a
Enter fullscreen mode Exit fullscreen mode

Okay good, looks like gibberish. But how do we know if $C is actually the correct AES-256-CTR ciphertext?

Well, we can calculate it with OpenSSL and compare:

$ echo -n $P | openssl enc \
    -aes-256-ctr \
    -K $key \
    -iv 00000000000000000000000000000000 \
    | hexdump -C
00000000  be ff 72 d3 47 69 f6 a0  da 86 f7 4a b9 41 1b ef  |..r.Gi.....J.A..|
00000010  82 7d 05 c7 3e 99 fe 88  c3 82                    |.}..>.....|
0000001a
Enter fullscreen mode Exit fullscreen mode

Yep, we did good. :)

I said above that encryption and decryption are the same function. We can decrypt the ciphertext by XORing the encrypted counter blocks again:

$ echo $(xor $C1 $O1)$(xor $C2 $O2_MSB)
Lorem ipsum dolor sit amet
Enter fullscreen mode Exit fullscreen mode

Beautiful!

But what's up with the initialization vector? Why just 16 zero bytes? And what does "initialization vector" even mean in the context of CTR? The NIST spec doesn't speak of IVs. Let's have a look at OpenSSL's implementation of CTR!

OpenSSL's implementation of CTR

The relevant function is CRYPTO_ctr128_encrypt. When we ignore the lines that are contingent on the OPENSSL_SMALL_FOOTPRINT and STRICT_ALIGNMENT flags, the function becomes pretty simple:

void CRYPTO_ctr128_encrypt(const unsigned char *in, unsigned char *out,
                           size_t len, const void *key,
                           unsigned char ivec[16],
                           unsigned char ecount_buf[16], unsigned int *num,
                           block128_f block)
{
  unsigned int n;
  size_t l = 0;

  n = *num;

  while (l < len) {
    if (n == 0) {
      (*block) (ivec, ecount_buf, key);
      ctr128_inc(ivec);
    }
    out[l] = in[l] ^ ecount_buf[n];
    ++l;
    n = (n + 1) % 16;
  }

  *num = n;
}
Enter fullscreen mode Exit fullscreen mode

The comment above the function states that *num and ecount_buf must be initialized with zeros. So n starts at 0. The while loop iterates over the input bytes that belong either to the plaintext (if we are encrypting) or to the ciphertext (if we are decrypting). The first thing that happens in the loop is that the IV ivec is encrypted with the given block cipher and the result is written to ecount_buf. The IV is then incremented. A look at ctr128_inc in the same file shows that this basically means that 1 is added to the IV. Then the first byte of the input is XORed with the first byte of the encrypted IV. In the next 15 iterations, the next input bytes are XORed with the next bytes of the encrypted IV. In the 17th iteration, n is 0 again. So now the incremented IV is encrypted, and the 17th input byte is then XORed with the first input byte of the result. And so on.

So when we give OpenSSL an "initialization vector" for CTR, this is the first counter block. Given any counter block, the subsequent counter block is generated by adding 1. This is why we could reproduce our encryption result with the counter blocks T1 and T2 by passing the zero IV to OpenSSL.

How to choose counter blocks

As you might suspect, it is not a very good idea to start the counter blocks with 0 by default. Or, in other words, to set the IV to 0 by default. The NIST spec states in Appendix B (p. 18): "The specification of the CTR mode requires a unique counter block for each plaintext block that is ever encrypted under a given key, across all messages." The authors then give some hints on how this can be achieved. I won't go into these details here. For now, I just wanted to understand the CTR algorithm itself and how it can be implemented in the shell.

Top comments (0)