Crypto on the Command Line, Part 2: Shared-Key Encryption

AES encrypts with one key that both ends hold: the same 32 bytes lock and unlock. This post runs the shared-key scene of the Security Primitives tool with openssl enc:

  • making the key and the IV, and passing both by hand;
  • encrypting a message and a document, and decrypting them;
  • decrypting with a wrong key;
  • changing a ciphertext without the key, so that it decrypts to a different amount.

The tools on Ubuntu 26.04

The commands are shown as a terminal session:

  • $ starts a command: type what follows it;
  • > marks a line that continues the command above it, for a command too long for one line;
  • a line with neither is output. A command with no lines under it prints nothing.

openssl (3.5 on 26.04) and xxd; if either is missing:

1$ sudo apt update
2$ sudo apt install openssl xxd
3$ mkdir -p ~/crypto-lab && cd ~/crypto-lab

The key and the IV

AES-256 takes a 32-byte key. openssl rand makes random bytes, and -hex prints them as hex:

1$ openssl rand -hex 32 > purple.key
2$ openssl rand -hex 16 > msg.iv
3$ cat purple.key msg.iv
440079df4702fc8134812ba0146475a3803cd94ce8bee9aa6afe70ce9d65b2662
555057c1385fa608c6ea8649cdbd17fdb

Your values differ: they are random.

  • The key is the secret. Alice and Bob both hold purple.key; in the tool they agreed it in person.
  • The IV (initialisation vector) is 16 bytes that start the encryption off. It is not secret and travels with the ciphertext. A new one is made for every message, so the same message encrypted twice gives two different ciphertexts.

Encrypting a message

-K and -iv take the key and the IV in hex. CBC is the mode: each 16-byte block is mixed with the previous block's ciphertext before AES scrambles it, and the IV stands in for the block before the first.

1$ printf 'pay carol 10' > msg.txt
2$ openssl enc -aes-256-cbc -K $(cat purple.key) -iv $(cat msg.iv) -in msg.txt -out msg.enc
3$ wc -c msg.txt msg.enc
412 msg.txt
516 msg.enc
628 total
7$ xxd msg.enc
800000000: db0c 540f 9194 2ac2 0800 ebad 34dc 9e05  ..T...*.....4...
  • AES works on whole 16-byte blocks, so the 12-byte message was padded to 16.
  • The padding is four bytes of 04: as many bytes as were added, each holding that number.

Decrypting

-d decrypts with the same key and IV:

1$ openssl enc -d -aes-256-cbc -K $(cat purple.key) -iv $(cat msg.iv) -in msg.enc
2pay carol 10

-nopad keeps the padding, which shows it:

1$ openssl enc -d -aes-256-cbc -K $(cat purple.key) -iv $(cat msg.iv) -nopad -in msg.enc | xxd
200000000: 7061 7920 6361 726f 6c20 3130 0404 0404  pay carol 10....

Decrypting with a wrong key

AES itself cannot tell a wrong key: it turns any 16 bytes into another 16. What fails is the padding check after it:

1$ openssl enc -d -aes-256-cbc -K $(openssl rand -hex 32) -iv $(cat msg.iv) -in msg.enc
2bad decrypt
3...:error:1C800064:Provider routines:ossl_cipher_unpadblock:bad decrypt:...
  • The wrong key decrypted the block to random bytes, and their last byte was not valid padding.
  • About one time in 256 random bytes happen to end in valid padding, and the command prints garbage instead.

Encrypting a document

The licence from Part 1 encrypts the same way. Its digest, after the round trip, is the one from Part 1:

1$ cp /usr/share/common-licenses/GPL-3 doc.txt
2$ openssl enc -aes-256-cbc -K $(cat purple.key) -iv $(cat msg.iv) -in doc.txt -out doc.enc
3$ wc -c doc.enc
435152 doc.enc
5$ openssl enc -d -aes-256-cbc -K $(cat purple.key) -iv $(cat msg.iv) -in doc.enc | sha256sum
63972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986  -
  • 35,149 bytes became 35,152: the next multiple of 16.
  • The ciphertext is as long as the document, give or take a block. Eve learns the length, and nothing else.

Changing a ciphertext without the key

Mallory has no key, and she can still change what Bob reads. In CBC, after AES decrypts the first block, the IV is XORed into it, so flipping a bit of the IV flips the same bit of the message:

  • 1 is 0x31 and 9 is 0x39; they differ by 0x08.
  • The 1 of 10 is byte 10 of the message, so hex characters 20–21 of the IV.
1$ iv=$(cat msg.iv)
2$ b=$(( 0x${iv:20:2} ^ 0x08 ))
3$ iv2=${iv:0:20}$(printf %02x $b)${iv:22}
4$ echo $iv2
555057c1385fa608c6ea86c9cdbd17fdb
6$ openssl enc -d -aes-256-cbc -K $(cat purple.key) -iv $iv2 -in msg.enc
7pay carol 90
  • Bob's decryption succeeds, the padding is valid, and the message says 90.
  • Encryption hides a message. It does not show that the message was changed.
  • AES-GCM adds a tag that does: a changed ciphertext or IV fails to decrypt. TLS and SSH use it (or ChaCha20-Poly1305).
  • openssl enc refuses GCM (enc: AEAD ciphers not supported). On the command line the fix is an HMAC over the IV and the ciphertext, which Part 6 adds to this message.

A key from a password

openssl enc -aes-256-cbc -pbkdf2 -in msg.txt -out msg.enc asks for a password instead of a key:

  • PBKDF2 hashes the password with a random salt many thousand times, and makes the key and the IV from the result;
  • the salt is written at the start of the output file, so decrypting needs only the password.

This post gives the key and the IV by hand so that each is visible.

Getting the key to the other end

Alice and Bob met to agree purple.key. Sent over the wire, the key would reach Eve too, and she would read everything locked with it. Part 3 makes the key pair that solves this.