commit ca427481a713d09da50b0b69df82bd927031c6ed
parent eace2b19a5f0ef8349783ae9a40aaa05c0638674
Author: Jack Sangdahl <jack@pngu.org>
Date: Sat, 18 Oct 2025 21:13:24 -0600
challenge 18: implement ctr, the stream cipher mode
Diffstat:
3 files changed, 120 insertions(+), 8 deletions(-)
diff --git a/lib/aes.c b/lib/aes.c
@@ -56,8 +56,6 @@ struct bytes pkcs7_validate_strip(unsigned char *data, size_t data_len) {
struct bytes aes_encrypt_ecb(char *data, const size_t data_len,
const unsigned char *key, const size_t key_len) {
- if (key_len != 16 && key_len != 24 && key_len != 32)
- return (struct bytes){NULL, 0};
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
const EVP_CIPHER *cipher;
switch (key_len) {
@@ -70,6 +68,8 @@ struct bytes aes_encrypt_ecb(char *data, const size_t data_len,
case 32:
cipher = EVP_aes_256_ecb();
break;
+ default:
+ return (struct bytes){NULL, 0};
}
if (EVP_EncryptInit_ex(ctx, cipher, NULL, key, NULL) != 1) {
EVP_CIPHER_CTX_free(ctx);
@@ -105,8 +105,6 @@ struct bytes aes_encrypt_ecb(char *data, const size_t data_len,
struct bytes aes_decrypt_ecb(const unsigned char *data, size_t data_len,
const unsigned char *key, size_t key_len) {
- if (key_len != 16 && key_len != 24 && key_len != 32)
- return (struct bytes){NULL, 0};
if (data_len == 0 || data_len % AES_BLOCK_SIZE)
return (struct bytes){NULL, 0};
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
@@ -121,6 +119,8 @@ struct bytes aes_decrypt_ecb(const unsigned char *data, size_t data_len,
case 32:
cipher = EVP_aes_256_ecb();
break;
+ default:
+ return (struct bytes){NULL, 0};
}
if (EVP_DecryptInit_ex(ctx, cipher, NULL, key, NULL) != 1) {
EVP_CIPHER_CTX_free(ctx);
@@ -151,8 +151,6 @@ struct bytes aes_decrypt_ecb(const unsigned char *data, size_t data_len,
struct bytes aes_encrypt_cbc(char *data, const size_t data_len,
const unsigned char *key, const size_t key_len,
unsigned char *iv) {
- if (key_len != 16 && key_len != 24 && key_len != 32)
- return (struct bytes){NULL, 0};
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
const EVP_CIPHER *cipher;
switch (key_len) {
@@ -165,6 +163,8 @@ struct bytes aes_encrypt_cbc(char *data, const size_t data_len,
case 32:
cipher = EVP_aes_256_cbc();
break;
+ default:
+ return (struct bytes){NULL, 0};
}
if (EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv) != 1) {
EVP_CIPHER_CTX_free(ctx);
@@ -201,8 +201,6 @@ struct bytes aes_encrypt_cbc(char *data, const size_t data_len,
struct bytes aes_decrypt_cbc(unsigned char *data, size_t data_len,
const unsigned char *key, size_t key_len,
unsigned char *iv) {
- if (key_len != 16 && key_len != 24 && key_len != 32)
- return (struct bytes){NULL, 0};
if (!data_len || data_len % AES_BLOCK_SIZE)
return (struct bytes){NULL, 0};
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
@@ -217,6 +215,8 @@ struct bytes aes_decrypt_cbc(unsigned char *data, size_t data_len,
case 32:
cipher = EVP_aes_256_cbc();
break;
+ default:
+ return (struct bytes){NULL, 0};
}
if (EVP_DecryptInit_ex(ctx, cipher, NULL, key, iv) != 1) {
EVP_CIPHER_CTX_free(ctx);
@@ -250,3 +250,56 @@ char *aes_oracle(unsigned char *encoded, size_t block_size) {
else
return "CBC";
}
+
+union aes_ctr_counter {
+ unsigned char buf[AES_BLOCK_SIZE];
+ struct nonce_counter {
+ uint64_t nonce;
+ uint64_t counter;
+ } nc;
+};
+
+struct bytes aes_ctr(const unsigned char *data, size_t data_len,
+ const unsigned char *key, size_t key_len,
+ const uint64_t nonce) {
+ const EVP_CIPHER *cipher;
+ switch (key_len) {
+ case 16:
+ cipher = EVP_aes_128_ecb();
+ break;
+ case 24:
+ cipher = EVP_aes_192_ecb();
+ break;
+ case 32:
+ cipher = EVP_aes_256_ecb();
+ break;
+ default:
+ return (struct bytes){NULL, 0};
+ }
+ EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+ if (EVP_EncryptInit_ex(ctx, cipher, NULL, key, NULL) != 1) {
+ EVP_CIPHER_CTX_free(ctx);
+ return (struct bytes){NULL, 0};
+ }
+ EVP_CIPHER_CTX_set_padding(ctx, 0);
+ unsigned char *decrypted =
+ (unsigned char *)calloc(data_len + 1, sizeof(unsigned char));
+ unsigned char keystream[AES_BLOCK_SIZE];
+ union aes_ctr_counter ctr_counter = {.nc = {.nonce = nonce, .counter = 0}};
+ for (size_t count = 0; count < data_len; count += AES_BLOCK_SIZE) {
+ size_t block_len =
+ (data_len - count < AES_BLOCK_SIZE) ? data_len - count : AES_BLOCK_SIZE;
+ ctr_counter.nc.counter = count / AES_BLOCK_SIZE;
+ int tmp_len = 0;
+ if (EVP_EncryptUpdate(ctx, keystream, &tmp_len, ctr_counter.buf,
+ AES_BLOCK_SIZE) != 1) {
+ free(decrypted);
+ EVP_CIPHER_CTX_free(ctx);
+ return (struct bytes){NULL, 0};
+ }
+ for (size_t i = 0; i < block_len; i++)
+ decrypted[count + i] = data[count + i] ^ keystream[i];
+ }
+ EVP_CIPHER_CTX_free(ctx);
+ return (struct bytes){decrypted, data_len};
+}
diff --git a/lib/aes.h b/lib/aes.h
@@ -30,3 +30,7 @@ struct bytes aes_decrypt_cbc(unsigned char *data, size_t data_len,
const unsigned char *key, size_t key_len,
unsigned char *iv);
char *aes_oracle(unsigned char *encoded, size_t block_size);
+
+struct bytes aes_ctr(const unsigned char *data, size_t data_len,
+ const unsigned char *key, size_t key_len,
+ const uint64_t nonce);
diff --git a/src/18_implement_ctr.c b/src/18_implement_ctr.c
@@ -0,0 +1,55 @@
+// Implement CTR, the stream cipher mode
+#include "aes.h"
+#include "base64.h"
+
+/*
+ * the string:
+ * L77na/nrFsKvynd6HzOoG7GHTLXsTVu9qvY/2syLXzhPweyyMTJULu/6/kXX0KSvoOLSFQ==
+ * ...decrypts to something approximating english in ctr mode, which is an aes
+ block cipher mode that turns aes into a stream cipher, with the following
+ parameters:
+ * key=YELLOW SUBMARINE
+ * nonce=0
+ * format=64 bit unsigned little endian nonce,
+ * 64 bit little endian block count (byte count / 16)
+ * ctr mode is very simple. instead of encrypting the plaintext, ctr mode
+ encrypts a running counter, producing a 16 byte block of keystream, which is
+ xor'd against the plaintext. for instance, the first 16 byts of a message with
+ these parameters:
+ * keystream = AES("YELLOW
+ SUBMARINE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
+ * ...for the next 16 bytes:
+ * keystream = AES("YELLOW
+ SUBMARINE\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00")
+ * ...and then:
+ * keystream = AES("YELLOW
+ SUBMARINE\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00")
+ * ctr mode does not require padding; run you run out of plaintext, you just
+ stop xor'ing keystream and stop generating keystream. decryption is identical
+ to encryption. generate the same keystream, xor, and recover the plaintext.
+ decrypt the string at the top of this function, then use your ctr function to
+ encrypt and decrypt other things. this is the only block cipher mode that
+ matters in good code. most modern cryptography relies on ctr mode to adapat
+ block ciphers into stream ciphers, because most of what we want to encrypt is
+ better described as a stream than a sequence of blocks. daneil bernstein once
+ quipped to phil rogaway that cood cryptosystems don't need the "decrypt"
+ transforms. constructions like ctr are what he was talking about
+*/
+
+int main(void) {
+ char *input = "L77na/nrFsKvynd6HzOoG7GHTLXsTVu9qvY/2syLXzhPweyyMTJULu/6/"
+ "kXX0KSvoOLSFQ==";
+ char *key = "YELLOW SUBMARINE";
+ char *decoded = NULL;
+ int decoded_len = base64_decode(input, strlen(input), &decoded);
+ uint64_t nonce = 0;
+ struct bytes res = aes_ctr((unsigned char *)decoded, decoded_len,
+ (const unsigned char *)key, strlen(key), nonce);
+ if (res.data) {
+ printf("%s", res.data);
+ free(res.data);
+ }
+ printf("\n");
+ free(decoded);
+ return 0;
+}