CipherFork is a command-line tool written in C++ that encrypts or decrypts all files within a specified directory using a simple substitution cipher. This project serves as a practical example of file I/O, inter-process communication using shared memory, and robust process synchronization in a modern C++ application.
- Concurrent Processing: Uses a multiprocessing architecture (
fork()) to handle multiple files at once, significantly speeding up operations in directories with many files. - Simple Interactive UI: An easy-to-use prompt guides you through selecting a directory and an action.
- Secure Key Management: The encryption key is safely loaded from a
.envfile, keeping it separate from the source code. - Robust Synchronization: Leverages POSIX semaphores to manage a process-safe producer-consumer queue, preventing race conditions.
- Memory Safe IPC: Uses POSIX shared memory (
mmap) for high-speed communication and employs safe string functions (strncpy) to prevent buffer overflows. - Resilient by Design: Includes comprehensive error checking for all system calls, ensuring the application fails gracefully instead of crashing on resource allocation errors.
The application is built around a producer-consumer model using multiprocessing. The main process acts as the producer, scanning for files and adding them to a shared task queue. It then forks child processes, which act as consumers, each handling a single encryption or decryption task from the queue.
main.cpp(The Conductor): The application's entry point. It takes user input, iterates through the target directory, and for each file, instructs theProcessManagementmodule to submit a new task. It is also responsible for waiting for all child processes to complete before exiting.ProcessManagementModule (The Task Manager): This is the heart of the concurrent architecture. It is responsible for:- Safely setting up and tearing down a shared memory region and POSIX semaphores.
- Forking new child processes to act as workers.
- Implementing the logic for safely adding tasks (producing) and executing tasks (consuming).
EncryptionModule (The Worker): This module contains the core encryption/decryption logic. It's executed by each child process, which is responsible for opening the file, processing its content, and closing it.
To allow the parent process (producer) and multiple child processes (consumers) to safely share the task queue, CipherFork uses a combination of shared memory and POSIX semaphores. This solves the classic computer science challenge of the bounded-buffer producer-consumer problem.
When multiple processes access a shared resource simultaneously, we risk critical bugs:
- Race Conditions: Two processes trying to modify the queue's state at the same time can lead to a corrupted queue, lost tasks, and unpredictable behavior.
- Memory Corruption: If a long file path is written into the shared memory without checking its length, it can write past its allocated buffer, corrupting adjacent data and causing random segmentation faults.
1. Semaphores as Locks and Signals:
As seen in ProcessManagement.cpp, the application uses three named semaphores to orchestrate access to the shared queue. These are managed by the operating system and work reliably across different processes.
mutexSemaphore: A binary semaphore (acting as a lock) that ensures only one process can be in a "critical section" (modifying the queue'sfrontandrearpointers) at any given time.itemsSemaphore: A counting semaphore that tracks the number of tasks in the queue. Consumers wait on this, preventing them from trying to read from an empty queue.emptySlotsSemaphore: A counting semaphore that tracks available space. The producer waits on this, preventing it from adding tasks to a full queue.
2. Robustness and Error Handling: A key feature of CipherFork's stability is its rigorous checking of system resources.
- Initialization Checks: The return values of
sem_open,shm_open, andmmapare all checked. If any of these critical resources fail to be allocated by the OS, the program throws an exception and terminates immediately with a clear error message, preventing a crash later on. - Memory Safety: The unsafe
strcpyfunction is avoided. Instead,strncpyis used to copy file paths into the shared memory, guaranteeing that a long path can never cause a buffer overflow.
Here is the sequence of operations for each process, which ensures a perfectly synchronized and safe workflow:
Parent Process (Producer Logic in main.cpp and submitToQueue):
- Scans the directory for a file.
- Calls
submitToQueuewhich waits for an empty slot (sem_wait(emptySlotsSemaphore)). - Acquires the mutex lock (
sem_wait(mutexSemaphore)). - Critical Section: Safely copies the task string into the shared
tasksarray and updates therearpointer. - Releases the mutex lock (
sem_post(mutexSemaphore)). - Signals that a new item is available (
sem_post(itemsSemaphore)). - Calls
fork()to create a new child process. - The parent process saves the child's PID and continues its loop. It does not execute the task.
- After the directory scan is complete, the parent waits for all saved child PIDs to exit.
Child Process (Consumer Logic in executeTask):
- The child process begins execution immediately after the
fork(). - It calls
executeTask(), which waits for an available item (sem_wait(itemsSemaphore)). - Acquires the mutex lock (
sem_wait(mutexSemaphore)). - Critical Section: Reads a task from the shared
tasksarray and updates thefrontpointer. - Releases the mutex lock (
sem_post(mutexSemaphore)). - Signals that a slot has been freed up (
sem_post(emptySlotsSemaphore)). - Executes the encryption/decryption on the retrieved task.
- Calls
exit(0)to terminate itself.
- G++ (GNU C++ Compiler)
- make
-
Download the Code: Clone or download the repository to your local machine.
-
Configure Environment: Create a
.envfile in the project root by copying the example file and add your secret key.cp .env_example .env # Now, edit the .env file and set your ENCRYPTION_KEY -
Compile: Open your terminal in the project's root directory and run
make.make
This will generate an executable file named
encrypt_decrypt.
To run the program, simply execute the compiled file and follow the on-screen instructions.
-
Start the program:
./encrypt_decrypt
-
Enter the directory path when prompted. For example, if you want to process files in a folder named
my_files, you would type:Enter the directory path: my_files -
Enter the action (
encryptordecrypt):Enter the action (encrypt/decrypt): encrypt
The application will then proceed to encrypt or decrypt every file in the my_files directory.