Overview
Encryption converts plaintext data into ciphertext using mathematical algorithms and secret keys. The process makes information unreadable to unauthorized parties while allowing authorized recipients to decrypt and recover the original data. Modern software systems require encryption for protecting user credentials, securing network communications, safeguarding stored data, and meeting regulatory compliance requirements.
The encryption process involves three primary components: plaintext (original data), an encryption algorithm (cipher), and a key (secret value). The algorithm applies mathematical transformations to the plaintext using the key, producing ciphertext. Decryption reverses this process using either the same key (symmetric encryption) or a related key (asymmetric encryption).
require 'openssl'
# Basic encryption demonstration
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
plaintext = "Sensitive user data"
ciphertext = cipher.update(plaintext) + cipher.final
# => "\x8F\x2E\xA3..." (binary ciphertext)
Encryption appears throughout modern applications: HTTPS secures web traffic, database encryption protects stored records, encrypted messaging ensures private communications, and filesystem encryption guards local data. The discipline combines mathematics, computer science, and engineering to address practical security challenges while maintaining system performance and usability.
Key Principles
Encryption systems operate on fundamental principles that determine their security properties and practical applicability. Understanding these principles enables informed decisions about which encryption approaches suit specific requirements.
Confidentiality Through Computational Difficulty
Encryption security depends on computational infeasibility rather than information hiding. A secure cipher remains secure even when attackers know the algorithm, relying solely on key secrecy. This principle, Kerckhoffs's principle, means that algorithm details can be public and peer-reviewed while security depends entirely on keeping keys secret. Breaking encryption without the key requires computational resources beyond practical reach.
# Security depends on key secrecy, not algorithm secrecy
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
# Key must remain secret - algorithm is public
key = cipher.random_key # 32 bytes for AES-256
iv = cipher.random_iv # Initialization vector
# Public algorithm, secret key model
ciphertext = cipher.update("Secret message") + cipher.final
Symmetric vs Asymmetric Cryptography
Symmetric encryption uses identical keys for encryption and decryption. The sender and receiver must share the secret key through a secure channel before communication begins. AES, ChaCha20, and DES represent symmetric algorithms. Performance characteristics favor symmetric encryption for bulk data encryption, with AES-256 encrypting data at gigabytes per second on modern hardware.
Asymmetric encryption uses mathematically related key pairs: a public key for encryption and a private key for decryption. Anyone can encrypt data using the public key, but only the private key holder can decrypt it. RSA, ECC, and ElGamal exemplify asymmetric algorithms. Computational overhead makes asymmetric encryption significantly slower than symmetric approaches, typically used for key exchange rather than bulk encryption.
# Symmetric encryption - same key for both operations
symmetric_cipher = OpenSSL::Cipher.new('AES-256-CBC')
symmetric_cipher.encrypt
shared_key = symmetric_cipher.random_key
encrypted = symmetric_cipher.update("Data") + symmetric_cipher.final
# Asymmetric encryption - different keys
rsa_key = OpenSSL::PKey::RSA.new(2048)
public_key = rsa_key.public_key
private_key = rsa_key
# Encrypt with public key
encrypted_data = public_key.public_encrypt("Data")
# Decrypt with private key
decrypted_data = private_key.private_decrypt(encrypted_data)
Block Ciphers and Stream Ciphers
Block ciphers process fixed-size blocks of data (typically 128 bits for AES). Input shorter than the block size requires padding, while longer input gets divided into blocks. Block ciphers operate in modes that determine how multiple blocks interact. Stream ciphers encrypt data one bit or byte at a time, generating a keystream that combines with plaintext through XOR operations.
Initialization Vectors and Nonces
Encrypting identical plaintext with the same key produces identical ciphertext, leaking information about repeated data patterns. Initialization vectors (IVs) and nonces (numbers used once) randomize encryption output. Each encryption operation should use a unique, unpredictable IV. The IV need not remain secret but must never repeat with the same key.
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
key = cipher.random_key
# Different IVs produce different ciphertext for same plaintext
plaintext = "Repeated message"
cipher.iv = cipher.random_iv
ciphertext1 = cipher.update(plaintext) + cipher.final
cipher.reset
cipher.encrypt
cipher.key = key
cipher.iv = cipher.random_iv # Different IV
ciphertext2 = cipher.update(plaintext) + cipher.final
# ciphertext1 != ciphertext2 despite identical plaintext and key
Authentication and Encryption
Encryption alone provides confidentiality but not integrity or authenticity. An attacker can modify ciphertext without detection if the system lacks authentication. Authenticated encryption modes combine encryption with message authentication codes (MAC), ensuring both confidentiality and integrity. AES-GCM and ChaCha20-Poly1305 represent authenticated encryption with associated data (AEAD) algorithms.
Key Management Complexity
Key generation, distribution, storage, rotation, and destruction present practical challenges exceeding the mathematical complexity of encryption algorithms. Weak key management undermines strong encryption. Keys require cryptographically secure random generation, secure storage protected from unauthorized access, and regular rotation to limit exposure from potential compromise.
Ruby Implementation
Ruby provides encryption capabilities through the OpenSSL library, exposing symmetric and asymmetric cryptographic operations through a high-level API. The OpenSSL::Cipher class implements symmetric encryption, while OpenSSL::PKey classes handle asymmetric operations.
Symmetric Encryption with AES
AES (Advanced Encryption Standard) represents the most widely deployed symmetric cipher. Ruby's OpenSSL bindings support AES variants including AES-128, AES-192, and AES-256 (numbers indicate key size in bits) across multiple modes of operation.
require 'openssl'
require 'base64'
def encrypt_data(plaintext, key)
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = key
iv = cipher.random_iv
ciphertext = cipher.update(plaintext) + cipher.final
# Return IV and ciphertext - IV needed for decryption
{
iv: Base64.strict_encode64(iv),
ciphertext: Base64.strict_encode64(ciphertext)
}
end
def decrypt_data(encrypted_data, key)
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.decrypt
cipher.key = key
cipher.iv = Base64.strict_decode64(encrypted_data[:iv])
ciphertext = Base64.strict_decode64(encrypted_data[:ciphertext])
plaintext = cipher.update(ciphertext) + cipher.final
plaintext
end
# Generate secure key
key = OpenSSL::Cipher.new('AES-256-CBC').random_key
# Encrypt
encrypted = encrypt_data("Confidential information", key)
# => {:iv=>"...", :ciphertext=>"..."}
# Decrypt
decrypted = decrypt_data(encrypted, key)
# => "Confidential information"
Authenticated Encryption with GCM
GCM (Galois/Counter Mode) provides authenticated encryption, detecting tampering attempts through authentication tags. This mode eliminates the need for separate MAC operations.
def encrypt_authenticated(plaintext, key, associated_data = nil)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = key
iv = cipher.random_iv
cipher.auth_data = associated_data if associated_data
ciphertext = cipher.update(plaintext) + cipher.final
auth_tag = cipher.auth_tag
{
iv: Base64.strict_encode64(iv),
ciphertext: Base64.strict_encode64(ciphertext),
auth_tag: Base64.strict_encode64(auth_tag)
}
end
def decrypt_authenticated(encrypted_data, key, associated_data = nil)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = key
cipher.iv = Base64.strict_decode64(encrypted_data[:iv])
cipher.auth_tag = Base64.strict_decode64(encrypted_data[:auth_tag])
cipher.auth_data = associated_data if associated_data
ciphertext = Base64.strict_decode64(encrypted_data[:ciphertext])
plaintext = cipher.update(ciphertext) + cipher.final
plaintext
rescue OpenSSL::Cipher::CipherError
raise "Authentication failed - data may be corrupted or tampered"
end
key = OpenSSL::Cipher.new('AES-256-GCM').random_key
associated_data = "user_id:12345"
encrypted = encrypt_authenticated("Secret data", key, associated_data)
decrypted = decrypt_authenticated(encrypted, key, associated_data)
Asymmetric Encryption with RSA
RSA enables encryption without shared secrets. Public keys encrypt data that only corresponding private keys can decrypt. RSA typically encrypts small amounts of data, such as symmetric keys, rather than bulk data.
# Generate RSA key pair
rsa_key = OpenSSL::PKey::RSA.new(2048)
public_key = rsa_key.public_key
private_key = rsa_key
# Encrypt with public key
plaintext = "Symmetric key material"
ciphertext = public_key.public_encrypt(
plaintext,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Decrypt with private key
decrypted = private_key.private_decrypt(
ciphertext,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# => "Symmetric key material"
# Export keys for storage or transmission
public_pem = public_key.to_pem
private_pem = private_key.to_pem
# Load keys from PEM format
loaded_public = OpenSSL::PKey::RSA.new(public_pem)
loaded_private = OpenSSL::PKey::RSA.new(private_pem)
Hybrid Encryption Pattern
Production systems typically combine symmetric and asymmetric encryption: asymmetric encryption protects symmetric keys, while symmetric encryption handles bulk data. This approach balances security and performance.
class HybridEncryption
def self.encrypt(plaintext, recipient_public_key)
# Generate random symmetric key for this message
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
symmetric_key = cipher.random_key
# Encrypt data with symmetric key
encrypted_data = encrypt_symmetric(plaintext, symmetric_key)
# Encrypt symmetric key with recipient's public key
encrypted_key = recipient_public_key.public_encrypt(
symmetric_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
{
encrypted_key: Base64.strict_encode64(encrypted_key),
encrypted_data: encrypted_data
}
end
def self.decrypt(encrypted_message, recipient_private_key)
# Decrypt symmetric key
encrypted_key = Base64.strict_decode64(encrypted_message[:encrypted_key])
symmetric_key = recipient_private_key.private_decrypt(
encrypted_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Decrypt data with symmetric key
decrypt_symmetric(encrypted_message[:encrypted_data], symmetric_key)
end
private
def self.encrypt_symmetric(plaintext, key)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = key
iv = cipher.random_iv
ciphertext = cipher.update(plaintext) + cipher.final
{
iv: Base64.strict_encode64(iv),
ciphertext: Base64.strict_encode64(ciphertext),
auth_tag: Base64.strict_encode64(cipher.auth_tag)
}
end
def self.decrypt_symmetric(encrypted_data, key)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = key
cipher.iv = Base64.strict_decode64(encrypted_data[:iv])
cipher.auth_tag = Base64.strict_decode64(encrypted_data[:auth_tag])
ciphertext = Base64.strict_decode64(encrypted_data[:ciphertext])
cipher.update(ciphertext) + cipher.final
end
end
# Usage
recipient_key = OpenSSL::PKey::RSA.new(2048)
message = "Large confidential document content..."
encrypted = HybridEncryption.encrypt(message, recipient_key.public_key)
decrypted = HybridEncryption.decrypt(encrypted, recipient_key)
Key Derivation Functions
User passwords lack sufficient entropy for direct use as encryption keys. Key derivation functions (KDF) transform passwords into cryptographically suitable keys through computationally intensive operations that resist brute-force attacks.
require 'openssl'
def derive_key_from_password(password, salt = nil)
# Generate salt if not provided
salt ||= OpenSSL::Random.random_bytes(16)
# PBKDF2 with SHA256, 100000 iterations
key = OpenSSL::PKCS5.pbkdf2_hmac(
password,
salt,
100_000, # Iteration count - higher is more secure but slower
32, # Key length in bytes (256 bits)
OpenSSL::Digest::SHA256.new
)
{
key: key,
salt: salt
}
end
# Derive key from password
password = "user_password_123"
derived = derive_key_from_password(password)
# Store salt with encrypted data - needed for decryption
# Salt can be public, but must be unique per password
salt = derived[:salt]
key = derived[:key]
# Use derived key for encryption
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = key
# ... encryption continues
Security Implications
Encryption implementation requires careful attention to security considerations beyond selecting a cipher algorithm. Improper usage patterns, weak key management, and subtle implementation errors can completely undermine cryptographic protections.
Key Generation Security
Cryptographically secure random number generation forms the foundation of encryption security. Standard random number generators produce predictable outputs unsuitable for cryptographic keys. Ruby's OpenSSL::Random.random_bytes provides cryptographically secure randomness sourced from operating system entropy pools.
# Insecure - predictable pseudo-random values
insecure_key = Array.new(32) { rand(256) }.pack('C*')
# Secure - cryptographically random values
secure_key = OpenSSL::Random.random_bytes(32)
# Secure key generation for ciphers
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
secure_key = cipher.random_key # Uses OpenSSL's secure RNG
Insufficient key length weakens security regardless of algorithm strength. AES-256 requires 256-bit keys, RSA requires minimum 2048-bit keys for adequate security. Shorter keys become vulnerable to brute-force attacks as computational power increases.
IV and Nonce Management
IV reuse with the same key catastrophically compromises encryption security, particularly for counter-based modes like CTR and GCM. Repeated IVs leak information about plaintext relationships and can enable complete plaintext recovery in some modes.
# Dangerous - IV reuse
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
key = cipher.random_key
# Same IV used multiple times - INSECURE
static_iv = cipher.random_iv
cipher.iv = static_iv
ciphertext1 = cipher.update("First message") + cipher.final
cipher.reset
cipher.encrypt
cipher.key = key
cipher.iv = static_iv # Reused IV - security compromised
ciphertext2 = cipher.update("Second message") + cipher.final
# Secure - unique IV per encryption
def secure_encrypt(plaintext, key)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = key
cipher.iv = cipher.random_iv # New IV each time
ciphertext = cipher.update(plaintext) + cipher.final
{
iv: cipher.iv,
ciphertext: ciphertext,
auth_tag: cipher.auth_tag
}
end
Padding Oracle Attacks
CBC mode with PKCS7 padding becomes vulnerable to padding oracle attacks when systems reveal whether decryption succeeded or padding validation failed. Attackers exploit timing differences or error messages to decrypt ciphertext without keys. Authenticated encryption modes like GCM eliminate this vulnerability class by verifying integrity before decryption.
Timing Attacks
Comparison operations that terminate early upon finding mismatches leak information through timing variations. Authentication tag verification must use constant-time comparison to prevent timing-based attacks.
# Vulnerable to timing attacks - early termination
def insecure_compare(a, b)
return false if a.length != b.length
a.bytes.zip(b.bytes).all? { |x, y| x == y }
end
# Constant-time comparison
def secure_compare(a, b)
return false if a.length != b.length
result = 0
a.bytes.zip(b.bytes).each { |x, y| result |= x ^ y }
result == 0
end
# Ruby's built-in secure compare
require 'openssl'
def verify_auth_tag(expected_tag, received_tag)
OpenSSL.secure_compare(expected_tag, received_tag)
end
Key Storage
Keys stored in source code, configuration files, or environment variables risk exposure through repository leaks, log files, or unauthorized access. Production systems require dedicated key management solutions: hardware security modules (HSM), key management services (KMS), or secure key vaults. Keys should exist in memory only during active use and be cleared after operations complete.
# Insecure - hardcoded key
ENCRYPTION_KEY = "secret_key_12345"
# Better - environment variable (still not ideal for production)
encryption_key = ENV['ENCRYPTION_KEY']
# Production approach - key management service
require 'aws-sdk-kms'
class SecureKeyManager
def initialize
@kms = Aws::KMS::Client.new(region: 'us-east-1')
end
def encrypt_data(plaintext)
# Generate data key from KMS
response = @kms.generate_data_key(
key_id: 'alias/application-key',
key_spec: 'AES_256'
)
plaintext_key = response.plaintext
encrypted_key = response.ciphertext_blob
# Encrypt data with data key
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = plaintext_key
iv = cipher.random_iv
ciphertext = cipher.update(plaintext) + cipher.final
# Clear key from memory
plaintext_key.replace("\0" * plaintext_key.length)
{
encrypted_key: Base64.strict_encode64(encrypted_key),
iv: Base64.strict_encode64(iv),
ciphertext: Base64.strict_encode64(ciphertext),
auth_tag: Base64.strict_encode64(cipher.auth_tag)
}
end
end
Encryption Is Not Encoding
Base64 encoding provides no security despite obscuring data. Encoding transforms data representation without secret keys and reverses trivially. Encryption requires secret keys and computational difficulty. Systems must never rely on encoding for confidentiality.
# Encoding - reversible without secrets
encoded = Base64.strict_encode64("Secret data")
decoded = Base64.strict_decode64(encoded) # No key needed
# Encryption - requires secret key
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
key = cipher.random_key # Secret required
cipher.key = key
encrypted = cipher.update("Secret data") + cipher.final
# Cannot decrypt without key
Practical Examples
Real-world encryption scenarios demonstrate practical application patterns, error handling requirements, and integration considerations. These examples show complete implementations suitable for production adaptation.
Encrypted File Storage
Applications frequently need to encrypt files before storing them on disk or cloud storage. This implementation encrypts files with metadata preservation and authenticated encryption.
require 'openssl'
require 'base64'
require 'json'
class EncryptedFileStorage
ALGORITHM = 'AES-256-GCM'
def initialize(master_key)
@master_key = master_key
end
def encrypt_file(source_path, destination_path)
plaintext = File.binread(source_path)
cipher = OpenSSL::Cipher.new(ALGORITHM)
cipher.encrypt
cipher.key = @master_key
iv = cipher.random_iv
ciphertext = cipher.update(plaintext) + cipher.final
auth_tag = cipher.auth_tag
# Store metadata with encrypted content
encrypted_data = {
version: 1,
algorithm: ALGORITHM,
iv: Base64.strict_encode64(iv),
auth_tag: Base64.strict_encode64(auth_tag),
ciphertext: Base64.strict_encode64(ciphertext),
metadata: {
original_size: plaintext.bytesize,
encrypted_at: Time.now.utc.iso8601
}
}
File.write(destination_path, JSON.generate(encrypted_data))
true
end
def decrypt_file(encrypted_path, destination_path)
encrypted_data = JSON.parse(File.read(encrypted_path), symbolize_names: true)
raise "Unsupported version" unless encrypted_data[:version] == 1
cipher = OpenSSL::Cipher.new(encrypted_data[:algorithm])
cipher.decrypt
cipher.key = @master_key
cipher.iv = Base64.strict_decode64(encrypted_data[:iv])
cipher.auth_tag = Base64.strict_decode64(encrypted_data[:auth_tag])
ciphertext = Base64.strict_decode64(encrypted_data[:ciphertext])
plaintext = cipher.update(ciphertext) + cipher.final
File.binwrite(destination_path, plaintext)
encrypted_data[:metadata]
rescue OpenSSL::Cipher::CipherError => e
raise "Decryption failed: #{e.message}"
end
end
# Usage
master_key = OpenSSL::Cipher.new('AES-256-GCM').random_key
storage = EncryptedFileStorage.new(master_key)
# Encrypt file
storage.encrypt_file('sensitive_document.pdf', 'document.encrypted')
# Decrypt file
metadata = storage.decrypt_file('document.encrypted', 'restored_document.pdf')
# => {:original_size=>1024, :encrypted_at=>"2025-10-07T10:30:00Z"}
Database Field Encryption
Encrypting specific database fields protects sensitive information while maintaining database functionality for non-sensitive fields. This pattern encrypts data before storage and decrypts after retrieval.
require 'openssl'
require 'base64'
module EncryptedAttribute
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def encrypt_attribute(attribute_name, key:)
define_method("#{attribute_name}=") do |value|
encrypted = encrypt_value(value, key)
instance_variable_set("@#{attribute_name}", value)
write_attribute("encrypted_#{attribute_name}", encrypted)
end
define_method(attribute_name) do
encrypted_value = read_attribute("encrypted_#{attribute_name}")
return nil unless encrypted_value
cached = instance_variable_get("@#{attribute_name}")
return cached if cached
decrypted = decrypt_value(encrypted_value, key)
instance_variable_set("@#{attribute_name}", decrypted)
decrypted
end
end
end
private
def encrypt_value(plaintext, key)
return nil if plaintext.nil?
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = key
iv = cipher.random_iv
ciphertext = cipher.update(plaintext.to_s) + cipher.final
# Pack IV, auth tag, and ciphertext together
packed = [
iv,
cipher.auth_tag,
ciphertext
].map { |data| Base64.strict_encode64(data) }.join(':')
packed
end
def decrypt_value(encrypted_value, key)
iv_b64, tag_b64, ciphertext_b64 = encrypted_value.split(':', 3)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = key
cipher.iv = Base64.strict_decode64(iv_b64)
cipher.auth_tag = Base64.strict_decode64(tag_b64)
ciphertext = Base64.strict_decode64(ciphertext_b64)
cipher.update(ciphertext) + cipher.final
end
end
# Example ActiveRecord model
class User
include EncryptedAttribute
ENCRYPTION_KEY = ENV['USER_ENCRYPTION_KEY']
encrypt_attribute :ssn, key: ENCRYPTION_KEY
encrypt_attribute :credit_card, key: ENCRYPTION_KEY
end
# Usage
user = User.new
user.ssn = "123-45-6789"
user.save
# Database stores encrypted value in encrypted_ssn column
# Accessing user.ssn automatically decrypts
retrieved_user = User.find(user.id)
puts retrieved_user.ssn # => "123-45-6789"
Message Encryption with Key Exchange
Secure messaging between parties requires exchanging encryption keys without prior shared secrets. This implementation uses public key cryptography for key establishment.
class SecureMessaging
def initialize
@private_key = OpenSSL::PKey::RSA.new(2048)
@public_key = @private_key.public_key
end
attr_reader :public_key
def send_message(plaintext, recipient_public_key)
# Generate ephemeral symmetric key
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
session_key = cipher.random_key
# Encrypt message with symmetric key
iv = cipher.random_iv
cipher.key = session_key
ciphertext = cipher.update(plaintext) + cipher.final
auth_tag = cipher.auth_tag
# Encrypt session key with recipient's public key
encrypted_session_key = recipient_public_key.public_encrypt(
session_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
{
encrypted_key: Base64.strict_encode64(encrypted_session_key),
iv: Base64.strict_encode64(iv),
ciphertext: Base64.strict_encode64(ciphertext),
auth_tag: Base64.strict_encode64(auth_tag)
}
end
def receive_message(encrypted_message)
# Decrypt session key with private key
encrypted_key = Base64.strict_decode64(encrypted_message[:encrypted_key])
session_key = @private_key.private_decrypt(
encrypted_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Decrypt message with session key
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = session_key
cipher.iv = Base64.strict_decode64(encrypted_message[:iv])
cipher.auth_tag = Base64.strict_decode64(encrypted_message[:auth_tag])
ciphertext = Base64.strict_decode64(encrypted_message[:ciphertext])
plaintext = cipher.update(ciphertext) + cipher.final
# Clear session key from memory
session_key.replace("\0" * session_key.length)
plaintext
end
end
# Usage
alice = SecureMessaging.new
bob = SecureMessaging.new
# Alice sends encrypted message to Bob
message = "Confidential communication"
encrypted = alice.send_message(message, bob.public_key)
# Bob decrypts message
decrypted = bob.receive_message(encrypted)
# => "Confidential communication"
Common Pitfalls
Encryption implementations frequently contain subtle errors that compromise security despite using strong algorithms. Understanding common mistakes helps developers avoid these vulnerabilities.
Using ECB Mode
ECB (Electronic Codebook) mode encrypts each block independently, leaking information about repeated plaintext patterns. Identical plaintext blocks produce identical ciphertext blocks, revealing data structure. ECB mode should never be used except for single-block encryption.
# Dangerous - ECB mode reveals patterns
cipher = OpenSSL::Cipher.new('AES-256-ECB')
cipher.encrypt
key = cipher.random_key
# Repeated data produces visible patterns in ciphertext
plaintext = "AAAAAAAAAAAAAAAA" * 10 # Repeated pattern
ciphertext = cipher.update(plaintext) + cipher.final
# Ciphertext contains repeating blocks
# Safe - CBC mode with random IV
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = key
cipher.iv = cipher.random_iv # Random IV prevents patterns
ciphertext = cipher.update(plaintext) + cipher.final
# Ciphertext appears random despite repeated plaintext
Ignoring Authentication
Encryption without authentication allows attackers to modify ciphertext undetected. Bit-flipping attacks can alter decrypted plaintext predictably without knowing keys. Always use authenticated encryption (AEAD) modes or combine encryption with HMAC.
# Vulnerable - no authentication
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
ciphertext = cipher.update("amount:1000") + cipher.final
# Attacker can modify ciphertext bits
# Decryption succeeds but produces altered plaintext
# Protected - authenticated encryption
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = key
iv = cipher.random_iv
ciphertext = cipher.update("amount:1000") + cipher.final
auth_tag = cipher.auth_tag
# Any modification detected during decryption
cipher.decrypt
cipher.key = key
cipher.iv = iv
cipher.auth_tag = auth_tag
begin
modified_ciphertext = ciphertext.dup
modified_ciphertext[0] = modified_ciphertext[0].ord ^ 1 # Flip one bit
plaintext = cipher.update(modified_ciphertext) + cipher.final
rescue OpenSSL::Cipher::CipherError
# Authentication fails - modification detected
end
Weak Password-Based Keys
Directly using passwords as encryption keys creates vulnerability to dictionary attacks. Passwords lack sufficient entropy and predictability. Key derivation functions with high iteration counts increase computational cost for attackers.
# Weak - password used directly as key
password = "user_password"
weak_key = OpenSSL::Digest::SHA256.digest(password)[0...32]
# Strong - PBKDF2 with high iteration count
salt = OpenSSL::Random.random_bytes(16)
strong_key = OpenSSL::PKCS5.pbkdf2_hmac(
password,
salt,
100_000, # High iteration count
32,
OpenSSL::Digest::SHA256.new
)
# Store salt with encrypted data for decryption
Reusing Keys Across Contexts
Single keys used for multiple purposes create cross-context vulnerabilities. Encryption keys should not sign data. Keys for different data types should remain separate. Key derivation can generate context-specific keys from master keys.
# Risky - same key for multiple purposes
master_key = OpenSSL::Cipher.new('AES-256-GCM').random_key
# Better - derive context-specific keys
def derive_context_key(master_key, context)
OpenSSL::PKCS5.pbkdf2_hmac(
master_key,
context,
1, # Single iteration - master key already strong
32,
OpenSSL::Digest::SHA256.new
)
end
encryption_key = derive_context_key(master_key, "encryption")
signing_key = derive_context_key(master_key, "signing")
backup_key = derive_context_key(master_key, "backup")
Insufficient Random Data
Using non-cryptographic random sources for keys or IVs creates predictability. Standard Random or rand functions produce deterministic sequences unsuitable for cryptography. Always use OpenSSL::Random.random_bytes or cipher random methods.
# Insecure - predictable randomness
bad_key = Array.new(32) { rand(256) }.pack('C*')
bad_iv = Array.new(16) { Random.rand(256) }.pack('C*')
# Secure - cryptographic randomness
good_key = OpenSSL::Random.random_bytes(32)
good_iv = OpenSSL::Random.random_bytes(16)
# Using cipher methods (also secure)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
secure_key = cipher.random_key
secure_iv = cipher.random_iv
Storing Plaintext Keys
Keys stored in source code, logs, error messages, or insecure locations compromise all encrypted data. Keys require the same protection as the data they encrypt. Production systems need key management infrastructure.
Trusting Client-Side Encryption
Encryption performed in browser JavaScript provides minimal security since adversaries control the execution environment. Client-side encryption can prevent passive network observation but cannot protect against compromised clients or malicious JavaScript. Server-side encryption with proper key management provides meaningful security.
Reference
Symmetric Algorithms
| Algorithm | Key Size | Block Size | Mode Support | Use Case |
|---|---|---|---|---|
| AES-128 | 128 bits | 128 bits | CBC, GCM, CTR, etc | Fast, widely supported |
| AES-192 | 192 bits | 128 bits | CBC, GCM, CTR, etc | Moderate security margin |
| AES-256 | 256 bits | 128 bits | CBC, GCM, CTR, etc | Maximum security, compliance |
| ChaCha20 | 256 bits | Stream | Poly1305 (AEAD) | Software performance, mobile |
| 3DES | 168 bits | 64 bits | CBC | Legacy systems only |
Asymmetric Algorithms
| Algorithm | Key Size | Operation | Use Case |
|---|---|---|---|
| RSA-2048 | 2048 bits | Encrypt/Decrypt | Key exchange, digital signatures |
| RSA-3072 | 3072 bits | Encrypt/Decrypt | Long-term security |
| RSA-4096 | 4096 bits | Encrypt/Decrypt | Maximum security, slower |
| ECC P-256 | 256 bits | Key agreement | Efficient key exchange |
| ECC P-384 | 384 bits | Key agreement | Government/compliance |
Common Cipher Modes
| Mode | IV Required | Authentication | Parallelizable | Use Case |
|---|---|---|---|---|
| CBC | Yes | No | Decrypt only | General purpose, legacy |
| CTR | Yes | No | Yes | Performance, random access |
| GCM | Yes | Yes | Yes | Modern authenticated encryption |
| CCM | Yes | Yes | No | Constrained environments |
| XTS | Yes | No | Yes | Disk encryption |
Ruby OpenSSL Cipher Operations
| Method | Description | Returns |
|---|---|---|
| OpenSSL::Cipher.new(name) | Creates cipher instance | Cipher object |
| cipher.encrypt | Sets cipher to encryption mode | nil |
| cipher.decrypt | Sets cipher to decryption mode | nil |
| cipher.random_key | Generates random key | Binary string |
| cipher.random_iv | Generates random IV | Binary string |
| cipher.key= | Sets encryption key | nil |
| cipher.iv= | Sets initialization vector | nil |
| cipher.auth_tag | Gets authentication tag (GCM/CCM) | Binary string |
| cipher.auth_tag= | Sets authentication tag for verification | nil |
| cipher.auth_data= | Sets additional authenticated data | nil |
| cipher.update(data) | Encrypts or decrypts data chunk | Binary string |
| cipher.final | Completes operation, returns final block | Binary string |
| cipher.reset | Resets cipher to initial state | nil |
Key Derivation Parameters
| Parameter | Purpose | Recommended Value |
|---|---|---|
| Salt | Prevents rainbow table attacks | 16+ bytes, unique per password |
| Iterations | Increases computational cost | 100,000+ for PBKDF2-SHA256 |
| Key length | Output key size | 256 bits for AES-256 |
| Hash function | Underlying hash for KDF | SHA-256 or SHA-512 |
Security Requirements
| Component | Requirement | Implementation |
|---|---|---|
| Keys | Cryptographically random | OpenSSL::Random.random_bytes |
| IVs | Unique per encryption | cipher.random_iv for each operation |
| Salt | Unique per password | OpenSSL::Random.random_bytes(16) |
| Auth tags | Verified before using plaintext | Check before decryption with GCM |
| Key storage | Protected, not in source code | KMS, vault, secure key storage |
| Key rotation | Regular key updates | Depends on data sensitivity |
OpenSSL Cipher Names
| Cipher String | Description |
|---|---|
| AES-128-CBC | AES 128-bit key, CBC mode |
| AES-256-CBC | AES 256-bit key, CBC mode |
| AES-128-GCM | AES 128-bit key, GCM authenticated mode |
| AES-256-GCM | AES 256-bit key, GCM authenticated mode |
| ChaCha20-Poly1305 | ChaCha20 stream cipher with Poly1305 MAC |
| AES-128-CTR | AES 128-bit key, counter mode |
| AES-256-CTR | AES 256-bit key, counter mode |
Error Handling
| Error | Cause | Resolution |
|---|---|---|
| OpenSSL::Cipher::CipherError | Authentication failure, wrong key, corrupted data | Verify key, check data integrity, validate auth tag |
| ArgumentError | Invalid key or IV length | Use correct length for algorithm |
| TypeError | Wrong data type for key/IV | Ensure binary string format |
| RuntimeError | Operation on uninitialized cipher | Call encrypt or decrypt before operations |