Beginner's Guide to Cryptography

Beginner's Guide to Cryptography

This guide is designed for developers new to cryptography. We’ll cover common use cases with simple, copy-paste ready examples that follow security best practices.

Quick Start: What Do You Need?

I need to hash data (checksums, integrity verification)

→ Use BLAKE3 Hashing or SHA-256

I need to encrypt data (protecting confidentiality)

→ Use AES-GCM Encryption

I need to hash passwords (user authentication)

→ Use Argon2 Password Hashing

I need to verify data hasn’t been tampered with

→ Use HMAC

I need to generate random data (keys, tokens, IDs)

→ Use Random Number Generation


Hashing Data (BLAKE3)

When to use: File integrity, content addressing, checksums

What it does: Creates a unique “fingerprint” of your data. Same input = same output. Different input = different output.

Security: One-way function (can’t reverse it to get original data)

Simple Example

#include <cryptopp/blake3.h>
#include <cryptopp/hex.h>
#include <cryptopp/filters.h>
#include <iostream>
#include <string>

std::string hashData(const std::string& data) {
    CryptoPP::BLAKE3 hash;
    std::string digest;

    CryptoPP::StringSource(data, true,
        new CryptoPP::HashFilter(hash,
            new CryptoPP::HexEncoder(
                new CryptoPP::StringSink(digest))));

    return digest;
}

int main() {
    std::string data = "Hello, World!";
    std::string hash = hashData(data);

    std::cout << "Data: " << data << std::endl;
    std::cout << "Hash: " << hash << std::endl;

    return 0;
}

Compile:

g++ -std=c++11 hash_example.cpp -o hash_example -lcryptopp

Hash a File

#include <cryptopp/blake3.h>
#include <cryptopp/hex.h>
#include <cryptopp/files.h>
#include <iostream>

std::string hashFile(const std::string& filename) {
    CryptoPP::BLAKE3 hash;
    std::string digest;

    try {
        CryptoPP::FileSource(filename.c_str(), true,
            new CryptoPP::HashFilter(hash,
                new CryptoPP::HexEncoder(
                    new CryptoPP::StringSink(digest))));
        return digest;
    }
    catch (const CryptoPP::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return "";
    }
}

int main() {
    std::string hash = hashFile("document.pdf");
    if (!hash.empty()) {
        std::cout << "File hash: " << hash << std::endl;
    }
    return 0;
}

Hashing Data (SHA-256)

When to use: When you need FIPS compliance or industry standard hashing

Difference from BLAKE3: Slower but more widely standardized

#include <cryptopp/sha.h>
#include <cryptopp/hex.h>
#include <cryptopp/filters.h>
#include <iostream>
#include <string>

std::string hashDataSHA256(const std::string& data) {
    CryptoPP::SHA256 hash;
    std::string digest;

    CryptoPP::StringSource(data, true,
        new CryptoPP::HashFilter(hash,
            new CryptoPP::HexEncoder(
                new CryptoPP::StringSink(digest))));

    return digest;
}

int main() {
    std::string data = "Hello, World!";
    std::string hash = hashDataSHA256(data);

    std::cout << "SHA-256: " << hash << std::endl;
    return 0;
}

Encrypting Data (AES-GCM)

When to use: Protecting sensitive data (files, messages, database entries)

What it does: Scrambles data so only someone with the key can read it. Also prevents tampering.

Important: Keep the key secret! If someone gets your key, they can decrypt everything.

Simple Encryption Class

#include <cryptopp/aes.h>
#include <cryptopp/gcm.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <cryptopp/hex.h>
#include <iostream>
#include <string>

class SimpleEncryption {
private:
    CryptoPP::SecByteBlock key;
    CryptoPP::AutoSeededRandomPool prng;

public:
    // Constructor: generates a random encryption key
    SimpleEncryption() : key(CryptoPP::AES::DEFAULT_KEYLENGTH) {
        prng.GenerateBlock(key, key.size());
    }

    // Encrypt a string
    std::string encrypt(const std::string& plaintext) {
        // Generate random nonce (must be unique for each encryption!)
        // 💡 Why? See: /docs/guides/security-concepts#nonce-and-iv-management
        CryptoPP::SecByteBlock nonce(12);
        prng.GenerateBlock(nonce, nonce.size());

        // Perform encryption
        std::string ciphertext;
        CryptoPP::GCM<CryptoPP::AES>::Encryption enc;
        enc.SetKeyWithIV(key, key.size(), nonce, nonce.size());

        CryptoPP::StringSource(plaintext, true,
            new CryptoPP::AuthenticatedEncryptionFilter(enc,
                new CryptoPP::StringSink(ciphertext)
            )
        );

        // Return: nonce + ciphertext (you need both to decrypt!)
        std::string result((char*)nonce.data(), nonce.size());
        result += ciphertext;
        return result;
    }

    // Decrypt a string
    bool decrypt(const std::string& encrypted, std::string& plaintext) {
        if (encrypted.size() < 12) {
            return false;  // Too short to be valid
        }

        // Extract nonce and ciphertext
        CryptoPP::SecByteBlock nonce((const CryptoPP::byte*)encrypted.data(), 12);
        std::string ciphertext = encrypted.substr(12);

        try {
            CryptoPP::GCM<CryptoPP::AES>::Decryption dec;
            dec.SetKeyWithIV(key, key.size(), nonce, nonce.size());

            CryptoPP::StringSource(ciphertext, true,
                new CryptoPP::AuthenticatedDecryptionFilter(dec,
                    new CryptoPP::StringSink(plaintext)
                )
            );
            return true;
        }
        catch (const CryptoPP::Exception&) {
            return false;  // Decryption failed (wrong key or tampered data)
        }
    }

    // Get key as hex string (for saving to file/database)
    std::string getKeyHex() const {
        std::string keyHex;
        CryptoPP::HexEncoder encoder(new CryptoPP::StringSink(keyHex));
        encoder.Put(key, key.size());
        encoder.MessageEnd();
        return keyHex;
    }

    // Load key from hex string
    void setKeyFromHex(const std::string& keyHex) {
        CryptoPP::HexDecoder decoder;
        decoder.Put((CryptoPP::byte*)keyHex.data(), keyHex.size());
        decoder.MessageEnd();

        key.resize(CryptoPP::AES::DEFAULT_KEYLENGTH);
        decoder.Get(key, key.size());
    }
};

int main() {
    SimpleEncryption crypto;

    // Encrypt
    std::string secret = "This is my secret message!";
    std::string encrypted = crypto.encrypt(secret);

    std::cout << "Original: " << secret << std::endl;
    std::cout << "Encrypted size: " << encrypted.size() << " bytes" << std::endl;

    // Decrypt
    std::string decrypted;
    if (crypto.decrypt(encrypted, decrypted)) {
        std::cout << "Decrypted: " << decrypted << std::endl;
    } else {
        std::cout << "Decryption failed!" << std::endl;
    }

    // Save key for later use
    std::string keyHex = crypto.getKeyHex();
    std::cout << "Save this key: " << keyHex << std::endl;

    return 0;
}

Encrypting Files

#include <cryptopp/aes.h>
#include <cryptopp/gcm.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <iostream>
#include <fstream>
#include <string>
#include <iterator>

bool encryptFile(const std::string& inputFile,
                 const std::string& outputFile,
                 const CryptoPP::SecByteBlock& key) {
    try {
        // Read whole file into memory (fine for most beginner examples)
        std::ifstream in(inputFile, std::ios::binary);
        if (!in) {
            std::cerr << "Error opening input file: " << inputFile << std::endl;
            return false;
        }

        std::string plaintext(
            (std::istreambuf_iterator<char>(in)),
            std::istreambuf_iterator<char>()
        );
        in.close();

        // Generate random 96-bit nonce (must be unique per encryption)
        CryptoPP::AutoSeededRandomPool prng;
        CryptoPP::SecByteBlock nonce(12);
        prng.GenerateBlock(nonce, nonce.size());

        // Encrypt plaintext
        std::string ciphertext;
        CryptoPP::GCM<CryptoPP::AES>::Encryption enc;
        enc.SetKeyWithIV(key, key.size(), nonce, nonce.size());

        CryptoPP::StringSource ss(
            plaintext,
            true,
            new CryptoPP::AuthenticatedEncryptionFilter(
                enc,
                new CryptoPP::StringSink(ciphertext)
            )
        );

        // Write file as: [nonce || ciphertext]
        std::ofstream out(outputFile, std::ios::binary);
        if (!out) {
            std::cerr << "Error opening output file: " << outputFile << std::endl;
            return false;
        }

        out.write(reinterpret_cast<const char*>(nonce.data()), nonce.size());
        out.write(ciphertext.data(), ciphertext.size());
        out.close();

        return true;
    }
    catch (const CryptoPP::Exception& ex) {
        std::cerr << "Encryption error: " << ex.what() << std::endl;
        return false;
    }
}

bool decryptFile(const std::string& inputFile,
                 const std::string& outputFile,
                 const CryptoPP::SecByteBlock& key) {
    try {
        // Read whole file
        std::ifstream in(inputFile, std::ios::binary);
        if (!in) {
            std::cerr << "Error opening input file: " << inputFile << std::endl;
            return false;
        }

        // Read nonce (first 12 bytes)
        CryptoPP::SecByteBlock nonce(12);
        in.read(reinterpret_cast<char*>(nonce.data()), nonce.size());
        if (!in) {
            std::cerr << "Error reading nonce from file" << std::endl;
            return false;
        }

        // Read ciphertext (rest of file)
        std::string ciphertext(
            (std::istreambuf_iterator<char>(in)),
            std::istreambuf_iterator<char>()
        );
        in.close();

        // Decrypt
        std::string plaintext;
        CryptoPP::GCM<CryptoPP::AES>::Decryption dec;
        dec.SetKeyWithIV(key, key.size(), nonce, nonce.size());

        CryptoPP::StringSource ss(
            ciphertext,
            true,
            new CryptoPP::AuthenticatedDecryptionFilter(
                dec,
                new CryptoPP::StringSink(plaintext)
            )
        );

        std::ofstream out(outputFile, std::ios::binary);
        if (!out) {
            std::cerr << "Error opening output file: " << outputFile << std::endl;
            return false;
        }

        out.write(plaintext.data(), plaintext.size());
        out.close();

        return true;
    }
    catch (const CryptoPP::Exception& ex) {
        std::cerr << "Decryption error: " << ex.what() << std::endl;
        return false;
    }
}

int main() {
    // Generate encryption key
    CryptoPP::AutoSeededRandomPool prng;
    CryptoPP::SecByteBlock key(CryptoPP::AES::DEFAULT_KEYLENGTH);
    prng.GenerateBlock(key, key.size());

    // Encrypt file
    if (encryptFile("document.pdf", "document.pdf.encrypted", key)) {
        std::cout << "File encrypted successfully" << std::endl;
    }

    // Decrypt file
    if (decryptFile("document.pdf.encrypted", "document_decrypted.pdf", key)) {
        std::cout << "File decrypted successfully" << std::endl;
    }

    return 0;
}

Password Hashing (Argon2)

When to use: Storing user passwords in a database

What it does: Creates a hash that’s slow to compute (makes brute-force attacks impractical)

Important: Never store plain passwords! Always hash them.

Simple Password Class

#include <cryptopp/argon2.h>
#include <cryptopp/osrng.h>
#include <cryptopp/hex.h>
#include <cryptopp/filters.h>
#include <iostream>
#include <string>

class PasswordHasher {
public:
    // Hash a password - returns salt+hash combined as hex string
    static std::string hashPassword(const std::string& password) {
        CryptoPP::AutoSeededRandomPool prng;

        // Generate random salt
        CryptoPP::SecByteBlock salt(16);
        prng.GenerateBlock(salt, salt.size());

        // Hash password with Argon2id
        CryptoPP::SecByteBlock hash(32);
        CryptoPP::Argon2id argon2;

        argon2.DeriveKey(
            hash, hash.size(),
            (const CryptoPP::byte*)password.data(), password.size(),
            salt, salt.size(),
            nullptr, 0,  // No secret
            nullptr, 0,  // No additional data
            3,           // Time cost (iterations)
            65536        // Memory cost (64 MB)
        );

        // Combine salt + hash and convert to hex
        std::string combined;
        combined.append((char*)salt.data(), salt.size());
        combined.append((char*)hash.data(), hash.size());

        std::string hexOutput;
        CryptoPP::HexEncoder encoder(new CryptoPP::StringSink(hexOutput));
        encoder.Put((CryptoPP::byte*)combined.data(), combined.size());
        encoder.MessageEnd();

        return hexOutput;
    }

    // Verify a password against stored hash
    static bool verifyPassword(const std::string& password,
                              const std::string& storedHashHex) {
        // Decode hex
        std::string decoded;
        CryptoPP::HexDecoder decoder(new CryptoPP::StringSink(decoded));
        decoder.Put((CryptoPP::byte*)storedHashHex.data(), storedHashHex.size());
        decoder.MessageEnd();

        if (decoded.size() != 48) {  // 16 bytes salt + 32 bytes hash
            return false;
        }

        // Extract salt and expected hash
        CryptoPP::SecByteBlock salt((const CryptoPP::byte*)decoded.data(), 16);
        CryptoPP::SecByteBlock expectedHash((const CryptoPP::byte*)decoded.data() + 16, 32);

        // Compute hash with same salt
        CryptoPP::SecByteBlock computedHash(32);
        CryptoPP::Argon2id argon2;

        argon2.DeriveKey(
            computedHash, computedHash.size(),
            (const CryptoPP::byte*)password.data(), password.size(),
            salt, salt.size(),
            nullptr, 0,
            nullptr, 0,
            3,
            65536
        );

        // Compare (constant-time to prevent timing attacks)
        // 💡 Why constant-time? See: /docs/guides/security-concepts#constant-time-operations
        return CryptoPP::VerifyBufsEqual(
            computedHash, expectedHash, 32
        );
    }
};

int main() {
    std::string password = "MySecurePassword123!";

    // Hash password (do this when user registers)
    std::string hashedPassword = PasswordHasher::hashPassword(password);
    std::cout << "Hashed password: " << hashedPassword << std::endl;
    std::cout << "Store this in your database!" << std::endl;

    // Verify password (do this when user logs in)
    bool valid = PasswordHasher::verifyPassword(password, hashedPassword);
    std::cout << "Password valid: " << (valid ? "YES" : "NO") << std::endl;

    // Try wrong password
    bool invalid = PasswordHasher::verifyPassword("WrongPassword", hashedPassword);
    std::cout << "Wrong password: " << (invalid ? "YES" : "NO") << std::endl;

    return 0;
}

Message Authentication (HMAC)

When to use: Verifying that a message hasn’t been tampered with

What it does: Creates a “signature” using a secret key. Anyone with the key can verify the message is authentic.

Use case: API authentication, message integrity

#include <cryptopp/hmac.h>
#include <cryptopp/sha.h>
#include <cryptopp/filters.h>
#include <cryptopp/hex.h>
#include <cryptopp/secblock.h>
#include <cryptopp/osrng.h>
#include <string>
#include <iostream>

class MessageAuth {
private:
    CryptoPP::SecByteBlock key;

public:
    MessageAuth() : key(32) {
        CryptoPP::AutoSeededRandomPool prng;
        prng.GenerateBlock(key, key.size());
    }

    // Create authentication tag for a message (returns hex)
    std::string sign(const std::string& message) {
        std::string mac;     // raw MAC bytes
        std::string macHex;  // hex-encoded MAC

        CryptoPP::HMAC<CryptoPP::SHA256> hmac(key, key.size());

        // Compute MAC
        CryptoPP::StringSource ss(
            message,
            true,
            new CryptoPP::HashFilter(
                hmac,
                new CryptoPP::StringSink(mac)
            )
        );

        // Encode MAC as hex for easy storage/transmission
        CryptoPP::StringSource ss2(
            mac,
            true,
            new CryptoPP::HexEncoder(
                new CryptoPP::StringSink(macHex),
                false  // lowercase
            )
        );

        return macHex;
    }

    // Verify a message's authentication tag (constant-time comparison)
    // 💡 Why constant-time? See: /docs/guides/security-concepts#constant-time-operations
    bool verify(const std::string& message, const std::string& macHex) {
        // 1) Decode provided MAC from hex to raw bytes
        std::string mac;
        try {
            CryptoPP::StringSource ss(
                macHex,
                true,
                new CryptoPP::HexDecoder(
                    new CryptoPP::StringSink(mac)
                )
            );
        }
        catch (const CryptoPP::Exception& e) {
            // Invalid hex input
            std::cerr << "MAC decode error: " << e.what() << std::endl;
            return false;
        }

        // 2) Recompute MAC for this message (raw bytes, not hex)
        std::string computed;
        CryptoPP::HMAC<CryptoPP::SHA256> hmac(key, key.size());

        CryptoPP::StringSource ss2(
            message,
            true,
            new CryptoPP::HashFilter(
                hmac,
                new CryptoPP::StringSink(computed)
            )
        );

        // 3) Constant-time comparison (prevents timing attacks)
        if (mac.size() != computed.size()) {
            return false;
        }

        return CryptoPP::VerifyBufsEqual(
            reinterpret_cast<const CryptoPP::byte*>(mac.data()),
            reinterpret_cast<const CryptoPP::byte*>(computed.data()),
            mac.size()
        );
    }
};

int main() {
    MessageAuth auth;

    std::string message = "Transfer $100 to account 12345";

    // Create authentication tag
    std::string mac = auth.sign(message);
    std::cout << "Message: " << message << std::endl;
    std::cout << "MAC: " << mac << std::endl;

    // Verify message
    bool valid = auth.verify(message, mac);
    std::cout << "Valid: " << (valid ? "YES" : "NO") << std::endl;

    // Try tampering with message
    std::string tamperedMessage = "Transfer $999 to account 12345";
    bool tampered = auth.verify(tamperedMessage, mac);
    std::cout << "Tampered message valid: " << (tampered ? "YES" : "NO") << std::endl;

    return 0;
}

Random Number Generation

When to use: Generating keys, tokens, session IDs, nonces

What it does: Provides cryptographically secure random numbers

Important: Never use rand() or srand() for security! Use AutoSeededRandomPool.

💡 Learn more: Why weak RNGs are dangerous

#include <cryptopp/osrng.h>
#include <cryptopp/hex.h>
#include <iostream>

class RandomGenerator {
private:
    CryptoPP::AutoSeededRandomPool prng;

public:
    // Generate random bytes
    std::string generateBytes(size_t length) {
        CryptoPP::SecByteBlock random(length);
        prng.GenerateBlock(random, random.size());

        std::string hex;
        CryptoPP::HexEncoder encoder(new CryptoPP::StringSink(hex));
        encoder.Put(random, random.size());
        encoder.MessageEnd();

        return hex;
    }

    // Generate random integer in range [min, max]
    // Note: modulo has slight bias unless (max-min+1) divides 2^32.
    // Fine for tokens/IDs; for high-assurance work, use rejection sampling.
    unsigned int generateInt(unsigned int min, unsigned int max) {
        unsigned int value;
        prng.GenerateBlock((CryptoPP::byte*)&value, sizeof(value));
        return min + (value % (max - min + 1));
    }

    // Generate session token
    std::string generateToken() {
        return generateBytes(32);  // 32 bytes = 256 bits
    }

    // Generate encryption key
    CryptoPP::SecByteBlock generateKey(size_t keySize = 16) {
        CryptoPP::SecByteBlock key(keySize);
        prng.GenerateBlock(key, key.size());
        return key;
    }
};

int main() {
    RandomGenerator rng;

    // Generate session token
    std::string token = rng.generateToken();
    std::cout << "Session token: " << token << std::endl;

    // Generate random bytes
    std::string randomBytes = rng.generateBytes(16);
    std::cout << "Random bytes: " << randomBytes << std::endl;

    // Generate random number
    unsigned int randomNum = rng.generateInt(1, 100);
    std::cout << "Random number (1-100): " << randomNum << std::endl;

    // Generate encryption key
    CryptoPP::SecByteBlock key = rng.generateKey(32);
    std::cout << "Generated 256-bit key" << std::endl;

    return 0;
}

Common Questions

How do I store encryption keys?

Bad: Hard-code in source code

// DON'T DO THIS!
std::string key = "mysecretkey123";

Good: Load from secure configuration

#include <cstdlib>  // for std::getenv

// Store in environment variable, config file, or key management system
const char* keyHex = std::getenv("ENCRYPTION_KEY");
if (keyHex) {
    // Load key from hex string
}

Better: Use OS key storage (Windows: DPAPI, Linux: Keyring, macOS: Keychain)

💡 Learn more: Key storage best practices

How do I transmit encrypted data?

Store as: nonce || ciphertext || auth_tag

All three components can be transmitted in the clear - only the key must be secret.

What key size should I use?

  • AES: 128-bit is fine for most uses, 256-bit for maximum security
  • HMAC: 256-bit (32 bytes)
  • Argon2: Handled automatically

How often should I rotate keys?

  • Encryption keys: Yearly, or after 2^32 encryptions (for GCM)
  • API keys: Quarterly or on suspected compromise
  • Password hashes: Never rotate (each password has unique salt)

Can I use this in production?

Yes, cryptopp-modern is designed for production use, and these examples follow security best practices.

However, your overall system still needs proper testing and review:

  • Test thoroughly in your specific environment
  • Consider professional security audit for critical applications
  • Keep library updated for security patches
  • These examples are educational - adapt error handling for your needs

Next Steps

Now that you understand the basics:

  1. ⚠️ Essential Security Reading:

    • Security Concepts Guide - Start here to understand:
      • Why constant-time comparison prevents timing attacks
      • Why GCM nonce reuse is catastrophic
      • Why you need separate keys for encryption and authentication
      • Why rand() is dangerous for cryptography
      • How to properly store keys
  2. Read the detailed algorithm guides:

  3. Explore advanced topics:

    • Public key cryptography (RSA, ECDSA)
    • Key derivation functions
    • Digital signatures

Complete Example: Secure Note Application

Putting it all together - a simple encrypted note storage application.

File format: [salt (16 bytes) || nonce (12 bytes) || ciphertext]

This ensures the same password produces the same key when loading a previously saved note.

#include <cryptopp/aes.h>
#include <cryptopp/gcm.h>
#include <cryptopp/argon2.h>
#include <cryptopp/osrng.h>
#include <cryptopp/filters.h>
#include <iostream>
#include <fstream>
#include <string>
#include <iterator>

class SecureNotes {
private:
    std::string password;
    CryptoPP::AutoSeededRandomPool prng;

    // Derive a 256-bit key from password + salt using Argon2id
    CryptoPP::SecByteBlock deriveKey(const std::string& pwd,
                                     const CryptoPP::SecByteBlock& salt) {
        CryptoPP::SecByteBlock key(32); // 256-bit key

        CryptoPP::Argon2id argon2;
        argon2.DeriveKey(
            key, key.size(),
            reinterpret_cast<const CryptoPP::byte*>(pwd.data()), pwd.size(),
            salt, salt.size(),
            nullptr, 0,   // no secret
            nullptr, 0,   // no additional data
            3,            // time cost
            65536         // memory cost (64 MB)
        );

        return key;
    }

public:
    explicit SecureNotes(const std::string& pwd)
        : password(pwd) {}

    // Save encrypted note to disk
    bool saveNote(const std::string& filename, const std::string& note) {
        try {
            // 1) Generate random salt and nonce
            CryptoPP::SecByteBlock salt(16);   // 128-bit salt
            CryptoPP::SecByteBlock nonce(12);  // 96-bit GCM nonce

            prng.GenerateBlock(salt, salt.size());
            prng.GenerateBlock(nonce, nonce.size());

            // 2) Derive key from password + salt
            CryptoPP::SecByteBlock key = deriveKey(password, salt);

            // 3) Encrypt note with AES-GCM
            std::string ciphertext;
            CryptoPP::GCM<CryptoPP::AES>::Encryption enc;
            enc.SetKeyWithIV(key, key.size(), nonce, nonce.size());

            CryptoPP::StringSource ss(
                note,
                true,
                new CryptoPP::AuthenticatedEncryptionFilter(
                    enc,
                    new CryptoPP::StringSink(ciphertext)
                )
            );

            // 4) Write file as: [salt || nonce || ciphertext]
            std::ofstream out(filename, std::ios::binary);
            if (!out) {
                std::cerr << "Error opening file for writing: " << filename << std::endl;
                return false;
            }

            out.write(reinterpret_cast<const char*>(salt.data()), salt.size());
            out.write(reinterpret_cast<const char*>(nonce.data()), nonce.size());
            out.write(ciphertext.data(), ciphertext.size());
            out.close();

            return true;
        }
        catch (const std::exception& ex) {
            std::cerr << "Error saving: " << ex.what() << std::endl;
            return false;
        }
    }

    // Load and decrypt note from disk
    bool loadNote(const std::string& filename, std::string& note) {
        try {
            std::ifstream in(filename, std::ios::binary);
            if (!in) {
                std::cerr << "Error opening file for reading: " << filename << std::endl;
                return false;
            }

            // 1) Read salt (16 bytes) and nonce (12 bytes)
            CryptoPP::SecByteBlock salt(16);
            CryptoPP::SecByteBlock nonce(12);

            in.read(reinterpret_cast<char*>(salt.data()), salt.size());
            if (!in) {
                std::cerr << "Error reading salt from file" << std::endl;
                return false;
            }

            in.read(reinterpret_cast<char*>(nonce.data()), nonce.size());
            if (!in) {
                std::cerr << "Error reading nonce from file" << std::endl;
                return false;
            }

            // 2) Read ciphertext (rest of file)
            std::string ciphertext(
                (std::istreambuf_iterator<char>(in)),
                std::istreambuf_iterator<char>()
            );
            in.close();

            // 3) Derive key from password + salt
            CryptoPP::SecByteBlock key = deriveKey(password, salt);

            // 4) Decrypt
            CryptoPP::GCM<CryptoPP::AES>::Decryption dec;
            dec.SetKeyWithIV(key, key.size(), nonce, nonce.size());

            CryptoPP::StringSource ss(
                ciphertext,
                true,
                new CryptoPP::AuthenticatedDecryptionFilter(
                    dec,
                    new CryptoPP::StringSink(note)
                )
            );

            return true;
        }
        catch (const std::exception& ex) {
            std::cerr << "Error loading: " << ex.what() << std::endl;
            return false;
        }
    }
};

int main() {
    std::string password = "MyMasterPassword123!";
    SecureNotes notes(password);

    // Save a note
    std::string myNote =
        "This is my secret note!\n"
        "It's encrypted with my password.";
    if (notes.saveNote("secret.enc", myNote)) {
        std::cout << "Note saved successfully" << std::endl;
    }

    // Load the note (works even after program restart!)
    std::string loadedNote;
    if (notes.loadNote("secret.enc", loadedNote)) {
        std::cout << "Note loaded successfully:" << std::endl;
        std::cout << loadedNote << std::endl;
    }

    return 0;
}

This example demonstrates:

  • Password-based key derivation (Argon2)
  • Authenticated encryption (AES-GCM)
  • Proper salt + nonce handling (both persisted in file)
  • File I/O with encrypted data
  • Error handling

Compile:

g++ -std=c++11 secure_notes.cpp -o secure_notes -lcryptopp

You now have a solid foundation for using cryptopp-modern securely!