A substitution cipher in C

To finish off my first week of learning programming, I’ve completed another task which summed up pretty much everything I’ve learned so far about the basics: variables, operators, loops, functions, arrays, and such.

Here is the problem description:

In a substitution cipher, we “encrypt” (i.e., conceal in a reversible way) a message by replacing every letter with another letter. To do so, we use a key: in this case, a mapping of each of the letters of the alphabet to the letter it should correspond to when we encrypt it. To “decrypt” the message, the receiver of the message would need to know the key, so that they can reverse the process: translating the encrypt text (generally called ciphertext) back into the original message (generally called plaintext).

A key, for example, might be the string NQXPOMAFTRHLZGECYJIUWSKDVB. This 26-character key means that A (the first letter of the alphabet) should be converted into N (the first character of the key), B (the second letter of the alphabet) should be converted into Q (the second character of the key), and so forth.

A message like HELLO, then, would be encrypted as FOLLE, replacing each of the letters according to the mapping determined by the key.

Create a program that enables you to encrypt messages using a substitution cipher. At the time the user executes the program, they should decide, by providing a command-line argument, on what the key should be in the secret message they’ll provide at runtime.

Your program must accept a single command-line argument, the key to use for the substitution. The key itself should be case-insensitive, so whether any character in the key is uppercase or lowercase should not affect the behavior of your program. If your program is executed without any command-line arguments or with more than one command-line argument, your program should print an error message of your choice (with printf) and return from main a value of 1 (which tends to signify an error) immediately. If the key is invalid (as by not containing 26 characters, containing any character that is not an alphabetic character, or not containing each letter exactly once), your program should print an error message of your choice (with printf) and return from main a value of 1 immediately.

Your program must output plaintext: (without a newline) and then prompt the user for a string of plaintext (using get_string). Your program must output ciphertext: (without a newline) followed by the plaintext’s corresponding ciphertext, with each alphabetical character in the plaintext substituted for the corresponding character in the ciphertext; non-alphabetical characters should be outputted unchanged. Your program must preserve case: capitalized letters must remain capitalized letters; lowercase letters must remain lowercase letters. After outputting ciphertext, you should print a newline. Your program should then exit by returning 0 from main.

Phew, that’s a pretty bulky description! I’ll start by outlining the big steps I would need to solve the problem as follows:

  1. Get key
  2. Validate key
  3. Get plaintext
  4. Encipher
  5. Print ciphertext

As I’ve just learned, the program can receive command-line arguments, so I can use the main function with two new input parameters: integer argc and array argv. The key that the program should receive is the second word in the command line (after the file name), so keeping arrays’ zero-indexing in mind, I start with this:

#include <cs50.h>
#include <stdio.h>

int main(int argc, string argv[])
{
    // Get key
    string key = argv[1];
}

Now the key is stored in the key variable. Next, I’ll get the key length, which is pretty easy now as I know about the strlen function, and also check whether the length equals 26:

#include <cs50.h>
#include <stdio.h>
#include <string.h>

int main(int argc, string argv[])
{
    // Get key
    string key = argv[1];

  // Get key length
    int key_length = strlen(key);

    // Check key length
    if (key_length != 26)
    {
        printf("Key must contain 26 characters.\n");
        return 1;
    }
}

I don’t quite like the ‘magic number’ 26 in the if condition, so I’ll move it out as a global constant. I also feel that I need to make the key variable global (though I’m not sure whether it’s a good idea):

#include <cs50.h>
#include <stdio.h>
#include <string.h>

// Constant
const int valid_key_length = 26;

string key;

int main(int argc, string argv[])
{
    // Get key
    key = argv[1];

  // Get key length
    int key_length = strlen(key);

    // Check key length
    if (key_length != valid_key_length)
    {
        printf("Key must contain 26 characters.\n");
        return 1;
    }
}

I guess I could have created a separate function for checking the key length, but would it be worth it given that the whole code is literally just one if and one printf? Let me know in the comments!

Next, I’m going to check whether the key only consists of alphabetic characters, and this is where a separate function definitely should be a good idea. So, I’ll create a function called is_key_alphabetic that can take a string as input and give me a boolean as output – whether the key consists of alphabetical is true or false.

Inside the function, I need a loop that keeps checking the string character by character until it’s not equal to nul, which in plain English means ‘until the end of the word’s length’. You may wonder, why making such an extravagant condition of the loop, and to be honest, it’s just for the sake of exercising. I find it pretty interesting that all arrays in C are ‘secretly’ end with null to indicate the end of the string.

So, here is the first function:

bool is_key_alphabetic(string s)
{
    for (int i = 0; s[i] != '\0'; i++)
    {
        if (!isalpha(s[i]))
        {
            return false;
        }
    }
    return true;
}

Now upon coming back to my program and writing this blog post, I realised that the naming here is a bit messy. I mean, the function is called is_key_alphabetic, whereas inside the loop there is an if statement checking whether a character is not alphabetic, and in that it case returns false, and then the whole function returns true. Apart from the variable name s which I admit isn’t very informative, is there any anything else I could improve here?

In the meantime, I’ll create another function to check for any repeated characters in the key. From a logical perspective, identifying a repeated character should be as easy as checking whether it’s equal to itself. However, in the code, I don’t know a better solution than doing a loop within a loop:

bool is_key_repeated(string s)
{
    for (int i = 0; s[i] != '\0'; i++)
    {
        for (int j = 0; j < i; j++)
        {
            if (s[i] == s[j])
            {
                printf("Key must not contain repeated characters.\n");
                return false;
            }
        }
    }
    return true;
}

Once again, upon writing this blog post, I already spotted some things I could’ve done better. For example, including printf in this function probably wasn’t a good idea.

Next, I’ll create another function to encipher plaintext. It seems I need to make some sort of mapping to assign each letter of the plaintext to according letter of the key. And thanks to the previous problem, Scrabble, I now know how to do it!

So, I’ll call the function encipher, which is going to take the input text, iterate through each character, replace it with the corresponding character from the provided key while preserving the case, and then print the resulting ciphertext.

void encipher(string text)
{
    int length = strlen(text);
    for (int i = 0; i < length; i++)
    {
        if (isupper(text[i]))
        {
            text[i] = toupper(key[text[i] - 'A']);
        }
        else if (islower(text[i]))
        {
            text[i] = tolower(key[text[i] - 'a']);
        }
    }
    // Print ciphertext
    printf("ciphertext: %s\n", text);
}

All I need from now is just to get a plaintext input using the get_string function, and then call all three functions I’ve created above in the main function. And so here is my final code:

// A program in C that enables you to encrypt messages using a substitution cipher.

#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>

// Constant
const int valid_key_length = 26;

// Function prototypes
bool is_key_alphabetic(string key);
bool is_key_repeated(string key);
void encipher(string text);

string key;

// Main
int main(int argc, string argv[])
{

    // Check if there's a key provided
    if (argc != 2)
    {
        printf("Usage: ./substitution KEY\n");
        return 1;
    }

    // Get key
    key = argv[1];

    // Get key length
    int key_length = strlen(key);

    // Check key length
    if (key_length != valid_key_length)
    {
        printf("Key must contain 26 characters.\n");
        return 1;
    }

    // Check key contains only alphabetic characters
    if (!is_key_alphabetic(key))
    {
        printf("Key must only contain alphabetical characters.\n");
        return 1;
    }

    // Check key doesn't contain repeated characters
    if (!is_key_repeated(key))
    {
        printf("Key must not contain repeated characters.\n");
        return 1;
    }

    // Get plaintext
    string plaintext = get_string("plaintext ");

    // Encipher
    encipher(plaintext);

    // Print ciphertext
    printf("\n");
    return 0;
}

bool is_key_alphabetic(string s)
{
    for (int i = 0; s[i] != '\0'; i++)
    {
        if (!isalpha(s[i]))
        {
            return false;
        }
    }
    return true;
}

bool is_key_repeated(string s)
{
    for (int i = 0; s[i] != '\0'; i++)
    {
        for (int j = 0; j < i; j++)
        {
            if (s[i] == s[j])
            {
                printf("Key must not contain repeated characters.\n");
                return false;
            }
        }
    }
    return true;
}

void encipher(string text)
{
    int length = strlen(text);
    for (int i = 0; i < length; i++)
    {
        if (isupper(text[i]))
        {
            text[i] = toupper(key[text[i] - 'A']);
        }
        else if (islower(text[i]))
        {
            text[i] = tolower(key[text[i] - 'a']);
        }
    }
    // Print ciphertext
    printf("ciphertext: %s\n", text);
}
 229   2 mo   C   CS50   Programming
Next
1 comment
Иларион Beeline 2 mo

Instead of

printf(“Key must contain 26 characters.\n”);

must go:

string res;
res = strcat(Key must contain “, string (valid_key_length));
res+= ” characters.“;
res+= ‘\n’);
printf(res);

Daniel Sokolovskiy 2 mo

I wasn’t sure what that meant, so I asked Chat GPT, and it says “Keep in mind that the original approach is straightforward and clear. The suggested modifications might add complexity without significant benefits in this case.” Could you please elaborate on why that would be better?

Here is the full answer of the chat:

© Daniel Sokolovskiy, 2024
Powered by Aegea