cryptopals

Cryptopals Challenges
git clone https://git.pngu.org/cryptopals
Log | Files | Refs

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:
Mlib/aes.c | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mlib/aes.h | 4++++
Asrc/18_implement_ctr.c | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
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; +}