Build A CLI Caesar Cipher Encryption/Decryption Tool

by Sebastian Müller 53 views

Hey guys! Let's dive into implementing a CLI (Command Line Interface) for a Caesar Cipher encryption/decryption tool with file handling. This is gonna be a fun project where we'll build a program that can encrypt and decrypt files using the Caesar Cipher, all from the command line. Think of it as your own little secret message creator and decoder! So, buckle up and let's get started!

Understanding the Project Requirements

Before we jump into the code, let's break down what we need to accomplish. We've got four main tasks:

  1. CLI Handling: We need to create a command-line interface that takes user input for the input file, output file, and encryption key. This means we'll be writing code that parses arguments from the command line and uses them to drive our encryption/decryption process.
  2. Caesar Cipher Encryption: We'll implement a function that takes a byte of data and an encryption key and returns the encrypted byte using the Caesar Cipher algorithm. The Caesar Cipher is a simple substitution cipher where each letter in the plaintext is shifted a certain number of positions down the alphabet. It's classic cryptography!
  3. Caesar Cipher Decryption: We'll also need a function that reverses the encryption process. This function will take an encrypted byte and the key and return the original, decrypted byte. Think of it as unlocking the secret message.
  4. File Handling: Finally, we'll write functions to open files, read their contents byte by byte, and write the processed (encrypted or decrypted) data to a new output file. This is where our program interacts with the file system, reading and writing data.

Let's tackle each of these tasks one by one, making sure we understand the concepts and write clean, efficient code.

Designing the main.c File

The heart of our program will be the main.c file. This is where the program execution begins, and it's where we'll handle user input and orchestrate the encryption/decryption process. So, how do we design this file effectively?

First, we need to think about the command-line arguments our program will accept. We'll need:

  • Input File: The path to the file we want to encrypt or decrypt.
  • Output File: The path where we'll save the processed output.
  • Encryption Key: An integer representing the shift value for the Caesar Cipher.
  • Operation Mode: An indicator to specify whether we want to encrypt or decrypt the file.

We can use the argc and argv parameters of the main function to access these arguments. Let's outline the basic structure of our main.c file:

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

// Function prototypes (we'll define these later)
unsigned char encrypt_byte(unsigned char byte, int key);
unsigned char decrypt_byte(unsigned char byte, int key);
bool process_file(const char *input_file, const char *output_file, int key, bool encrypt);

int main(int argc, char *argv[]) {
    // 1. Parse command-line arguments
    // 2. Validate arguments
    // 3. Call process_file function
    // 4. Handle errors
    // 5. Return success/failure

    return 0;
}

Now, let's fill in the details. We'll use getopt to parse command-line options. This function makes it easier to handle different options and their arguments.

Here’s an example of how to use getopt:

#include <unistd.h>

int main(int argc, char *argv[]) {
    int opt;
    char *input_file = NULL;
    char *output_file = NULL;
    int key = 0;
    bool encrypt = true; // Default to encryption

    while ((opt = getopt(argc, argv, "e:d:i:o:k:")) != -1) {
        switch (opt) {
            case 'e': // Encryption mode
                encrypt = true;
                break;
            case 'd': // Decryption mode
                encrypt = false;
                break;
            case 'i': // Input file
                input_file = optarg;
                break;
            case 'o': // Output file
                output_file = optarg;
                break;
            case 'k': // Key
                key = atoi(optarg);
                break;
            case '?': // Unknown option
                fprintf(stderr, "Usage: %s [-e|-d] -i input_file -o output_file -k key\n", argv[0]);
                return 1;
            default:
                break;
        }
    }

    // ... (rest of the main function)
}

In this code snippet, we're using getopt to parse options like -e for encryption, -d for decryption, -i for the input file, -o for the output file, and -k for the key. The optarg variable holds the argument associated with the option (e.g., the filename after -i).

Next, we need to validate the arguments. We should check if the input file, output file, and key are provided. If any of these are missing, we should print an error message and exit. Remember, error handling is crucial for robust programs!

    if (input_file == NULL || output_file == NULL || key == 0) {
        fprintf(stderr, "Error: Missing required arguments.\n");
        fprintf(stderr, "Usage: %s [-e|-d] -i input_file -o output_file -k key\n", argv[0]);
        return 1;
    }

Finally, we call the process_file function (which we'll define later) to handle the actual encryption or decryption. We'll also add some basic error handling to catch potential issues during file processing.

    if (!process_file(input_file, output_file, key, encrypt)) {
        fprintf(stderr, "Error: File processing failed.\n");
        return 1;
    }

    printf("File processed successfully.\n");
    return 0;

Implementing the Caesar Cipher

Now let's move on to the core of our encryption and decryption logic: the Caesar Cipher. As we discussed earlier, the Caesar Cipher is a substitution cipher where each letter is shifted a certain number of positions down the alphabet. For example, with a key of 3, 'A' becomes 'D', 'B' becomes 'E', and so on. Think of it as a simple rotation of the alphabet.

We'll implement two functions: encrypt_byte and decrypt_byte. These functions will take a byte and a key as input and return the encrypted or decrypted byte, respectively. But guys, since we are dealing with bytes (0-255), we need to apply the Caesar cipher to the entire byte range, not just letters. This makes it a bit more interesting.

Here’s how we can implement encrypt_byte:

unsigned char encrypt_byte(unsigned char byte, int key) {
    return (byte + key) % 256;
}

This function adds the key to the byte and then takes the modulo 256 of the result. The modulo operation ensures that the result stays within the byte range (0-255). It's like wrapping around the byte range when you exceed 255.

Similarly, here’s the implementation of decrypt_byte:

unsigned char decrypt_byte(unsigned char byte, int key) {
    return (byte - key + 256) % 256;
}

This function subtracts the key from the byte and adds 256 before taking the modulo. The addition of 256 is crucial to handle negative results correctly. Without it, if byte - key is negative, the modulo operation might not give the expected result. We're essentially ensuring that we always have a positive number before applying the modulo.

These two functions form the core of our Caesar Cipher implementation. They're simple, yet effective for basic encryption and decryption. Remember, though, that the Caesar Cipher is not very secure for real-world applications. It's easily cracked, especially with computers. But for our learning purposes, it’s a great starting point.

File Handling Functions

Next, we need to write functions to handle file I/O. We'll create a process_file function that opens the input file, reads its contents byte by byte, encrypts or decrypts each byte, and writes the processed bytes to the output file. This is where our program interacts with the file system, and it's essential for our tool to be useful.

Here’s the prototype of our process_file function:

bool process_file(const char *input_file, const char *output_file, int key, bool encrypt);

This function takes the input file path, output file path, encryption key, and a boolean flag indicating whether to encrypt or decrypt. It returns true if the processing is successful and false otherwise.

Let's look at the implementation:

bool process_file(const char *input_file, const char *output_file, int key, bool encrypt) {
    FILE *in_file = fopen(input_file, "rb");
    if (in_file == NULL) {
        perror("Error opening input file");
        return false;
    }

    FILE *out_file = fopen(output_file, "wb");
    if (out_file == NULL) {
        perror("Error opening output file");
        fclose(in_file);
        return false;
    }

    int byte;
    while ((byte = fgetc(in_file)) != EOF) {
        unsigned char processed_byte;
        if (encrypt) {
            processed_byte = encrypt_byte((unsigned char)byte, key);
        } else {
            processed_byte = decrypt_byte((unsigned char)byte, key);
        }
        if (fputc(processed_byte, out_file) == EOF) {
            perror("Error writing to output file");
            fclose(in_file);
            fclose(out_file);
            return false;
        }
    }

    fclose(in_file);
    fclose(out_file);
    return true;
}

Let's break this down step by step:

  1. We open the input file in binary read mode ("rb") using fopen. We check if the file was opened successfully. If not, we print an error message using perror and return false.
  2. We open the output file in binary write mode ("wb"). Again, we check for errors and return false if the file couldn't be opened. It's crucial to handle file opening errors to prevent crashes.
  3. We read the input file byte by byte using fgetc. The loop continues until we reach the end of the file (EOF).
  4. Inside the loop, we encrypt or decrypt each byte using the encrypt_byte or decrypt_byte functions, depending on the encrypt flag.
  5. We write the processed byte to the output file using fputc. We check for errors during writing and return false if there's an issue.
  6. Finally, we close both the input and output files using fclose. Closing files is essential to release resources and prevent data corruption.

Putting It All Together

Okay, guys, we've built all the pieces of our Caesar Cipher encryption/decryption tool. We've got the CLI handling, the encryption/decryption logic, and the file handling functions. Now it's time to put it all together and see our program in action!

Here’s the complete main.c file:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>

// Function prototypes
unsigned char encrypt_byte(unsigned char byte, int key);
unsigned char decrypt_byte(unsigned char byte, int key);
bool process_file(const char *input_file, const char *output_file, int key, bool encrypt);

int main(int argc, char *argv[]) {
    int opt;
    char *input_file = NULL;
    char *output_file = NULL;
    int key = 0;
    bool encrypt = true; // Default to encryption

    while ((opt = getopt(argc, argv, "e:d:i:o:k:")) != -1) {
        switch (opt) {
            case 'e': // Encryption mode
                encrypt = true;
                break;
            case 'd': // Decryption mode
                encrypt = false;
                break;
            case 'i': // Input file
                input_file = optarg;
                break;
            case 'o': // Output file
                output_file = optarg;
                break;
            case 'k': // Key
                key = atoi(optarg);
                break;
            case '?': // Unknown option
                fprintf(stderr, "Usage: %s [-e|-d] -i input_file -o output_file -k key\n", argv[0]);
                return 1;
            default:
                break;
        }
    }

    if (input_file == NULL || output_file == NULL || key == 0) {
        fprintf(stderr, "Error: Missing required arguments.\n");
        fprintf(stderr, "Usage: %s [-e|-d] -i input_file -o output_file -k key\n", argv[0]);
        return 1;
    }

    if (!process_file(input_file, output_file, key, encrypt)) {
        fprintf(stderr, "Error: File processing failed.\n");
        return 1;
    }

    printf("File processed successfully.\n");
    return 0;
}

unsigned char encrypt_byte(unsigned char byte, int key) {
    return (byte + key) % 256;
}

unsigned char decrypt_byte(unsigned char byte, int key) {
    return (byte - key + 256) % 256;
}

bool process_file(const char *input_file, const char *output_file, int key, bool encrypt) {
    FILE *in_file = fopen(input_file, "rb");
    if (in_file == NULL) {
        perror("Error opening input file");
        return false;
    }

    FILE *out_file = fopen(output_file, "wb");
    if (out_file == NULL) {
        perror("Error opening output file");
        fclose(in_file);
        return false;
    }

    int byte;
    while ((byte = fgetc(in_file)) != EOF) {
        unsigned char processed_byte;
        if (encrypt) {
            processed_byte = encrypt_byte((unsigned char)byte, key);
        } else {
            processed_byte = decrypt_byte((unsigned char)byte, key);
        }
        if (fputc(processed_byte, out_file) == EOF) {
            perror("Error writing to output file");
            fclose(in_file);
            fclose(out_file);
            return false;
        }
    }

    fclose(in_file);
    fclose(out_file);
    return true;
}

To compile this code, you can use a C compiler like GCC:

gcc main.c -o caesar

This will create an executable file named caesar. Now you can run the program from the command line:

./caesar -e -i input.txt -o encrypted.txt -k 3

This command will encrypt the input.txt file using a key of 3 and save the output to encrypted.txt. To decrypt the file, you can use:

./caesar -d -i encrypted.txt -o decrypted.txt -k 3

This will decrypt encrypted.txt and save the result to decrypted.txt. Pretty cool, right?

Conclusion

So, there you have it! We've successfully implemented a CLI-based Caesar Cipher encryption/decryption tool with file handling. We covered a lot of ground, from parsing command-line arguments to implementing the Caesar Cipher algorithm and handling file I/O. This project is a great example of how to combine different programming concepts to build a useful tool.

Remember, the Caesar Cipher is a simple cipher and is not secure for real-world applications. However, this project provides a solid foundation for learning more advanced encryption techniques. You can extend this project by implementing more sophisticated ciphers, adding error handling, and improving the user interface.

Keep coding, keep learning, and have fun building cool stuff! Remember that understanding the fundamentals is key to mastering more complex topics. So, take the time to experiment, modify the code, and see what you can create. You've got this!