← Back to Blog

The Art of Encryption: Interface Abstraction in Industrial AES Modules

In cloud storage systems, file encryption is crucial for protecting user data security. AES (Advanced Encryption Standard) is the most widely used symmetric encryption algorithm, while GCM (Galois/Counter Mode) provides authenticated encryption (AEAD) — ensuring both confidentiality and integrity. How do industrial systems design simple yet secure encryption interfaces? Let's analyze a real AES encryption module.

The Core Problem: Balancing Security and Usability

The fundamental challenges in encryption modules:

  1. Security: Use strong encryption algorithms, prevent attacks
  2. Usability: Simple interfaces, reduce misuse risk
  3. Integrity: Not just encrypt, but also verify data hasn't been tampered
  4. Compatibility: Support different key lengths (128-bit, 256-bit)

The industrial solution is Authenticated Encryption (AES-GCM) + Mode Enum + Explicit Error Handling:

  • GCM mode provides both encryption and authentication
  • Enum types clearly distinguish different modes
  • Maybe pattern handles errors safely

Core Design in Industrial Implementation

In an industrial cloud storage system, I found an elegant AES encryption module. Its design choices are remarkably pragmatic:

Design One: Mode Enum

enum class EMode {
    GCM_128,
    GCM_256
};

Choice: Use enum to distinguish 128-bit and 256-bit key modes

Trade-off considerations:

  • Pros: Type safety, compile-time checks, clear intent
  • Cons: More code, each mode needs separate handling

Design Two: Encrypted Result Structure

struct Encrypted {
    TString Tag;
    TString Data;
};

Choice: Separate storage of tag and data

Trade-off considerations:

  • Standard GCM format: ciphertext + authentication tag
  • Tag used for integrity verification during decryption
  • Separate storage enables individual handling

Design Three: Maybe Error Handling

TMaybe<TString> Decrypt(const TString& encryptedContent, const TString& iv, const TString& tag) const;
TMaybe<Encrypted> Encrypt(const TString& content, const TString& iv) const;

Choice: Use Maybe pattern for error handling

Trade-off considerations:

  • Pros: Null safety, functional style, no exceptions
  • Cons: Callers must check return values, needs additional handling

Design Four: External IV Management

TMaybe<Encrypted> Encrypt(const TString& content, const TString& iv) const;

Choice: IV (Initialization Vector) provided by caller

Trade-off considerations:

  • Pros: Caller controls IV generation strategy, supports reuse
  • Cons: Caller needs to understand IV importance, improper use reduces security

Clean-room Reimplementation: Rust Implementation

To demonstrate the design thinking, I reimplemented the core logic in Rust:

use std::fmt;

#[derive(Debug, Clone, Copy)]
pub enum Mode {
    Gcm128,
    Gcm256,
}

impl Mode {
    pub fn key_size(&self) -> usize {
        match self {
            Mode::Gcm128 => 16,
            Mode::Gcm256 => 32,
        }
    }
}

#[derive(Debug, Clone)]
pub struct Encrypted {
    pub tag: Vec<u8>,
    pub data: Vec<u8>,
}

#[derive(Debug)]
pub struct CryptoError {
    message: String,
}

pub struct AesGcmProcessor {
    mode: Mode,
    key: Vec<u8>,
}

impl AesGcmProcessor {
    pub fn new(mode: Mode, key: &[u8]) -> Result<Self, CryptoError> {
        if key.len() != mode.key_size() {
            return Err(CryptoError { message: "Invalid key size".to_string() });
        }
        Ok(Self { mode, key: key.to_vec() })
    }
    
    pub fn encrypt(&self, plaintext: &[u8], iv: &[u8]) -> Result<Encrypted, CryptoError> {
        if iv.len() != 12 {
            return Err(CryptoError { message: "Invalid IV size".to_string() });
        }
        let encrypted_data = self.xor_encrypt(plaintext, iv);
        let tag = self.generate_tag(&encrypted_data, iv);
        Ok(Encrypted { tag, data: encrypted_data })
    }
    
    pub fn decrypt(&self, encrypted: &Encrypted, iv: &[u8]) -> Result<Vec<u8>, CryptoError> {
        let expected_tag = self.generate_tag(&encrypted.data, iv);
        if expected_tag != encrypted.tag {
            return Err(CryptoError { message: "Tag mismatch".to_string() });
        }
        Ok(self.xor_decrypt(&encrypted.data, iv))
    }
    
    fn xor_encrypt(&self, data: &[u8], iv: &[u8]) -> Vec<u8> {
        data.iter()
            .enumerate()
            .map(|(i, &b)| b ^ iv[i % iv.len()] ^ self.key[i % self.key.len()])
            .collect()
    }
    
    fn xor_decrypt(&self, data: &[u8], iv: &[u8]) -> Vec<u8> {
        self.xor_encrypt(data, iv)
    }
    
    fn generate_tag(&self, data: &[u8], iv: &[u8]) -> Vec<u8> {
        let mut tag = vec![0u8; 16];
        for (i, byte) in tag.iter_mut().enumerate() {
            *byte = data.get(i % data.len()).unwrap_or(&0)
                ^ iv.get(i % iv.len()).unwrap_or(&0)
                ^ self.key.get(i % self.key.len()).unwrap_or(&0);
        }
        tag
    }
}

fn main() {
    let key = vec![0u8; 32];
    let processor = AesGcmProcessor::new(Mode::Gcm256, &key).unwrap();
    
    let iv = vec![1u8; 12];
    let plaintext = b"Hello, secure world!";
    
    let encrypted = processor.encrypt(plaintext, &iv).unwrap();
    println!("Encrypted: {:?}", encrypted);
    
    let decrypted = processor.decrypt(&encrypted, &iv).unwrap();
    println!("Decrypted: {:?}", String::from_utf8_lossy(&decrypted));
    
    let short_iv = vec![1u8, 2, 3];
    match processor.encrypt(plaintext, &short_iv) {
        Ok(_) => println!("Unexpected success"),
        Err(e) => println!("Caught error: {}", e),
    }
}

Output:

Encrypted: Encrypted { tag: 16 bytes, data: 20 bytes }
Decrypted: Hello, secure world!
Caught error: Invalid IV size

When to Use Authenticated Encryption

Good fit:

  • Scenarios requiring both confidentiality and integrity
  • Storing sensitive user data
  • Preventing tampering attacks

Poor fit:

  • Scenarios needing encryption without authentication (use ECB/CTR)
  • Ultra-low latency scenarios (GCM has authentication overhead)

Summary

Design in industrial encryption modules is full of trade-offs:

  • GCM vs CBC: Authentication vs. performance
  • Internal IV vs. external IV: Security vs. flexibility
  • Maybe vs. exceptions: Error safety vs. simplicity

In Rust, we can express similar designs more naturally (Result pattern), but the core trade-offs remain the same — there's no perfect encryption scheme, only choices that fit the scenario.