Secure Data Encryption Using AES-128 with HMAC Authentication

📅 May 30, 2025   ⏱ 5 min read

Introduction

Client-server communication is at the heart of most software systems. Whether you are building a licensing system, game cheat backend, or a secure embedded platform, you must ensure the data exchanged between client and server is protected from prying eyes. In this blog, we'll walk through implementing strong symmetric encryption using AES-128 in C++, combined with HMAC-SHA256 for data authentication and integrity.

We'll cover implementation, real-world security concerns, how attackers might try to break it, and how to harden it effectively.

Why Encryption Matters

Without encryption, any data sent over the network can be intercepted using a man-in-the-middle (MITM) attack. Attackers can sniff packets, extract authentication tokens, and even impersonate legitimate users. This is particularly dangerous in high-value domains like defense, telemetry, and financial systems.

Common Threats to Client-Server Systems

We can combat this using proper encryption (AES), key separation, per-packet nonces, and integrity verification (HMAC).

Client-Side C++ Implementation

// AES-128 + HMAC client encryption example
std::vector<uint8_t> EncryptAndProtect(const std::string& message, const std::string& key, const std::string& hmacKey) {
    // Generate a random IV (nonce)
    std::array<uint8_t, 16> iv = GenerateRandomIV();

    // Encrypt using AES-CBC mode
    std::vector<uint8_t> ciphertext = AES128_CBC_Encrypt(message, key, iv);

    // Concatenate IV + ciphertext
    std::vector<uint8_t> packet = iv;
    packet.insert(packet.end(), ciphertext.begin(), ciphertext.end());

    // Compute HMAC-SHA256 for the packet
    std::vector<uint8_t> hmac = HMAC_SHA256(packet, hmacKey);
    packet.insert(packet.end(), hmac.begin(), hmac.end());

    return packet;
}

Server-Side C++ Implementation

// AES-128 + HMAC server decryption example
std::string DecryptAndValidate(const std::vector<uint8_t>& packet, const std::string& key, const std::string& hmacKey) {
    // Extract IV and ciphertext
    std::array<uint8_t, 16> iv;
    std::copy(packet.begin(), packet.begin() + 16, iv.begin());

    std::vector<uint8_t> ciphertext(packet.begin() + 16, packet.end() - 32);
    std::vector<uint8_t> recvHMAC(packet.end() - 32, packet.end());

    // Verify HMAC
    std::vector<uint8_t> calcHMAC = HMAC_SHA256(std::vector(packet.begin(), packet.end() - 32), hmacKey);
    if (calcHMAC != recvHMAC)
        throw std::runtime_error("HMAC mismatch - tampering detected");

    // Decrypt and return plaintext
    return AES128_CBC_Decrypt(ciphertext, key, iv);
}

Best Practices

Conclusion

Encryption isn't just about hiding your data. It's about protecting the integrity of your system, preventing impersonation, and reducing the attack surface of your service. Whether you're building an authentication loader, internal tooling for secure deployments, or a real-time embedded system for defense, you need to understand these fundamentals. A strong AES + HMAC setup, combined with layered security and software hardening, is a solid foundation for any modern secure application.