Symmetric Cryptography
Symmetric cryptography uses the same key for both encryption and decryption. It’s fast and efficient for encrypting large amounts of data.
Common Symmetric Algorithms
Advanced Encryption Standard (AES)
- 128-bit block cipher with key sizes of 128, 192, or 256 bits
- Modes: ECB, CBC, CFB, OFB, CTR, GCM
- Considered secure and widely adopted as the standard
- Used in TLS, disk encryption, and file encryption
ChaCha20
- Stream cipher designed for software implementations
- 256-bit key and 64-bit nonce
- Faster than AES on platforms without AES-NI
- Used in TLS 1.3 and modern protocols
- Often paired with Poly1305 for authentication
Legacy Algorithms
- 3DES (Triple DES): Applies DES three times with different keys
- AES: 56-bit key (insecure, deprecated)
- Blowfish: 64-bit block cipher with variable key length
- RC4: Stream cipher (insecure, deprecated)
Security Considerations
- Key Management: Securely generate, store, and exchange keys
- IVs: Never reuse Initialization Vectors (IVs)
- Authentication: Use AEAD modes (like AES-GCM) when possible
- Padding: Be aware of padding oracle attacks in CBC mode
- Key Size: Use at least 128-bit keys (256-bit recommended)
Implementation Examples
Python: AES-256-GCM
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
def encrypt_aes_gcm(plaintext, key=None):
if key is None:
key = os.urandom(32) # 256-bit key
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce for GCM
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
return key, nonce, ciphertext
def decrypt_aes_gcm(key, nonce, ciphertext):
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, None).decode()
Go: ChaCha20-Poly1305
package main
import (
"crypto/cipher"
"crypto/rand"
"golang.org/x/crypto/chacha20poly1305"
)
func encryptChaCha20(key, plaintext []byte) ([]byte, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
return aead.Seal(nonce, nonce, plaintext, nil), nil
}
func decryptChaCha20(key, ciphertext []byte) ([]byte, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
nonceSize := aead.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
return aead.Open(nil, nonce, ciphertext, nil)
}
C: AES-256-CBC with OpenSSL
#include <openssl/evp.h>
#include <openssl/rand.h>
int encrypt_aes_256_cbc(unsigned char *plaintext, int plaintext_len, unsigned char *key,
unsigned char *iv, unsigned char *ciphertext) {
EVP_CIPHER_CTX *ctx;
int len;
int ciphertext_len;
if(!(ctx = EVP_CIPHER_CTX_new()))
return -1;
if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
return -1;
if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len))
return -1;
ciphertext_len = len;
if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len))
return -1;
ciphertext_len += len;
EVP_CIPHER_CTX_free(ctx);
return ciphertext_len;
}
Rust: XChaCha20-Poly1305
use chacha20poly1305::{
aead::{Aead, KeyInit},
XChaCha20Poly1305, XNonce
};
use rand_core::OsRng;
fn encrypt_xchacha20(key: &[u8; 32], plaintext: &[u8]) -> (XNonce, Vec<u8>) {
let cipher = XChaCha20Poly1305::new_from_slice(key).unwrap();
let nonce = XNonce::from_slice(b"24-byte nonce for XChaCha20");
let ciphertext = cipher.encrypt(nonce, plaintext).unwrap();
(*nonce, ciphertext)
}
fn decrypt_xchacha20(key: &[u8; 32], nonce: &XNonce, ciphertext: &[u8]) -> Result<Vec<u8>, String> {
let cipher = XChaCha20Poly1305::new_from_slice(key).map_err(|e| e.to_string())?;
cipher.decrypt(nonce, ciphertext).map_err(|e| e.to_string())
}
Performance Considerations
- Hardware Acceleration: AES-NI on x86 significantly improves AES performance
- Key Size Impact: 256-bit keys are only marginally slower than 128-bit
- Mode Selection: GCM provides both encryption and authentication
- Memory Usage: Some modes require more memory than others
- Parallelization: CTR and GCM modes support parallel encryption/decryption
Authenticated Encryption with Associated Data (AEAD)
AEAD combines encryption and authentication into a single cryptographic scheme, ensuring both confidentiality and integrity of the encrypted data. It’s the recommended approach for modern cryptographic systems.
Why Use AEAD?
- Simplicity: Single API for both encryption and authentication
- Security: Eliminates the risk of combining encryption and MAC incorrectly
- Performance: More efficient than separate encryption and MAC operations
- Standardization: Well-vetted algorithms with strong security proofs
Common AEAD Algorithms
AES-GCM (Galois/Counter Mode)
- Most widely used AEAD
- 128-bit block cipher
- 12-byte nonce (96 bits) recommended
- 16-byte authentication tag (128 bits)
ChaCha20-Poly1305
- Faster in software than AES on non-AES-NI hardware
- 256-bit key
- 12-byte nonce
- 16-byte authentication tag
AES-CCM
- Combines CTR mode with CBC-MAC
- Used in wireless protocols like 802.11i
- Slower than GCM due to two passes over the data
Implementation Examples
Python: AES-GCM
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
def encrypt_aead(key, plaintext, associated_data):
# Generate a random 96-bit nonce
nonce = os.urandom(12)
# Create AES-GCM cipher
aesgcm = AESGCM(key)
# Encrypt and authenticate
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
# Nonce is not secret, but must be unique for each encryption with the same key
return nonce, ciphertext
def decrypt_aead(key, nonce, ciphertext, associated_data):
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, associated_data)
Go: ChaCha20-Poly1305
package main
import (
"crypto/cipher"
"crypto/rand"
"golang.org/x/crypto/chacha20poly1305"
"io"
)
func encryptAEAD(key, plaintext, additionalData []byte) ([]byte, error) {
// Create new ChaCha20-Poly1305 cipher
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
// Nonce must be unique for each encryption with this key
nonce := make([]byte, aead.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Encrypt and authenticate
ciphertext := aead.Seal(nil, nonce, plaintext, additionalData)
// Prepend nonce to ciphertext for storage/transmission
return append(nonce, ciphertext...), nil
}
func decryptAEAD(key, data, additionalData []byte) ([]byte, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
// Extract nonce from the beginning of the data
nonceSize := aead.NonceSize()
if len(data) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce := data[:nonceSize]
ciphertext := data[nonceSize:]
// Decrypt and verify
return aead.Open(nil, nonce, ciphertext, additionalData)
}
Security Considerations for AEAD
Nonce Management
- Never reuse a nonce with the same key
- Use a counter or cryptographically secure random nonce
- For AES-GCM, 96-bit nonces are recommended
Key Management
- Use proper key derivation for password-based encryption
- Rotate keys periodically
- Store keys securely (HSM, key management service)
Associated Data
- Authenticate all data that affects message handling
- Include protocol version numbers and message types
- Include message sequence numbers to prevent replay attacks
Performance Benchmarks
| Algorithm | Throughput (MB/s) | Latency (μs) | Key Size | Nonce Size | Tag Size |
|---|---|---|---|---|---|
| AES-128-GCM | 1,200 | 0.8 | 16 B | 12 B | 16 B |
| AES-256-GCM | 1,000 | 1.0 | 32 B | 12 B | 16 B |
| ChaCha20-Poly1305 | 900 | 1.1 | 32 B | 12 B | 16 B |
| AES-CCM | 600 | 1.8 | 16/24/32 B | 13 B | 4-16 B |
Common Pitfalls
Nonce Reuse
- Catastrophic security failure in most AEAD schemes
- Solution: Use a counter or cryptographically secure random nonce
Unverified Plaintext
- Always verify the authentication tag before decrypting
- Solution: Use the API correctly - don’t skip verification
Key Derivation
- Weak keys from passwords can compromise security
- Solution: Use Argon2, scrypt, or PBKDF2 with sufficient iterations
Timing Attacks
- Some implementations may be vulnerable to timing attacks
- Solution: Use constant-time comparison for authentication tags
Best Practices
- Always use AEAD for new development when possible
- Generate cryptographically secure random keys and nonces
- Never reuse the same (key, nonce) pair
- Use key derivation functions (like Argon2) for password-based encryption
- Keep your cryptographic libraries up to date
- Authenticate all data that affects message handling
- Use appropriate key sizes (256-bit for long-term security)
- Implement proper error handling - don’t leak timing information
- Consider hardware acceleration for better performance
- Regularly audit your implementation for security vulnerabilities