Skip to content

Final Capstone Project — Build a Complete C++ Application, File Encryption Tool, CLI with CMake, Testing, Documentation

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Final Capstone Project. We cover key concepts, practical examples, and best practices to help you master this topic.

This C++ capstone project builds a complete file encryption/decryption tool — integrating RAII, STL containers, algorithms, CMake, unit testing, and debugging — applying every concept from the 70-lesson series in one real-world application.

What You'll Learn

You will design and implement a complete C++ CLI application from scratch, apply RAII for file handle and memory management, use the STL (vectors, strings, algorithms, filesystem), implement XOR encryption and Base64 encoding, organize the project with CMake and multi-file structure, write unit tests with Google Test, debug with GDB and sanitizers, and document the API and usage.

Why It Matters

This capstone ties together everything from the series. You move from learning individual concepts to applying them together in a cohesive, real-world project. The file encryption tool mirrors professional C++ applications: it processes binary data, handles errors, manages resources, provides a CLI interface, and is testable and maintainable.

Learning Path

graph LR
    A["69: Best Practices"] --> B["70: Final Capstone Project"]
    B --> C["C++ Journey Complete!"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#27ae60,stroke:#1e8449,color:#fff

Project Specification: FileCrypt

FileCrypt is a command-line file encryption tool supporting XOR cipher and Base64 encoding.

Features

  • Encrypt and decrypt files with a password-derived key
  • XOR cipher with repeated key (stream cipher)
  • Base64 encoding for text-safe output
  • File integrity verification with CRC32
  • Support for large files (streaming, not loading entire file)
  • Verbose and quiet modes
  • Overwrite protection

CLI Interface

Usage: filecrypt <command> [options] <input> <output>

Commands:
  encrypt   Encrypt a file
  decrypt   Decrypt a file (detects format automatically)

Options:
  -k, --key <password>   Encryption password (required)
  -m, --mode <mode>      Output mode: raw | base64 (default: raw)
  -v, --verbose          Show progress information
  -f, --force            Overwrite output file without asking
  -h, --help             Show this help message

Examples:
  filecrypt encrypt -k "mypassword" secret.txt secret.enc
  filecrypt decrypt -k "mypassword" secret.enc secret.txt
  filecrypt encrypt -k "pass" -m base64 data.txt data.b64
  filecrypt decrypt -k "pass" -m base64 data.b64 data.txt

Project Structure

filecrypt/
├── CMakeLists.txt
├── README.md
├── include/
│   ├── filecrypt/
│   │   ├── cipher.h          # XOR cipher interface
│   │   ├── base64.h          # Base64 encode/decode
│   │   ├── cli.h             # CLI argument parsing
│   │   ├── file_handler.h    # RAII file operations
│   │   ├── crc32.h           # CRC32 checksum
│   │   └── app.h             # Application orchestration
├── src/
│   ├── main.cpp              # Entry point
│   ├── cipher.cpp            # XOR cipher implementation
│   ├── base64.cpp            # Base64 implementation
│   ├── cli.cpp               # CLI parser
│   ├── file_handler.cpp      # File I/O
│   ├── crc32.cpp             # CRC32 implementation
│   └── app.cpp               # Application logic
├── tests/
│   ├── CMakeLists.txt
│   ├── test_cipher.cpp
│   ├── test_base64.cpp
│   ├── test_crc32.cpp
│   └── test_app.cpp
└── tools/
    └── benchmark.cpp

Implementation — Core Components

1. XOR Cipher (cipher.h)

#pragma once
#include <vector>
#include <cstdint>
#include <string>

namespace filecrypt {

// XOR stream cipher
class XorCipher {
public:
    explicit XorCipher(std::string password);

    // Encrypt/decrypt are symmetric (XOR is its own inverse)
    void process(std::vector<uint8_t>& data) const;
    void processInPlace(uint8_t* data, size_t size) const;

private:
    std::string password_;
    uint8_t key_byte(size_t position) const;
};

// Key derivation: simple hash of password
std::vector<uint8_t> derive_key(const std::string& password, size_t key_length = 32);

}  // namespace filecrypt

2. Base64 Encoding (base64.h)

#pragma once
#include <string>
#include <vector>
#include <cstdint>

namespace filecrypt {

std::string base64_encode(const std::vector<uint8_t>& data);
std::vector<uint8_t> base64_decode(const std::string& encoded);

}  // namespace filecrypt

3. RAII File Handler (file_handler.h)

#pragma once
#include <cstdio>
#include <string>
#include <vector>
#include <cstdint>

namespace filecrypt {

// RAII wrapper for FILE* with read/write operations
class FileHandler {
    FILE* file_;
    std::string path_;
    bool readable_;
    bool writable_;

public:
    FileHandler(const std::string& path, const std::string& mode);
    ~FileHandler();

    // Move only
    FileHandler(FileHandler&& other) noexcept;
    FileHandler& operator=(FileHandler&& other) noexcept;

    size_t read(std::vector<uint8_t>& buffer, size_t max_size);
    size_t write(const std::vector<uint8_t>& data);
    size_t write(const uint8_t* data, size_t size);

    bool is_open() const { return file_ != nullptr; }
    const std::string& path() const { return path_; }
    size_t size() const;

    // No copy
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;
};

}  // namespace filecrypt

4. CLI Parser (cli.h)

#pragma once
#include <string>
#include <optional>

namespace filecrypt {

struct CliOptions {
    enum class Command { Encrypt, Decrypt, Help };

    Command command;
    std::string input_path;
    std::string output_path;
    std::string password;
    bool base64_mode = false;
    bool verbose = false;
    bool force = false;
};

class CliParser {
public:
    CliOptions parse(int argc, char* argv[]);

private:
    void print_usage() const;
};

}  // namespace filecrypt

Test Plan

// test_cipher.cpp
#include <gtest/gtest.h>
#include "filecrypt/cipher.h"

using namespace filecrypt;

TEST(XorCipherTest, EncryptDecryptRoundtrip) {
    std::string password = "test_password";
    XorCipher cipher(password);

    std::vector<uint8_t> original = {'H', 'e', 'l', 'l', 'o'};
    std::vector<uint8_t> encrypted = original;
    cipher.process(encrypted);

    // Decrypt (XOR is symmetric)
    cipher.process(encrypted);

    EXPECT_EQ(original, encrypted);
}

TEST(XorCipherTest, DifferentPasswordsProduceDifferentOutput) {
    XorCipher cipher1("password1");
    XorCipher cipher2("password2");

    std::vector<uint8_t> data(100, 'A');
    std::vector<uint8_t> encrypted1 = data;
    std::vector<uint8_t> encrypted2 = data;

    cipher1.process(encrypted1);
    cipher2.process(encrypted2);

    EXPECT_NE(encrypted1, encrypted2);
}

TEST(XorCipherTest, EmptyData) {
    XorCipher cipher("pass");
    std::vector<uint8_t> data;
    cipher.process(data);
    EXPECT_TRUE(data.empty());
}

TEST(XorCipherTest, LargeData) {
    XorCipher cipher("key");
    std::vector<uint8_t> data(1024 * 1024, 0xAB);  // 1 MB

    std::vector<uint8_t> original = data;
    cipher.process(data);
    cipher.process(data);

    EXPECT_EQ(original, data);
}
// test_base64.cpp
TEST(Base64Test, EncodeDecodeRoundtrip) {
    std::vector<uint8_t> original = {'H', 'e', 'l', 'l', 'o'};
    std::string encoded = base64_encode(original);
    auto decoded = base64_decode(encoded);

    EXPECT_EQ(original, decoded);
}

TEST(Base64Test, StandardVectors) {
    EXPECT_EQ(base64_encode({}), "");
    EXPECT_EQ(base64_encode({'f'}), "Zg==");
    EXPECT_EQ(base64_encode({'f', 'o'}), "Zm8=");
    EXPECT_EQ(base64_encode({'f', 'o', 'o'}), "Zm9v");
    EXPECT_EQ(base64_encode({'f', 'o', 'o', 'b'}), "Zm9vYg==");
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(FileCrypt VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Library
file(GLOB_RECURSE LIB_SOURCES src/*.cpp)

add_library(filecrypt_lib ${LIB_SOURCES})
target_include_directories(filecrypt_lib
    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_options(filecrypt_lib PRIVATE -Wall -Wextra -Wpedantic)

# CLI executable
add_executable(filecrypt src/main.cpp)
target_link_libraries(filecrypt PRIVATE filecrypt_lib)

# Tests
option(ENABLE_TESTS "Build unit tests" ON)
if(ENABLE_TESTS)
    enable_testing()
    include(FetchContent)
    FetchContent_Declare(googletest
        GIT_REPOSITORY https://github.com/google/googletest.git
        GIT_TAG v1.14.0
    )
    FetchContent_MakeAvailable(googletest)

    add_executable(filecrypt_tests
        tests/test_cipher.cpp
        tests/test_base64.cpp
        tests/test_crc32.cpp
    )
    target_link_libraries(filecrypt_tests PRIVATE
        filecrypt_lib
        GTest::gtest_main
    )
    include(GoogleTest)
    gtest_discover_tests(filecrypt_tests)
endif()

# Install
install(TARGETS filecrypt RUNTIME DESTINATION bin)
install(DIRECTORY include/ DESTINATION include)

Full Application (app.cpp — Orchestration)

#include "filecrypt/app.h"
#include "filecrypt/file_handler.h"
#include "filecrypt/cipher.h"
#include "filecrypt/base64.h"
#include "filecrypt/crc32.h"
#include <iostream>
#include <vector>
#include <chrono>

namespace filecrypt {

void App::run(const CliOptions& opts) {
    if (opts.verbose) {
        std::cout << "FileCrypt v" << APP_VERSION << "\n";
        std::cout << "Command: "
                  << (opts.command == CliOptions::Command::Encrypt
                      ? "encrypt" : "decrypt") << "\n";
        std::cout << "Input: " << opts.input_path << "\n";
        std::cout << "Output: " << opts.output_path << "\n";
    }

    auto start = std::chrono::steady_clock::now();
    XorCipher cipher(opts.password);

    // Open files with RAII
    FileHandler input(opts.input_path, "rb");
    FileHandler output(opts.output_path, "wb");

    if (!input.is_open()) {
        throw std::runtime_error("Cannot open input: " + opts.input_path);
    }
    if (!output.is_open()) {
        throw std::runtime_error("Cannot open output: " + opts.output_path);
    }

    // Process in chunks for large file support
    constexpr size_t k_chunk_size = 64 * 1024;  // 64 KB
    std::vector<uint8_t> buffer(k_chunk_size);
    uint64_t total_bytes = 0;

    while (true) {
        size_t bytes_read = input.read(buffer, k_chunk_size);
        if (bytes_read == 0) break;

        buffer.resize(bytes_read);
        cipher.process(buffer);

        if (opts.base64_mode && opts.command == CliOptions::Command::Encrypt) {
            std::string encoded = base64_encode(buffer);
            output.write(reinterpret_cast<const uint8_t*>(encoded.data()),
                        encoded.size());
        } else {
            output.write(buffer);
        }

        total_bytes += bytes_read;

        if (opts.verbose) {
            std::cout << "\rProcessed: " << total_bytes << " bytes" << std::flush;
        }
    }

    if (opts.verbose) {
        auto end = std::chrono::steady_clock::now();
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
                    end - start).count();
        std::cout << "\nDone. " << total_bytes << " bytes in "
                  << ms << "ms\n";
    }
}

}  // namespace filecrypt

Extension Ideas

Once the base project works, extend it with:

  1. Random salt: Prepend a random salt to the key derivation to prevent rainbow table attacks
  2. Compression: Integrate zlib compression before encryption
  3. Multi-threading: Use std::async to encrypt chunks in parallel
  4. Key file support: Read the encryption key from a separate file
  5. Self-test mode: filecrypt --self-test runs internal verification
  6. GUI: Build a Qt or GTK frontend (separate Repository)
  7. Benchmark: Measure throughput for various file sizes and chunk sizes
  8. Plugin system: Load cipher implementations from shared libraries

What You've Learned (70 Lessons Recap)

Module Lessons Topics
1. Fundamentals 01-10 C++ basics, types, control flow, functions
2. OOP 11-20 Classes, inheritance, polymorphism, operator overloading
3. Memory 21-28 Pointers, smart pointers, dynamic memory, allocators
4. STL Containers 29-35 Vector, map, set, string_view, span
5. STL Algorithms 36-41 Sorting, searching, ranges, iterators
6. Templates 42-49 Function/class templates, specialization, SFINAE, concepts
7. Modern C++ 50-58 Lambdas, auto, move, forwarding, structured bindings, coroutines
8. Advanced C++ 59-64 Exception safety, RAII, Design Patterns, concurrency, I/O
9. Tools 65-70 CMake, testing, debugging, profiling, best practices, capstone

Checklist Before Submitting

Before considering your project complete, verify each item:

[ ] Builds with CMake (cmake -B build && cmake --build build)
[ ] All tests pass (ctest --test-dir build)
[ ] Works on small and large files (1KB to 1GB)
[ ] Works with binary files (images, PDFs)
[ ] Encrypt/decrypt roundtrip produces identical output
[ ] Base64 mode produces valid Base64 output
[ ] --help displays usage information
[ ] Wrong password gives garbage output (expected)
[ ] Overwrite protection works (-f to force)
[ ] No memory leaks (valgrind --leak-check=full)
[ ] No undefined behavior (UBSan clean)
[ ] clang-tidy reports no warnings
[ ] clang-format applied consistently
[ ] Code follows Core Guidelines

You have completed the full C++ programming series — from "Hello, World!" to a complete, professional-quality C++ application. This is the same skill trajectory used by C++ developers at companies like Google, Microsoft, and JetBrains. The tools and patterns you've learned translate directly to Java, Python, C, and every other systems language.

Congratulations. Now go build something amazing.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro