AES-256 Security-Focused API Documentation
Version: 1.0
Last Updated: 2025-07-05
**Security Classification: PUBLIC
Author: Phantom (phantom@metamui.id)
Overview
AES-256 (Advanced Encryption Standard with 256-bit keys) is a symmetric block cipher standardized by NIST. It operates on 128-bit blocks and is widely deployed with hardware acceleration on modern processors. This documentation covers secure usage of AES-256 across different modes of operation.
Security Level: 256-bit key, 128-bit block
Key Size: 32 bytes (256 bits)
Block Size: 16 bytes (128 bits)
Rounds: 14
Security Warnings ⚠️
- Mode Selection Critical: ECB mode is NEVER safe - use GCM, CTR, or CBC
- IV/Nonce Uniqueness: Reusing IV/nonce with same key breaks security
- Padding Oracle: CBC mode vulnerable without authenticated encryption
- Block Size Limit: Birthday bound at 2^64 blocks (~256 EB) per key
- Not Authenticated: AES alone doesn’t provide authenticity - use AES-GCM
API Functions
generate_key() -> bytes[32]
Security Contract:
- Preconditions:
- System CSPRNG must be available
- Sufficient entropy in system
- Postconditions:
- Returns 32 bytes of cryptographic randomness
- Key suitable for AES-256 operations
- Each key is independent
Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Weak Keys | ✅ | No weak keys in AES | | Related Key | ✅ | With proper generation | | Predictability | ⚠️ | Depends on RNG quality | | Key Recovery | ✅ | 2^256 operations |
Secure Usage Example:
import os
import secrets
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
class AES256KeyManager:
def __init__(self):
self.keys = {}
self.key_lifetime = 30 * 24 * 3600 # 30 days
def generate_data_key(self) -> tuple:
"""Generate new data encryption key"""
# Generate AES-256 key
key = secrets.token_bytes(32)
# Generate key ID
key_id = secrets.token_urlsafe(16)
# Store with metadata
self.keys[key_id] = {
'key': key,
'created': time.time(),
'usage_count': 0,
'bytes_encrypted': 0
}
return key_id, key
def rotate_keys(self):
"""Rotate old or overused keys"""
current_time = time.time()
for key_id, key_data in list(self.keys.items()):
# Check age
if current_time - key_data['created'] > self.key_lifetime:
self._retire_key(key_id)
continue
# Check usage limits (NIST recommends)
if key_data['bytes_encrypted'] > 2**39: # ~550 GB
self._retire_key(key_id)
continue
if key_data['usage_count'] > 2**32:
self._retire_key(key_id)
def _retire_key(self, key_id: str):
"""Securely retire a key"""
key_data = self.keys[key_id]
# Re-encrypt data under new key
new_key_id, new_key = self.generate_data_key()
self._reencrypt_data(key_data['key'], new_key)
# Securely delete old key
secure_zero(key_data['key'])
del self.keys[key_id]
# SECURE: Key wrapping for storage
def wrap_aes_key(kek: bytes, key: bytes) -> bytes:
"""AES Key Wrap (RFC 3394) for secure key storage"""
from cryptography.hazmat.primitives.keywrap import aes_key_wrap
# Wrap the key
wrapped = aes_key_wrap(
wrapping_key=kek,
key_to_wrap=key,
backend=default_backend()
)
return wrapped
# SECURE: Hardware security module integration
class HSMKeyManager:
def __init__(self, hsm_client):
self.hsm = hsm_client
def generate_aes_key(self, key_label: str) -> str:
"""Generate AES key in HSM"""
key_spec = {
'algorithm': 'AES',
'length': 256,
'extractable': False,
'usage': ['ENCRYPT', 'DECRYPT']
}
# Key never leaves HSM
key_handle = self.hsm.generate_key(key_label, key_spec)
return key_handle
Common Mistakes:
# INSECURE: Weak key generation
key = hashlib.sha256(b"my password").digest() # NOT RANDOM!
# INSECURE: Predictable keys
key = os.urandom(32) # OK on Unix, bad on some systems
# Use: key = secrets.token_bytes(32)
# INSECURE: Key reuse across applications
GLOBAL_KEY = b"0123456789abcdef" * 2 # NEVER hardcode!
# INSECURE: Not rotating keys
# Using same key for years without rotation
encrypt_gcm(key: bytes[32], plaintext: bytes, associated_data: bytes = b"") -> (bytes, bytes[16], bytes[12])
Security Contract:
- Preconditions:
keymust be 32 bytes- Unique nonce must be generated
associated_dataauthenticated but not encrypted
- Postconditions:
- Returns (ciphertext, tag, nonce) tuple
- Provides authenticated encryption
- Nonce must be stored with ciphertext
Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Confidentiality | ✅ | AES-256 encryption | | Authenticity | ✅ | GCM authentication | | Nonce Reuse | ❌ | Catastrophic if reused | | Forgery | ✅ | 2^128 attempts needed | | Padding Oracle | ✅ | No padding in GCM |
Security Requirements:
- NEVER reuse nonce with same key
- Verify tag before using plaintext
- Limit data per key to 2^39 bytes
- Use 96-bit nonces for best performance
Secure Usage Example:
use aes_gcm::{
aead::{Aead, KeyInit, OsRng},
Aes256Gcm, Nonce, Key
};
pub struct SecureAesGcm {
cipher: Aes256Gcm,
nonce_counter: u64,
}
impl SecureAesGcm {
pub fn new(key: &[u8; 32]) -> Self {
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
Self {
cipher,
nonce_counter: 0,
}
}
pub fn encrypt_with_ad(
&mut self,
plaintext: &[u8],
associated_data: &[u8],
) -> Result<EncryptedData, Error> {
// Generate unique nonce
let nonce = self.generate_nonce()?;
// Create payload with associated data
let payload = Payload {
msg: plaintext,
aad: associated_data,
};
// Encrypt
let ciphertext = self.cipher
.encrypt(&nonce, payload)
.map_err(|_| Error::EncryptionFailed)?;
// Split ciphertext and tag
let (ct, tag) = ciphertext.split_at(ciphertext.len() - 16);
Ok(EncryptedData {
nonce: nonce.to_vec(),
ciphertext: ct.to_vec(),
tag: tag.to_vec(),
aad: associated_data.to_vec(),
})
}
fn generate_nonce(&mut self) -> Result<Nonce<U12>, Error> {
// Counter-based nonce generation
if self.nonce_counter >= 2u64.pow(32) {
return Err(Error::NonceExhaustion);
}
let mut nonce_bytes = [0u8; 12];
nonce_bytes[..8].copy_from_slice(&self.nonce_counter.to_le_bytes());
self.nonce_counter += 1;
Ok(Nonce::from_slice(&nonce_bytes).copied())
}
}
// SECURE: File encryption with AES-GCM
pub fn encrypt_file(
file_path: &Path,
key: &[u8; 32],
) -> Result<(), Error> {
let metadata = std::fs::metadata(file_path)?;
// Read file
let plaintext = std::fs::read(file_path)?;
// Create associated data from metadata
let ad = format!(
"file:{},size:{},modified:{}",
file_path.display(),
metadata.len(),
metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs()
);
// Encrypt with AES-GCM
let mut cipher = SecureAesGcm::new(key);
let encrypted = cipher.encrypt_with_ad(&plaintext, ad.as_bytes())?;
// Write encrypted file
let enc_path = file_path.with_extension("enc");
let mut file = File::create(&enc_path)?;
// Write header
file.write_all(b"AES256GCM")?;
file.write_all(&encrypted.nonce)?;
file.write_all(&encrypted.tag)?;
file.write_all(&(ad.len() as u32).to_le_bytes())?;
file.write_all(ad.as_bytes())?;
file.write_all(&encrypted.ciphertext)?;
// Securely delete original
secure_delete(file_path)?;
Ok(())
}
encrypt_ctr(key: bytes[32], plaintext: bytes, nonce: bytes[16]) -> bytes
Security Contract:
- Preconditions:
keymust be 32 bytesnoncemust be unique per key- No authentication provided
- Postconditions:
- Returns ciphertext same length as plaintext
- Provides confidentiality only
- Malleable without authentication
Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Confidentiality | ✅ | Stream cipher mode | | Authenticity | ❌ | No authentication | | Bit Flipping | ❌ | Malleable | | Nonce Reuse | ❌ | XOR reveals plaintext | | Known Plaintext | ✅ | Still secure |
Security Requirements:
- Add authentication (HMAC/Poly1305)
- Never reuse nonce/counter
- Consider AES-GCM instead
- Limit counter wraparound
Secure Usage Example:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import struct
class SecureAESCTR:
def __init__(self, key: bytes):
if len(key) != 32:
raise ValueError("Key must be 32 bytes")
self.key = key
def encrypt_with_hmac(self, plaintext: bytes) -> dict:
"""CTR encryption with HMAC authentication"""
# Generate random IV
iv = os.urandom(16)
# Encrypt using CTR mode
cipher = Cipher(
algorithms.AES(self.key),
modes.CTR(iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
# Add authentication
hmac_key = hkdf_expand(self.key, b"hmac-key", 32)
h = hmac.new(hmac_key, digestmod=hashlib.sha256)
h.update(iv)
h.update(ciphertext)
tag = h.digest()
# Clear derived key
secure_zero(hmac_key)
return {
'ciphertext': ciphertext,
'iv': iv,
'tag': tag,
'algorithm': 'AES-256-CTR-HMAC-SHA256'
}
def create_seekable_cipher(self, base_nonce: bytes) -> 'SeekableCipher':
"""Create cipher that can encrypt/decrypt at any position"""
return self.SeekableCipher(self.key, base_nonce)
class SeekableCipher:
def __init__(self, key: bytes, base_nonce: bytes):
self.key = key
self.base_nonce = base_nonce[:12] # 96-bit nonce
def process_block(self, block_num: int, data: bytes) -> bytes:
"""Encrypt/decrypt specific block"""
# Create counter for this block
counter = self.base_nonce + struct.pack('>I', block_num)
# Create cipher for this block
cipher = Cipher(
algorithms.AES(self.key),
modes.CTR(counter),
backend=default_backend()
)
processor = cipher.encryptor() # Same for encrypt/decrypt
return processor.update(data) + processor.finalize()
# SECURE: Stream encryption
def encrypt_stream(input_stream, output_stream, key: bytes):
"""Encrypt stream with CTR mode"""
# Generate random nonce
nonce = os.urandom(12)
counter = 0
# Write header
output_stream.write(b'AES256CTR')
output_stream.write(nonce)
# Initialize HMAC
hmac_key = hkdf_expand(key, b"stream-hmac", 32)
h = hmac.new(hmac_key, digestmod=hashlib.sha256)
h.update(nonce)
# Process in chunks
while True:
chunk = input_stream.read(4096)
if not chunk:
break
# Generate keystream for this chunk
counter_bytes = nonce + counter.to_bytes(4, 'big')
cipher = Cipher(
algorithms.AES(key),
modes.CTR(counter_bytes),
backend=default_backend()
)
encryptor = cipher.encryptor()
# Encrypt chunk
encrypted_chunk = encryptor.update(chunk) + encryptor.finalize()
# Update HMAC
h.update(encrypted_chunk)
# Write encrypted chunk
output_stream.write(encrypted_chunk)
counter += 1
# Write final HMAC
output_stream.write(h.digest())
# Clear HMAC key
secure_zero(hmac_key)
encrypt_cbc(key: bytes[32], plaintext: bytes, iv: bytes[16]) -> bytes
Security Contract:
- Preconditions:
keymust be 32 bytesivmust be 16 bytes and unpredictableplaintextwill be padded to block size
- Postconditions:
- Returns padded and encrypted ciphertext
- No authentication provided
- Vulnerable to padding oracle without MAC
Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Confidentiality | ✅ | When IV is random | | Authenticity | ❌ | No authentication | | Padding Oracle | ❌ | Without MAC | | IV Prediction | ❌ | If IV predictable | | Block Reordering | ❌ | Without MAC |
Security Requirements:
- Always use random IV
- Always add authentication (encrypt-then-MAC)
- Handle padding errors as authentication failures
- Consider AES-GCM instead
Secure Usage Example:
/// Secure CBC with encrypt-then-MAC
pub struct AesCbcMac {
cipher_key: [u8; 32],
mac_key: [u8; 32],
}
impl AesCbcMac {
pub fn new(master_key: &[u8; 32]) -> Self {
// Derive separate keys
let cipher_key = hkdf_expand(master_key, b"aes-key", 32);
let mac_key = hkdf_expand(master_key, b"mac-key", 32);
Self {
cipher_key: cipher_key.try_into().unwrap(),
mac_key: mac_key.try_into().unwrap(),
}
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptedMessage, Error> {
// Generate random IV
let mut iv = [0u8; 16];
OsRng.fill_bytes(&mut iv);
// Pad plaintext
let padded = pkcs7_pad(plaintext, 16);
// Encrypt
let cipher = Aes256CbcEnc::new(&self.cipher_key.into(), &iv.into());
let ciphertext = cipher.encrypt_padded_vec::<Pkcs7>(&padded)?;
// Compute MAC over IV || ciphertext
let mut mac_data = Vec::new();
mac_data.extend_from_slice(&iv);
mac_data.extend_from_slice(&ciphertext);
let tag = hmac_sha256(&self.mac_key, &mac_data);
Ok(EncryptedMessage {
iv,
ciphertext,
tag,
})
}
pub fn decrypt(&self, msg: &EncryptedMessage) -> Result<Vec<u8>, Error> {
// Verify MAC first
let mut mac_data = Vec::new();
mac_data.extend_from_slice(&msg.iv);
mac_data.extend_from_slice(&msg.ciphertext);
let expected_tag = hmac_sha256(&self.mac_key, &mac_data);
if !constant_time_eq(&expected_tag, &msg.tag) {
return Err(Error::AuthenticationFailed);
}
// Only decrypt after MAC verification
let cipher = Aes256CbcDec::new(&self.cipher_key.into(), &msg.iv.into());
let padded = cipher.decrypt_padded_vec::<Pkcs7>(&msg.ciphertext)?;
Ok(padded)
}
}
/// Timing-safe padding validation
fn validate_pkcs7_padding(data: &[u8]) -> Result<usize, Error> {
if data.is_empty() {
return Err(Error::InvalidPadding);
}
let pad_byte = data[data.len() - 1];
let pad_len = pad_byte as usize;
if pad_len == 0 || pad_len > 16 || pad_len > data.len() {
return Err(Error::InvalidPadding);
}
// Constant-time padding check
let mut valid = 1u8;
for i in 0..pad_len {
let byte = data[data.len() - 1 - i];
valid &= constant_time_eq_u8(byte, pad_byte);
}
if valid == 0 {
return Err(Error::InvalidPadding);
}
Ok(data.len() - pad_len)
}
Security Best Practices
Mode Selection Guide
def select_aes_mode(use_case: str) -> dict:
"""Select appropriate AES mode for use case"""
modes = {
'general_encryption': {
'mode': 'GCM',
'reason': 'Authenticated encryption, best general choice',
'implementation': 'AES-256-GCM'
},
'streaming_encryption': {
'mode': 'CTR',
'reason': 'No padding, seekable, add HMAC',
'implementation': 'AES-256-CTR-HMAC'
},
'disk_encryption': {
'mode': 'XTS',
'reason': 'Designed for storage, no IV expansion',
'implementation': 'AES-256-XTS'
},
'legacy_compatibility': {
'mode': 'CBC',
'reason': 'Wide support, needs encrypt-then-MAC',
'implementation': 'AES-256-CBC-HMAC'
},
'key_wrapping': {
'mode': 'KW/KWP',
'reason': 'RFC 3394/5649 key wrapping',
'implementation': 'AES-256-KW'
}
}
if use_case not in modes:
# Default to GCM
return modes['general_encryption']
return modes[use_case]
Secure Implementation Patterns
/// Secure AES implementation with all modes
pub struct SecureAES {
key: [u8; 32],
usage_counter: AtomicU64,
created_at: Instant,
}
impl SecureAES {
pub fn new() -> Result<Self, Error> {
let mut key = [0u8; 32];
OsRng.fill_bytes(&mut key);
Ok(Self {
key,
usage_counter: AtomicU64::new(0),
created_at: Instant::now(),
})
}
pub fn should_rotate(&self) -> bool {
// Check usage limits
let usage = self.usage_counter.load(Ordering::Relaxed);
if usage > 2u64.pow(32) {
return true;
}
// Check age
if self.created_at.elapsed() > Duration::from_secs(30 * 24 * 3600) {
return true;
}
false
}
pub fn encrypt_authenticated(
&self,
plaintext: &[u8],
associated_data: &[u8],
) -> Result<Vec<u8>, Error> {
// Increment usage counter
self.usage_counter.fetch_add(1, Ordering::Relaxed);
// Use GCM for authenticated encryption
let cipher = Aes256Gcm::new(Key::from_slice(&self.key));
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let payload = Payload {
msg: plaintext,
aad: associated_data,
};
let ciphertext = cipher.encrypt(&nonce, payload)?;
// Format: nonce || ciphertext || tag
let mut result = Vec::with_capacity(12 + ciphertext.len());
result.extend_from_slice(&nonce);
result.extend_from_slice(&ciphertext);
Ok(result)
}
}
impl Drop for SecureAES {
fn drop(&mut self) {
// Securely clear key on drop
self.key.zeroize();
}
}
Common Integration Patterns
Database Field Encryption
class FieldEncryption:
def __init__(self, master_key: bytes):
self.master_key = master_key
def encrypt_field(self, table: str, column: str, value: bytes, row_id: int) -> dict:
"""Encrypt database field with deterministic IV for searchability"""
# Derive table-specific key
table_key = hkdf_expand(
self.master_key,
f"table:{table}".encode(),
32
)
# For deterministic encryption (searchable)
if column in ['email', 'ssn', 'phone']:
# Derive IV from value for deterministic encryption
iv = hkdf_expand(
table_key,
f"iv:{column}:{value}".encode(),
16
)
# Use SIV mode for deterministic authenticated encryption
return self._encrypt_siv(table_key, value, iv)
# For randomized encryption
else:
return self._encrypt_gcm(table_key, value, row_id)
Envelope Encryption
/// Envelope encryption for large data
pub struct EnvelopeEncryption {
kek: [u8; 32], // Key encryption key
}
impl EnvelopeEncryption {
pub fn encrypt_large_file(&self, file_path: &Path) -> Result<(), Error> {
// Generate data encryption key
let mut dek = [0u8; 32];
OsRng.fill_bytes(&mut dek);
// Encrypt DEK with KEK
let wrapped_dek = aes_key_wrap(&self.kek, &dek)?;
// Encrypt file with DEK
let mut file = File::open(file_path)?;
let mut encrypted = Vec::new();
let cipher = Aes256Gcm::new(Key::from_slice(&dek));
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
// Process in chunks
let mut buffer = vec![0u8; 1024 * 1024]; // 1MB chunks
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
let chunk = cipher.encrypt(&nonce, &buffer[..n])?;
encrypted.extend_from_slice(&chunk);
}
// Clear DEK
dek.zeroize();
// Write encrypted file with header
let output = format!("{}.enc", file_path.display());
let mut out = File::create(output)?;
// Header: wrapped_dek || nonce || ciphertext
out.write_all(&wrapped_dek)?;
out.write_all(&nonce)?;
out.write_all(&encrypted)?;
Ok(())
}
}
Performance Considerations
| Mode | Speed (AES-NI) | Speed (Software) | Notes |
|---|---|---|---|
| ECB | 0.6 cycles/byte | 10 cycles/byte | Never use |
| CBC Encrypt | 3.0 cycles/byte | 15 cycles/byte | Sequential |
| CBC Decrypt | 0.7 cycles/byte | 15 cycles/byte | Parallel |
| CTR | 0.7 cycles/byte | 11 cycles/byte | Parallel |
| GCM | 1.0 cycles/byte | 20 cycles/byte | Auth included |
| XTS | 1.5 cycles/byte | 25 cycles/byte | Storage |
Hardware Acceleration
- Intel AES-NI: 10-20x speedup
- ARM Crypto Extensions: 5-10x speedup
- Check CPU features at runtime
- Fallback to software implementation
Security Auditing
Verification Checklist
- Never using ECB mode
- IV/nonce always unique per key
- Authentication added to encryption
- Padding oracles prevented
- Keys rotated regularly
- Usage limits enforced
- Keys cleared after use
Common Vulnerabilities
# AUDIT: Look for these patterns
# ❌ ECB mode
cipher = AES.new(key, AES.MODE_ECB) # NEVER!
# ❌ Static IV
iv = b'\x00' * 16 # Breaks security!
# ❌ No authentication
ciphertext = encrypt_cbc(key, plaintext, iv) # Malleable!
# ❌ Predictable IV
iv = struct.pack('<Q', counter) + b'\x00' * 8 # Predictable!
# ❌ Hardcoded keys
KEY = b'0123456789abcdef' * 2 # In source code!
Security Analysis
Threat Model: AES-256 Threat Model
The comprehensive threat analysis covers:
- Algorithm-specific attack vectors
- Implementation vulnerabilities
- Side-channel considerations
- Quantum resistance analysis (where applicable)
- Deployment recommendations
For complete security analysis and risk assessment, see the dedicated threat model documentation.
References
Support
Security Issues: security@metamui.id
Documentation Updates: phantom@metamui.id
Vulnerability Disclosure: See SECURITY.md
Document Version: 1.0
Review Cycle: Quarterly
Next Review: 2025-04-05
Classification: PUBLIC