How fabrica-util's AES-GCM Implementation Ensures Data Integrity and Confidentiality

fabrica-util's AES-GCM implementation ensures data integrity and confidentiality by using authenticated encryption with associated data (AEAD), randomly generated nonces for each message, and strict authentication tag verification that rejects any tampered ciphertext.

The go-pantheon/fabrica-util repository provides a production-ready cryptographic utility specifically designed for game servers and clients. Its security/aes/aes.go file implements an AES-GCM codec that simultaneously protects message content from eavesdroppers and detects any unauthorized modifications, making it ideal for securing real-time game communication protocols.

Confidentiality Through AES-GCM Encryption

The implementation achieves confidentiality by encrypting plaintext using the AES block cipher in Galois/Counter Mode (GCM). In security/aes/aes.go, the Encrypt method generates a cryptographically secure random nonce via io.ReadFull(rand.Reader, nonce), ensuring each encryption operation uses unique initialization vectors.

The Cipher struct wraps an AES-GCM instance created through cipher.NewGCM. When Encrypt is called, it invokes c.block.Seal(nil, nonce, data, nil), which produces ciphertext that is computationally infeasible to decrypt without the original key. The nonce is prepended to the sealed output, resulting in a payload format of nonce‖ciphertext‖tag that travels securely over the network.

Integrity Verification via Authentication Tags

Data integrity is enforced through GCM's built-in authentication mechanism. Every encryption automatically appends a 16-byte authentication tag to the ciphertext. During decryption, the Decrypt method extracts the nonce from the front of the data and calls c.block.Open(nil, nonce, ciphertext, nil).

If any bit of the ciphertext, nonce, or authentication tag has been altered during transmission, Open returns an error. The implementation catches this in security/aes/aes.go and wraps it using errors.Wrap(err, "failed to decrypt data"), immediately signaling to the caller that the message integrity has been compromised and the data should be discarded.

Replay Protection with Random Nonce Generation

The implementation prevents replay attacks through strict nonce uniqueness guarantees. Each call to Encrypt creates a fresh nonce sized according to c.block.NonceSize()—standardly 12 bytes for AES-GCM. Because these nonces are randomly generated using crypto/rand, the probability of nonce reuse is negligible, ensuring that identical plaintexts produce completely different ciphertexts.

During decryption, the nonce is extracted from the first NonceSize() bytes of the received data (nonce := data[:c.block.NonceSize()]), then used to initialize the decryption context. This per-message uniqueness prevents attackers from replaying captured packets to spoof game state or actions.

Implementation Details in security/aes/aes.go

Key Validation and Initialization

The NewAESCipher function enforces strict key length requirements, accepting only 16, 24, or 32-byte keys for AES-128, AES-192, and AES-256 respectively. This validation occurs before the AEAD instance is constructed via cipher.NewGCM(block), ensuring only valid AES keys initialize the cipher.

Handling Empty Payloads

The API includes EncryptAllowEmpty and DecryptAllowEmpty variants that bypass the standard non-empty input checks. These convenience methods allow game protocols to transmit zero-length payloads—such as keepalive packets—without compromising the security model or triggering unnecessary errors.

Error Handling Strategy

All cryptographic failures utilize the repository's custom errors package, preserving full stack traces while providing clear failure messages to calling code. This allows game servers to log detailed security events while maintaining clean error boundaries between the codec and network layers.

Using the Cipher in Game Communication

The following example demonstrates encrypting a JSON game payload and verifying integrity on the receiving side:

package main

import (
    "fmt"
    "log"

    "github.com/go-pantheon/fabrica-util/security/aes"
    "github.com/go-pantheon/fabrica-util/xrand"
)

func main() {
    // Generate a 32-byte random key (AES-256)
    key, _ := xrand.RandAlphaNumString(32)

    // Initialize cipher instances for both client and server
    client, err := aes.NewAESCipher([]byte(key))
    if err != nil {
        log.Fatalf("cipher init: %v", err)
    }
    server, err := aes.NewAESCipher([]byte(key))
    if err != nil {
        log.Fatalf("cipher init: %v", err)
    }

    // Encrypt a game action payload
    payload := []byte(`{"action":"move","x":42,"y":7}`)
    enc, err := client.Encrypt(payload)
    if err != nil {
        log.Fatalf("encrypt: %v", err)
    }

    // Transmit enc over the network...
    // If any byte is altered here, decryption will fail

    // Decrypt and verify integrity
    dec, err := server.Decrypt(enc)
    if err != nil {
        log.Fatalf("decrypt (tampered?): %v", err)
    }

    fmt.Printf("Recovered payload: %s\n", string(dec))
}

If an attacker modifies the ciphertext during transit—flipping even a single bit—the Decrypt method returns an error because the GCM authentication tag verification fails, protecting the game state from corruption or exploits.

Summary

  • Confidentiality: Achieved through AES-GCM encryption with random 12-byte nonces generated via crypto/rand
  • Integrity: Enforced by 16-byte authentication tags that cause c.block.Open to error on any tampering
  • Replay Protection: Guaranteed by unique per-message nonces prepended to ciphertext in security/aes/aes.go
  • Key Safety: NewAESCipher validates 16, 24, or 32-byte keys before initialization
  • Flexibility: EncryptAllowEmpty and DecryptAllowEmpty support zero-length protocol payloads

Frequently Asked Questions

What key sizes does fabrica-util's AES-GCM implementation support?

The implementation supports standard AES key lengths: 16 bytes (AES-128), 24 bytes (AES-192), and 32 bytes (AES-256). The NewAESCipher function in security/aes/aes.go explicitly validates these lengths and returns an error for invalid key sizes, ensuring compatibility with the underlying Go standard library AES implementation.

How does the implementation prevent replay attacks?

Replay attacks are prevented through cryptographically random nonce generation. Each call to Encrypt generates a fresh nonce using io.ReadFull(rand.Reader, nonce), creating unique ciphertexts even for identical plaintexts. Since nonces are never reused with the same key, attackers cannot replay captured messages to spoof game actions.

What happens if ciphertext is tampered with during transmission?

If any portion of the ciphertext—including the prepended nonce or appended authentication tag—is modified, the Decrypt method detects the tampering. During the c.block.Open call, GCM tag verification fails and returns an error, which the implementation wraps and returns to the caller. The decrypted plaintext is automatically discarded, preventing corrupted data from reaching game logic.

Can the cipher handle empty payloads for keepalive messages?

Yes, through the EncryptAllowEmpty and DecryptAllowEmpty methods. While standard Encrypt and Decrypt reject empty byte slices, these variants bypass the non-empty checks to support protocols requiring zero-length payloads—such as TCP keepalives or heartbeat signals—without compromising the security model or requiring workarounds.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →