Symmetric Encryption

Symmetric Encryption

Symmetric encryption uses the same key for both encryption and decryption. cryptopp-modern provides comprehensive support for modern symmetric ciphers and modes of operation.

Supported Algorithms

Block Ciphers

  • AES (Advanced Encryption Standard) - Industry standard, FIPS approved
  • ChaCha20 - Modern stream cipher, excellent performance
  • Serpent - Highly secure block cipher
  • Twofish - Fast and flexible
  • Camellia - International standard (ISO/IEC 18033-3)
  • ARIA - Korean standard (RFC 5794)

Modes of Operation

  • GCM (Galois/Counter Mode) - Authenticated encryption (recommended)
  • CCM (Counter with CBC-MAC) - Authenticated encryption
  • EAX - Authenticated encryption
  • CBC (Cipher Block Chaining) - Traditional mode
  • CTR (Counter) - Stream cipher mode
  • CFB (Cipher Feedback) - Stream cipher mode
  • OFB (Output Feedback) - Stream cipher mode

Quick Comparison

AlgorithmKey SizeSpeedSecurityUse Case
AES-GCM128/192/256-bitVery Fast⭐⭐⭐⭐⭐General purpose (recommended)
ChaCha20-Poly1305256-bitVery Fast⭐⭐⭐⭐⭐Mobile, no AES hardware
AES-CBC128/192/256-bitFast⭐⭐⭐⭐Legacy compatibility
AES-CTR128/192/256-bitFast⭐⭐⭐⭐Parallel encryption

AES-GCM (Recommended)

AES-GCM provides both encryption and authentication, protecting against tampering. This is the recommended mode for most applications.

Encryption with AES-GCM

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

int main() {
    CryptoPP::AutoSeededRandomPool prng;

    // Generate random key and IV
    CryptoPP::SecByteBlock key(CryptoPP::AES::DEFAULT_KEYLENGTH);  // 128-bit
    CryptoPP::SecByteBlock iv(CryptoPP::AES::BLOCKSIZE);           // 128-bit
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    // Plaintext
    std::string plaintext = "Secret message to encrypt";
    std::string ciphertext, recovered;

    try {
        // Encryption
        CryptoPP::GCM<CryptoPP::AES>::Encryption enc;
        enc.SetKeyWithIV(key, key.size(), iv, iv.size());

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

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

        CryptoPP::StringSource ss2(ciphertext, true,
            new CryptoPP::AuthenticatedDecryptionFilter(dec,
                new CryptoPP::StringSink(recovered)
            )
        );

        std::cout << "Plaintext:  " << plaintext << std::endl;
        std::cout << "Recovered:  " << recovered << std::endl;
    }
    catch (const CryptoPP::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

AES-GCM with Additional Authenticated Data (AAD)

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

int main() {
    CryptoPP::AutoSeededRandomPool prng;

    CryptoPP::SecByteBlock key(CryptoPP::AES::DEFAULT_KEYLENGTH);
    CryptoPP::SecByteBlock iv(12);  // GCM commonly uses 96-bit IV
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    std::string plaintext = "Secret data";
    std::string aad = "Version:1.0,UserID:12345";  // Authenticated but not encrypted
    std::string ciphertext, recovered, recoveredAAD;

    try {
        // Encryption with AAD
        CryptoPP::GCM<CryptoPP::AES>::Encryption enc;
        enc.SetKeyWithIV(key, key.size(), iv, iv.size());

        CryptoPP::AuthenticatedEncryptionFilter ef(enc,
            new CryptoPP::StringSink(ciphertext)
        );

        ef.ChannelPut(CryptoPP::AAD_CHANNEL, (const CryptoPP::byte*)aad.data(), aad.size());
        ef.ChannelMessageEnd(CryptoPP::AAD_CHANNEL);

        ef.ChannelPut(CryptoPP::DEFAULT_CHANNEL, (const CryptoPP::byte*)plaintext.data(), plaintext.size());
        ef.ChannelMessageEnd(CryptoPP::DEFAULT_CHANNEL);

        // Decryption with AAD verification
        CryptoPP::GCM<CryptoPP::AES>::Decryption dec;
        dec.SetKeyWithIV(key, key.size(), iv, iv.size());

        CryptoPP::AuthenticatedDecryptionFilter df(dec,
            new CryptoPP::StringSink(recovered)
        );

        df.ChannelPut(CryptoPP::AAD_CHANNEL, (const CryptoPP::byte*)aad.data(), aad.size());
        df.ChannelMessageEnd(CryptoPP::AAD_CHANNEL);

        df.ChannelPut(CryptoPP::DEFAULT_CHANNEL, (const CryptoPP::byte*)ciphertext.data(), ciphertext.size());
        df.ChannelMessageEnd(CryptoPP::DEFAULT_CHANNEL);

        std::cout << "Plaintext: " << plaintext << std::endl;
        std::cout << "AAD: " << aad << std::endl;
        std::cout << "Recovered: " << recovered << std::endl;
        std::cout << "Authentication: OK" << std::endl;
    }
    catch (const CryptoPP::Exception& ex) {
        std::cerr << "Authentication failed: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

⚠️ Critical: GCM Nonce Reuse

GCM has a catastrophic failure mode if you reuse a nonce (IV) with the same key. Nonce reuse completely breaks GCM security and can leak your encryption key.

The Problem

// DANGEROUS: Reusing the same nonce
CryptoPP::SecByteBlock key(16);
CryptoPP::SecByteBlock nonce(12);
prng.GenerateBlock(key, key.size());
prng.GenerateBlock(nonce, nonce.size());  // Generated once

CryptoPP::GCM<CryptoPP::AES>::Encryption enc;

// First message - OK
enc.SetKeyWithIV(key, key.size(), nonce, nonce.size());
// ... encrypt message 1

// Second message - CATASTROPHIC FAILURE!
enc.SetKeyWithIV(key, key.size(), nonce, nonce.size());  // SAME NONCE!
// ... encrypt message 2
// Security is now completely broken!

What happens with nonce reuse:

  • Attacker can XOR two ciphertexts to cancel out the keystream
  • Authentication key is exposed
  • Attacker can forge authenticated messages
  • Entire key must be considered compromised

Safe GCM Usage

Option 1: Random Nonces (Recommended for most cases)

CryptoPP::AutoSeededRandomPool prng;
CryptoPP::SecByteBlock key(16);
prng.GenerateBlock(key, key.size());

// Generate NEW random nonce for EACH encryption
for (int i = 0; i < messages.size(); i++) {
    CryptoPP::SecByteBlock nonce(12);  // 96-bit nonce
    prng.GenerateBlock(nonce, nonce.size());  // NEW NONCE EVERY TIME

    CryptoPP::GCM<CryptoPP::AES>::Encryption enc;
    enc.SetKeyWithIV(key, key.size(), nonce, nonce.size());

    // ... encrypt message
    // Store nonce with ciphertext: nonce || ciphertext || tag
}

Limitations:

  • With 96-bit random nonces, you have ~2^48 encryptions before collision risk
  • After 2^32 messages, consider rotating the key
  • Never encrypt more than 2^48 messages with the same key

Option 2: Counter-Based Nonces

#include <cryptopp/aes.h>
#include <cryptopp/gcm.h>
#include <cstring>
#include <atomic>

class SafeGCM {
private:
    CryptoPP::SecByteBlock key;
    std::atomic<uint64_t> counter;

public:
    SafeGCM() : key(16), counter(0) {
        CryptoPP::AutoSeededRandomPool prng;
        prng.GenerateBlock(key, key.size());
    }

    std::string encrypt(const std::string& plaintext) {
        // Get next counter value
        uint64_t count = counter.fetch_add(1);

        // Ensure we don't overflow
        if (count >= (1ULL << 48)) {
            throw std::runtime_error("Nonce space exhausted - must rotate key!");
        }

        // Build nonce: 32-bit fixed || 64-bit counter
        CryptoPP::SecByteBlock nonce(12);
        memset(nonce, 0, 4);  // Fixed 32-bit prefix
        memcpy(nonce + 4, &count, 8);  // 64-bit counter

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

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

        // Return: nonce || ciphertext (includes auth tag)
        return std::string((char*)nonce.data(), nonce.size()) + ciphertext;
    }

    bool decrypt(const std::string& stored, std::string& recovered) {
        if (stored.size() < 12 + 16) {  // nonce + min ciphertext + tag
            return false;
        }

        // Extract nonce and ciphertext
        CryptoPP::SecByteBlock nonce((const CryptoPP::byte*)stored.data(), 12);
        std::string ciphertext = stored.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(recovered)
                )
            );
            return true;
        }
        catch (const CryptoPP::Exception&) {
            return false;
        }
    }
};

int main() {
    SafeGCM gcm;

    // Safe: Each encryption uses a unique nonce
    std::string enc1 = gcm.encrypt("Message 1");
    std::string enc2 = gcm.encrypt("Message 2");
    std::string enc3 = gcm.encrypt("Message 3");

    std::string dec;
    if (gcm.decrypt(enc1, dec)) {
        std::cout << "Decrypted: " << dec << std::endl;
    }

    return 0;
}

Benefits of counter-based nonces:

  • No collision risk (deterministic)
  • Can encrypt 2^64 messages safely
  • Track usage and enforce key rotation
  • Suitable for high-throughput systems

Option 3: Derived Nonces (Advanced)

For protocols where you can’t store nonces:

// Derive unique nonce from message-specific data
// WARNING: Derivation input MUST be unique per message!
std::string deriveNonce(const std::string& messageId) {
    CryptoPP::SHA256 hash;
    std::string digest;

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

    // Use first 12 bytes of hash as nonce
    return digest.substr(0, 12);
}

// messageId must be unique (e.g., UUID, database ID, timestamp+counter)
std::string nonce = deriveNonce(uniqueMessageId);

Key Rotation

When to rotate keys:

  • After 2^32 encryptions (4 billion) - hard limit for random nonces
  • After 2^48 encryptions with counter-based nonces
  • On any suspected nonce reuse
  • Periodically as policy (e.g., every 90 days)
class KeyRotatingGCM {
private:
    CryptoPP::SecByteBlock currentKey;
    std::atomic<uint64_t> messageCount;
    const uint64_t MAX_MESSAGES = (1ULL << 32);  // 2^32

public:
    void checkAndRotate() {
        if (messageCount.load() >= MAX_MESSAGES) {
            // Generate new key
            CryptoPP::AutoSeededRandomPool prng;
            currentKey.resize(16);
            prng.GenerateBlock(currentKey, currentKey.size());
            messageCount = 0;

            // Store new key securely and notify system
        }
    }

    std::string encrypt(const std::string& plaintext) {
        checkAndRotate();
        messageCount++;
        // ... perform encryption with currentKey
    }
};

Detection and Recovery

If you suspect nonce reuse has occurred:

  1. Immediately stop using the compromised key
  2. Generate a new key
  3. Re-encrypt all data with the new key
  4. Investigate the cause to prevent recurrence
  5. Audit logs for potential exploitation

Summary: GCM Nonce Safety

DO:

  • Generate a new random nonce for every encryption
  • Use counter-based nonces with proper tracking
  • Store nonces with ciphertext (they’re not secret)
  • Rotate keys before reaching nonce limits
  • Use 96-bit (12-byte) nonces for optimal performance

DON’T:

  • Ever reuse a nonce with the same key
  • Use predictable nonces without proper construction
  • Exceed 2^32 encryptions with random nonces
  • Assume the library will prevent reuse (it won’t)

GCM is excellent when used correctly, but unforgiving of mistakes. If you’re uncertain about nonce management, consider using ChaCha20-Poly1305 or implementing a well-tested wrapper class.

ChaCha20

ChaCha20 is a modern stream cipher designed by Daniel J. Bernstein. It’s particularly useful when AES hardware acceleration is not available.

ChaCha20-Poly1305 Encryption

#include <cryptopp/chacha.h>
#include <cryptopp/poly1305.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <iostream>

int main() {
    CryptoPP::AutoSeededRandomPool prng;

    // ChaCha20 uses 256-bit key and 96-bit IV
    CryptoPP::SecByteBlock key(32);
    CryptoPP::SecByteBlock iv(12);
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    std::string plaintext = "Message encrypted with ChaCha20";
    std::string ciphertext, recovered;

    try {
        // Encryption
        CryptoPP::ChaCha::Encryption enc;
        enc.SetKeyWithIV(key, key.size(), iv, iv.size());

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

        // Decryption
        CryptoPP::ChaCha::Decryption dec;
        dec.SetKeyWithIV(key, key.size(), iv, iv.size());

        CryptoPP::StringSource ss2(ciphertext, true,
            new CryptoPP::StreamTransformationFilter(dec,
                new CryptoPP::StringSink(recovered)
            )
        );

        std::cout << "Plaintext:  " << plaintext << std::endl;
        std::cout << "Recovered:  " << recovered << std::endl;
    }
    catch (const CryptoPP::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

AES-CBC

CBC mode is a traditional block cipher mode. Note: CBC requires manual padding and does not provide authentication.

AES-CBC Encryption

#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <iostream>

int main() {
    CryptoPP::AutoSeededRandomPool prng;

    // Key and IV
    CryptoPP::SecByteBlock key(CryptoPP::AES::DEFAULT_KEYLENGTH);
    CryptoPP::SecByteBlock iv(CryptoPP::AES::BLOCKSIZE);
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    std::string plaintext = "CBC mode encryption example";
    std::string ciphertext, recovered;

    try {
        // Encryption
        CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption enc;
        enc.SetKeyWithIV(key, key.size(), iv);

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

        // Decryption
        CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption dec;
        dec.SetKeyWithIV(key, key.size(), iv);

        CryptoPP::StringSource ss2(ciphertext, true,
            new CryptoPP::StreamTransformationFilter(dec,
                new CryptoPP::StringSink(recovered)
            )
        );

        std::cout << "Plaintext:  " << plaintext << std::endl;
        std::cout << "Recovered:  " << recovered << std::endl;
    }
    catch (const CryptoPP::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

AES-CBC with HMAC (Encrypt-then-MAC)

CBC mode does not provide authentication. For secure CBC usage, combine it with HMAC using the Encrypt-then-MAC construction.

Encryption with HMAC-SHA256

#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/hmac.h>
#include <cryptopp/sha.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <cryptopp/hex.h>
#include <iostream>

int main() {
    CryptoPP::AutoSeededRandomPool prng;

    // Separate keys for encryption and authentication (important!)
    CryptoPP::SecByteBlock encKey(CryptoPP::AES::DEFAULT_KEYLENGTH);  // 128-bit
    CryptoPP::SecByteBlock macKey(32);  // 256-bit for HMAC-SHA256
    CryptoPP::SecByteBlock iv(CryptoPP::AES::BLOCKSIZE);

    prng.GenerateBlock(encKey, encKey.size());
    prng.GenerateBlock(macKey, macKey.size());
    prng.GenerateBlock(iv, iv.size());

    std::string plaintext = "Sensitive data requiring authentication";
    std::string ciphertext, mac;

    try {
        // Step 1: Encrypt with AES-CBC
        CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption enc;
        enc.SetKeyWithIV(encKey, encKey.size(), iv);

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

        // Step 2: Compute HMAC over IV + Ciphertext (Encrypt-then-MAC)
        CryptoPP::HMAC<CryptoPP::SHA256> hmac(macKey, macKey.size());

        std::string authData = std::string((char*)iv.data(), iv.size()) + ciphertext;

        CryptoPP::StringSource ss2(authData, true,
            new CryptoPP::HashFilter(hmac,
                new CryptoPP::StringSink(mac)
            )
        );

        std::cout << "Encryption successful" << std::endl;
        std::cout << "Ciphertext length: " << ciphertext.size() << " bytes" << std::endl;
        std::cout << "MAC length: " << mac.size() << " bytes" << std::endl;

        // In practice, store: IV || Ciphertext || MAC
        std::string stored = std::string((char*)iv.data(), iv.size()) + ciphertext + mac;

    }
    catch (const CryptoPP::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Decryption with HMAC Verification

#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/hmac.h>
#include <cryptopp/sha.h>
#include <cryptopp/filters.h>
#include <cryptopp/secblock.h>
#include <iostream>
#include <string>

bool decryptAndVerify(
    const std::string& stored,
    const CryptoPP::SecByteBlock& encKey,
    const CryptoPP::SecByteBlock& macKey,
    std::string& recovered)
{
    const size_t IV_SIZE = CryptoPP::AES::BLOCKSIZE;
    const size_t MAC_SIZE = CryptoPP::SHA256::DIGESTSIZE;

    // Validate minimum size
    if (stored.size() < IV_SIZE + MAC_SIZE) {
        std::cerr << "Invalid stored data size" << std::endl;
        return false;
    }

    // Extract components: IV || Ciphertext || MAC
    std::string ivStr = stored.substr(0, IV_SIZE);
    std::string ciphertext = stored.substr(IV_SIZE, stored.size() - IV_SIZE - MAC_SIZE);
    std::string receivedMac = stored.substr(stored.size() - MAC_SIZE);

    CryptoPP::SecByteBlock iv((const CryptoPP::byte*)ivStr.data(), ivStr.size());

    try {
        // Step 1: Verify HMAC (authenticate before decrypting)
        CryptoPP::HMAC<CryptoPP::SHA256> hmac(macKey, macKey.size());

        std::string authData = ivStr + ciphertext;
        std::string computedMac;

        CryptoPP::StringSource ss1(authData, true,
            new CryptoPP::HashFilter(hmac,
                new CryptoPP::StringSink(computedMac)
            )
        );

        // Constant-time comparison
        if (!CryptoPP::VerifyBufsEqual(
                (const CryptoPP::byte*)computedMac.data(),
                (const CryptoPP::byte*)receivedMac.data(),
                MAC_SIZE)) {
            std::cerr << "HMAC verification failed - data may be tampered!" << std::endl;
            return false;
        }

        // Step 2: Decrypt (only if HMAC verified)
        CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption dec;
        dec.SetKeyWithIV(encKey, encKey.size(), iv);

        CryptoPP::StringSource ss2(ciphertext, true,
            new CryptoPP::StreamTransformationFilter(dec,
                new CryptoPP::StringSink(recovered)
            )
        );

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

int main() {
    CryptoPP::AutoSeededRandomPool prng;

    CryptoPP::SecByteBlock encKey(CryptoPP::AES::DEFAULT_KEYLENGTH);
    CryptoPP::SecByteBlock macKey(32);
    prng.GenerateBlock(encKey, encKey.size());
    prng.GenerateBlock(macKey, macKey.size());

    // Simulate stored encrypted data
    std::string stored = "...";  // IV || Ciphertext || MAC
    std::string recovered;

    if (decryptAndVerify(stored, encKey, macKey, recovered)) {
        std::cout << "Decryption successful" << std::endl;
        std::cout << "Recovered: " << recovered << std::endl;
    } else {
        std::cerr << "Decryption or verification failed" << std::endl;
    }

    return 0;
}

Complete Example: Encrypt-then-MAC

#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/hmac.h>
#include <cryptopp/sha.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <iostream>

class EncryptThenMAC {
private:
    CryptoPP::SecByteBlock encKey;
    CryptoPP::SecByteBlock macKey;

public:
    EncryptThenMAC() : encKey(CryptoPP::AES::DEFAULT_KEYLENGTH), macKey(32) {
        CryptoPP::AutoSeededRandomPool prng;
        prng.GenerateBlock(encKey, encKey.size());
        prng.GenerateBlock(macKey, macKey.size());
    }

    std::string encrypt(const std::string& plaintext) {
        CryptoPP::AutoSeededRandomPool prng;

        // Generate random IV
        CryptoPP::SecByteBlock iv(CryptoPP::AES::BLOCKSIZE);
        prng.GenerateBlock(iv, iv.size());

        // Encrypt
        std::string ciphertext;
        CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption enc;
        enc.SetKeyWithIV(encKey, encKey.size(), iv);

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

        // Compute HMAC over IV + Ciphertext
        std::string mac;
        std::string authData = std::string((char*)iv.data(), iv.size()) + ciphertext;

        CryptoPP::HMAC<CryptoPP::SHA256> hmac(macKey, macKey.size());
        CryptoPP::StringSource(authData, true,
            new CryptoPP::HashFilter(hmac,
                new CryptoPP::StringSink(mac)
            )
        );

        // Return: IV || Ciphertext || MAC
        return std::string((char*)iv.data(), iv.size()) + ciphertext + mac;
    }

    bool decrypt(const std::string& stored, std::string& recovered) {
        const size_t IV_SIZE = CryptoPP::AES::BLOCKSIZE;
        const size_t MAC_SIZE = CryptoPP::SHA256::DIGESTSIZE;

        if (stored.size() < IV_SIZE + MAC_SIZE) {
            return false;
        }

        // Extract components
        std::string ivStr = stored.substr(0, IV_SIZE);
        std::string ciphertext = stored.substr(IV_SIZE, stored.size() - IV_SIZE - MAC_SIZE);
        std::string receivedMac = stored.substr(stored.size() - MAC_SIZE);

        // Verify HMAC
        std::string computedMac;
        std::string authData = ivStr + ciphertext;

        CryptoPP::HMAC<CryptoPP::SHA256> hmac(macKey, macKey.size());
        CryptoPP::StringSource(authData, true,
            new CryptoPP::HashFilter(hmac,
                new CryptoPP::StringSink(computedMac)
            )
        );

        if (!CryptoPP::VerifyBufsEqual(
                (const CryptoPP::byte*)computedMac.data(),
                (const CryptoPP::byte*)receivedMac.data(),
                MAC_SIZE)) {
            return false;
        }

        // Decrypt
        CryptoPP::SecByteBlock iv((const CryptoPP::byte*)ivStr.data(), ivStr.size());
        CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption dec;
        dec.SetKeyWithIV(encKey, encKey.size(), iv);

        try {
            CryptoPP::StringSource(ciphertext, true,
                new CryptoPP::StreamTransformationFilter(dec,
                    new CryptoPP::StringSink(recovered)
                )
            );
            return true;
        }
        catch (const CryptoPP::Exception&) {
            return false;
        }
    }
};

int main() {
    EncryptThenMAC crypto;

    std::string plaintext = "Secret message with authentication";

    // Encrypt
    std::string encrypted = crypto.encrypt(plaintext);
    std::cout << "Encrypted length: " << encrypted.size() << " bytes" << std::endl;

    // Decrypt
    std::string decrypted;
    if (crypto.decrypt(encrypted, decrypted)) {
        std::cout << "Original:  " << plaintext << std::endl;
        std::cout << "Decrypted: " << decrypted << std::endl;
        std::cout << "Match: " << (plaintext == decrypted ? "YES" : "NO") << std::endl;
    } else {
        std::cerr << "Decryption failed!" << std::endl;
    }

    // Test tampering detection
    encrypted[20] ^= 0x01;  // Flip one bit
    if (!crypto.decrypt(encrypted, decrypted)) {
        std::cout << "Tampering detected correctly!" << std::endl;
    }

    return 0;
}

Security Best Practices for CBC + HMAC

Always use separate keys:

// GOOD: Different keys for encryption and MAC
CryptoPP::SecByteBlock encKey(16);
CryptoPP::SecByteBlock macKey(32);

// BAD: Never use the same key
// CryptoPP::SecByteBlock key(16);
// Use 'key' for both encryption and HMAC - NO!

Always use Encrypt-then-MAC (not MAC-then-Encrypt):

// GOOD: Encrypt then compute HMAC over ciphertext
std::string ciphertext = encrypt(plaintext);
std::string mac = hmac(ciphertext);

// BAD: MAC-then-Encrypt is vulnerable to padding oracle attacks
// std::string mac = hmac(plaintext);
// std::string ciphertext = encrypt(plaintext + mac);  // NO!

Verify HMAC before decrypting:

// GOOD: Check authentication first
if (verify_hmac(data)) {
    plaintext = decrypt(data);
}

// BAD: Decrypt then verify
// plaintext = decrypt(data);  // NO! Decrypt untrusted data
// if (verify_hmac(data)) { ... }

Include IV in HMAC computation:

// GOOD: HMAC covers IV and ciphertext
std::string authData = iv + ciphertext;
std::string mac = hmac(authData);

// BAD: Not authenticating IV allows attacks
// std::string mac = hmac(ciphertext);  // NO!

Use constant-time comparison:

// GOOD: Prevents timing attacks
bool valid = CryptoPP::VerifyBufsEqual(computed, received, MAC_SIZE);

// BAD: Variable-time comparison
// bool valid = (computed == received);  // NO!

AES-CTR

Counter mode turns a block cipher into a stream cipher and allows parallel encryption/decryption.

#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/filters.h>
#include <cryptopp/osrng.h>
#include <iostream>

int main() {
    CryptoPP::AutoSeededRandomPool prng;

    CryptoPP::SecByteBlock key(CryptoPP::AES::DEFAULT_KEYLENGTH);
    CryptoPP::SecByteBlock iv(CryptoPP::AES::BLOCKSIZE);
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    std::string plaintext = "CTR mode allows parallel processing";
    std::string ciphertext, recovered;

    // Encryption
    CryptoPP::CTR_Mode<CryptoPP::AES>::Encryption enc;
    enc.SetKeyWithIV(key, key.size(), iv);

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

    // Decryption
    CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption dec;
    dec.SetKeyWithIV(key, key.size(), iv);

    CryptoPP::StringSource ss2(ciphertext, true,
        new CryptoPP::StreamTransformationFilter(dec,
            new CryptoPP::StringSink(recovered)
        )
    );

    std::cout << "Plaintext:  " << plaintext << std::endl;
    std::cout << "Recovered:  " << recovered << std::endl;

    return 0;
}

Key Sizes

AES

  • AES-128: 128-bit key (16 bytes) - Fast, secure for most uses
  • AES-192: 192-bit key (24 bytes) - Higher security margin
  • AES-256: 256-bit key (32 bytes) - Maximum security
// AES-128
CryptoPP::SecByteBlock key128(CryptoPP::AES::DEFAULT_KEYLENGTH);  // 16 bytes

// AES-256
CryptoPP::SecByteBlock key256(CryptoPP::AES::MAX_KEYLENGTH);      // 32 bytes

CryptoPP::GCM<CryptoPP::AES>::Encryption enc;
enc.SetKeyWithIV(key256, key256.size(), iv, iv.size());

ChaCha20

  • Fixed: 256-bit key (32 bytes)
CryptoPP::SecByteBlock key(32);  // ChaCha20 always uses 256-bit keys

IV/Nonce Requirements

GCM Mode

  • Size: 96 bits (12 bytes) recommended
  • Uniqueness: MUST be unique for each encryption with same key
  • Random or counter: Both acceptable
CryptoPP::SecByteBlock iv(12);  // 96-bit IV for GCM
prng.GenerateBlock(iv, iv.size());

CBC Mode

  • Size: 128 bits (16 bytes) for AES
  • Uniqueness: Must be unpredictable
  • Random: Should be random
CryptoPP::SecByteBlock iv(CryptoPP::AES::BLOCKSIZE);  // 128-bit
prng.GenerateBlock(iv, iv.size());

CTR Mode

  • Size: 128 bits (16 bytes) for AES
  • Uniqueness: Must be unique (can use counter)
  • Never reuse: Critical for security

Security Best Practices

Always Use Authenticated Encryption

// GOOD: AES-GCM provides authentication
CryptoPP::GCM<CryptoPP::AES>::Encryption enc;

// BAD: CBC does not authenticate
// Use HMAC separately if you must use CBC

Never Reuse IV/Nonce

// GOOD: Generate new IV for each encryption
for (int i = 0; i < messages.size(); i++) {
    prng.GenerateBlock(iv, iv.size());
    // ... encrypt with new IV
}

// BAD: Reusing IV breaks security
// CryptoPP::SecByteBlock iv(12);
// prng.GenerateBlock(iv, iv.size());  // Only once - NO!

Use Strong Random Keys

// GOOD: Cryptographically secure random
CryptoPP::AutoSeededRandomPool prng;
prng.GenerateBlock(key, key.size());

// BAD: Weak randomness
// srand(time(NULL));  // NO!
// for (int i = 0; i < key.size(); i++)
//     key[i] = rand();  // NO!

Store IV with Ciphertext

// Typical storage format: IV || Ciphertext || AuthTag (for GCM)
std::string stored = ivStr + ciphertext;  // IV is not secret

// When decrypting, extract IV first
std::string ivStr = stored.substr(0, 12);
std::string ciphertext = stored.substr(12);

When to Use Each Mode

Use AES-GCM when:

  • You need encryption and authentication
  • Hardware AES acceleration is available
  • General-purpose encryption (most common case)

Use ChaCha20-Poly1305 when:

  • No AES hardware acceleration
  • Mobile devices
  • Software-only implementations

Use AES-CBC when:

  • Legacy system compatibility
  • You can add HMAC for authentication
  • Specific protocol requirements

Use AES-CTR when:

  • You need parallelizable encryption
  • Random access to encrypted data
  • Streaming applications (with authentication)

Performance Considerations

Hardware Acceleration

Modern CPUs have AES-NI instructions that make AES extremely fast:

  • With AES-NI: AES-GCM > 2 GB/s
  • Without AES-NI: ChaCha20 > 500 MB/s

Benchmarking

#include <cryptopp/aes.h>
#include <cryptopp/gcm.h>
#include <chrono>

// Benchmark your specific use case
auto start = std::chrono::high_resolution_clock::now();
// ... perform encryption
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

Building

All symmetric ciphers are included by default in cryptopp-modern.

#include <cryptopp/aes.h>
#include <cryptopp/gcm.h>
#include <cryptopp/chacha.h>
#include <cryptopp/modes.h>

Compile:

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

Further Reading