Overview
Encryption transforms readable data into an unreadable format using mathematical algorithms and keys. Two fundamental approaches exist: symmetric encryption uses the same key for both encryption and decryption, while asymmetric encryption uses a pair of mathematically related keys where one encrypts and the other decrypts.
Symmetric encryption emerged first in computing history, with DES standardized in 1977 and later replaced by AES in 2001. The approach mirrors physical locks where the same key opens and closes a door. Every party that needs to decrypt data must possess the identical key, creating a key distribution challenge.
Asymmetric encryption, developed in the 1970s with the RSA algorithm, introduced a revolutionary concept: splitting the key into public and private components. The public key encrypts data that only the corresponding private key can decrypt. This eliminated the key distribution problem but introduced computational overhead.
The distinction matters because each approach solves different problems. Symmetric encryption excels at bulk data encryption where speed matters and key distribution can be managed. Asymmetric encryption handles key exchange, digital signatures, and scenarios where parties cannot share secrets beforehand.
Modern systems rarely use either approach exclusively. HTTPS, for example, uses asymmetric encryption to establish a secure channel and exchange symmetric keys, then uses symmetric encryption for actual data transfer. This hybrid approach combines the security benefits of asymmetric encryption with the performance of symmetric encryption.
require 'openssl'
# Symmetric encryption: one key for both operations
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
# Asymmetric encryption: key pair with different purposes
keypair = OpenSSL::PKey::RSA.new(2048)
public_key = keypair.public_key
private_key = keypair
Key Principles
Symmetric encryption relies on a shared secret key known to both sender and receiver. The same key performs both encryption and decryption operations, making the process reversible only for parties possessing that key. The security depends entirely on keeping the key confidential. If an attacker obtains the key, all past and future communications encrypted with that key become compromised.
Block ciphers divide data into fixed-size blocks and encrypt each block independently or with dependencies on previous blocks. AES operates on 128-bit blocks regardless of key size. Stream ciphers generate a keystream and combine it with plaintext using XOR operations, encrypting data bit by bit or byte by byte. ChaCha20 represents a modern stream cipher used in TLS 1.3.
Symmetric algorithms require initialization vectors (IVs) for many modes of operation. The IV prevents identical plaintexts from producing identical ciphertexts, which would leak information. Each encryption operation should use a unique, unpredictable IV. The IV does not need secrecy but must never be reused with the same key. Cipher modes like CBC, CTR, and GCM determine how blocks interact and whether operations can be parallelized.
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
cipher.key = key
cipher.iv = iv
plaintext = "Sensitive data"
ciphertext = cipher.update(plaintext) + cipher.final
auth_tag = cipher.auth_tag
# Decryption requires same key and IV
decipher = OpenSSL::Cipher.new('AES-256-GCM')
decipher.decrypt
decipher.key = key
decipher.iv = iv
decipher.auth_tag = auth_tag
recovered = decipher.update(ciphertext) + decipher.final
Asymmetric encryption uses mathematical relationships between key pairs. The RSA algorithm relies on the difficulty of factoring large prime numbers. Generating an RSA keypair involves selecting two large prime numbers, computing their product (the modulus), and deriving encryption and decryption exponents through modular arithmetic. The public key contains the modulus and encryption exponent; the private key contains the modulus and decryption exponent.
Elliptic curve cryptography (ECC) provides asymmetric encryption with smaller key sizes. A 256-bit ECC key offers security comparable to a 3072-bit RSA key. ECC operations involve point multiplication on elliptic curves over finite fields. The discrete logarithm problem on elliptic curves provides the security foundation.
Asymmetric encryption imposes size limitations on plaintext. RSA can only encrypt data smaller than the key size minus padding overhead. A 2048-bit RSA key encrypts at most 245 bytes with OAEP padding. This constraint leads to hybrid encryption patterns where asymmetric encryption protects a symmetric key, and symmetric encryption handles the actual data.
# RSA key generation with security parameters
rsa_key = OpenSSL::PKey::RSA.new(2048)
public_key = rsa_key.public_key
private_key = rsa_key
# Encryption limited by key size
small_data = "Secret key"
encrypted = public_key.public_encrypt(
small_data,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
decrypted = private_key.private_decrypt(
encrypted,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# ECC alternative with smaller keys
ec_key = OpenSSL::PKey::EC.generate('prime256v1')
Digital signatures use asymmetric cryptography in reverse: the private key creates a signature that the public key verifies. The signer hashes the message and encrypts the hash with their private key. Recipients decrypt the signature with the public key and compare the result to their own hash of the message. Matching hashes prove the message originated from the private key holder and was not modified.
Key derivation functions (KDFs) generate encryption keys from passwords or other secrets. Password-based encryption uses KDFs like PBKDF2, bcrypt, or Argon2 to derive symmetric keys from user passwords. These functions intentionally consume computational resources to slow brute-force attacks. The derived key encrypts data, allowing password-protected encryption without exposing the password directly.
Perfect forward secrecy (PFS) ensures that compromising a long-term key does not compromise past session keys. Systems achieve PFS by generating ephemeral key pairs for each session. After the session ends and parties discard the ephemeral keys, an attacker who later obtains the long-term private key cannot decrypt recorded traffic.
Ruby Implementation
Ruby's OpenSSL library provides access to symmetric and asymmetric encryption through the OpenSSL wrapper. The library exposes OpenSSL's battle-tested implementations of standard algorithms rather than implementing cryptography directly in Ruby. This approach ensures compatibility with other systems and leverages extensively audited C code.
Symmetric encryption in Ruby centers on the Cipher class. Algorithm selection happens through string identifiers that specify the cipher, key size, and mode. The format follows the pattern "CIPHER-KEYSIZE-MODE" such as "AES-256-CBC" or "AES-128-GCM". The cipher object maintains state and must be configured before use.
require 'openssl'
# Create and configure a symmetric cipher
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
# Generate cryptographically secure random keys
key = cipher.random_key
iv = cipher.random_iv
# Configure the cipher
cipher.key = key
cipher.iv = iv
# Encrypt data
plaintext = "Confidential message"
ciphertext = cipher.update(plaintext) + cipher.final
# Store both ciphertext and IV; keep key secure
encrypted_package = {
ciphertext: Base64.strict_encode64(ciphertext),
iv: Base64.strict_encode64(iv)
}
Decryption mirrors encryption but requires initializing the cipher in decrypt mode. The IV used during encryption must be provided during decryption. The key must match exactly. Authentication tags from authenticated encryption modes like GCM must be set before finalization.
# Decrypt the data
decipher = OpenSSL::Cipher.new('AES-256-CBC')
decipher.decrypt
decipher.key = key
decipher.iv = Base64.strict_decode64(encrypted_package[:iv])
ciphertext = Base64.strict_decode64(encrypted_package[:ciphertext])
recovered = decipher.update(ciphertext) + decipher.final
# recovered equals original plaintext
Authenticated encryption with GCM mode adds integrity protection. The auth_tag method retrieves the authentication tag after encryption and must be set before decryption finalization. Authentication failure raises an exception rather than returning corrupted data.
# Authenticated encryption prevents tampering
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
cipher.key = key
cipher.iv = iv
# Optional additional authenticated data (not encrypted)
cipher.auth_data = "metadata"
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
# Decryption with authentication
decipher = OpenSSL::Cipher.new('AES-256-GCM')
decipher.decrypt
decipher.key = key
decipher.iv = iv
decipher.auth_tag = tag
decipher.auth_data = "metadata"
begin
recovered = decipher.update(ciphertext) + decipher.final
rescue OpenSSL::Cipher::CipherError
# Authentication failed - data was tampered with
raise "Integrity check failed"
end
RSA implementation in Ruby uses the PKey::RSA class for keypair generation and operations. Key size directly impacts security and performance. Generate keys once and store them securely rather than regenerating for each operation.
# Generate RSA keypair
rsa_key = OpenSSL::PKey::RSA.new(2048)
# Extract public and private components
public_key = rsa_key.public_key
private_key = rsa_key
# Save keys to PEM format
private_pem = rsa_key.to_pem
public_pem = public_key.to_pem
# Load keys from PEM
loaded_private = OpenSSL::PKey::RSA.new(private_pem)
loaded_public = OpenSSL::PKey::RSA.new(public_pem)
# Encrypt with public key
plaintext = "Session key"
encrypted = public_key.public_encrypt(
plaintext,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Decrypt with private key
decrypted = private_key.private_decrypt(
encrypted,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
Hybrid encryption combines both approaches. Asymmetric encryption protects a randomly generated symmetric key, and symmetric encryption handles the actual data. This pattern provides the security properties of asymmetric encryption with the performance of symmetric encryption.
# Hybrid encryption implementation
def hybrid_encrypt(plaintext, public_key)
# Generate random symmetric key
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
sym_key = cipher.random_key
iv = cipher.random_iv
cipher.key = sym_key
cipher.iv = iv
# Encrypt data with symmetric cipher
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
# Encrypt symmetric key with public key
encrypted_key = public_key.public_encrypt(
sym_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
{
encrypted_key: Base64.strict_encode64(encrypted_key),
ciphertext: Base64.strict_encode64(ciphertext),
iv: Base64.strict_encode64(iv),
tag: Base64.strict_encode64(tag)
}
end
def hybrid_decrypt(package, private_key)
# Decrypt symmetric key
encrypted_key = Base64.strict_decode64(package[:encrypted_key])
sym_key = private_key.private_decrypt(
encrypted_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Decrypt data with symmetric cipher
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = sym_key
cipher.iv = Base64.strict_decode64(package[:iv])
cipher.auth_tag = Base64.strict_decode64(package[:tag])
ciphertext = Base64.strict_decode64(package[:ciphertext])
cipher.update(ciphertext) + cipher.final
end
Digital signatures use the sign and verify methods. The signer creates a hash of the message and encrypts it with their private key. Verifiers decrypt the signature with the public key and compare hashes.
# Create digital signature
data = "Important document"
signature = private_key.sign('SHA256', data)
# Verify signature
valid = public_key.verify('SHA256', signature, data)
if valid
# Signature valid: data originated from private key holder
# and has not been modified
else
# Signature invalid: either wrong key or tampered data
raise "Signature verification failed"
end
Elliptic curve operations use the PKey::EC class. Curve selection balances security and performance. The prime256v1 curve provides 128-bit security and wide compatibility.
# Generate EC keypair
ec_key = OpenSSL::PKey::EC.generate('prime256v1')
ec_public = OpenSSL::PKey::EC.new(ec_key.group)
ec_public.public_key = ec_key.public_key
# ECDH key exchange
other_party = OpenSSL::PKey::EC.generate('prime256v1')
shared_secret = ec_key.dh_compute_key(other_party.public_key)
# Derive symmetric key from shared secret
kdf = OpenSSL::KDF.pbkdf2_hmac(
shared_secret,
salt: OpenSSL::Random.random_bytes(16),
iterations: 100_000,
length: 32,
hash: 'sha256'
)
Practical Examples
File encryption requires encrypting file contents and storing metadata needed for decryption. The IV and encrypted data must be preserved. Authentication tags verify file integrity. Hybrid encryption handles files too large for direct asymmetric encryption.
require 'openssl'
require 'base64'
require 'json'
class FileEncryptor
def self.encrypt_file(input_path, output_path, public_key)
plaintext = File.binread(input_path)
# Generate symmetric key and IV
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
sym_key = cipher.random_key
iv = cipher.random_iv
cipher.key = sym_key
cipher.iv = iv
# Encrypt file content
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
# Encrypt symmetric key with recipient's public key
encrypted_key = public_key.public_encrypt(
sym_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Package everything needed for decryption
package = {
encrypted_key: Base64.strict_encode64(encrypted_key),
iv: Base64.strict_encode64(iv),
tag: Base64.strict_encode64(tag),
ciphertext: Base64.strict_encode64(ciphertext)
}
File.write(output_path, JSON.generate(package))
end
def self.decrypt_file(input_path, output_path, private_key)
package = JSON.parse(File.read(input_path), symbolize_names: true)
# Decrypt symmetric key
encrypted_key = Base64.strict_decode64(package[:encrypted_key])
sym_key = private_key.private_decrypt(
encrypted_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Decrypt file content
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = sym_key
cipher.iv = Base64.strict_decode64(package[:iv])
cipher.auth_tag = Base64.strict_decode64(package[:tag])
ciphertext = Base64.strict_decode64(package[:ciphertext])
plaintext = cipher.update(ciphertext) + cipher.final
File.binwrite(output_path, plaintext)
end
end
# Usage
keypair = OpenSSL::PKey::RSA.new(2048)
FileEncryptor.encrypt_file('document.pdf', 'document.enc', keypair.public_key)
FileEncryptor.decrypt_file('document.enc', 'restored.pdf', keypair)
Secure message exchange between parties requires each party to have a keypair. Senders encrypt with recipient public keys and sign with their own private key. Recipients verify the signature and decrypt the message.
class SecureMessaging
def initialize(private_key)
@private_key = private_key
@public_key = private_key.public_key
end
attr_reader :public_key
def send_message(plaintext, recipient_public_key)
# Hybrid encrypt the message
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
sym_key = cipher.random_key
iv = cipher.random_iv
cipher.key = sym_key
cipher.iv = iv
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
# Encrypt symmetric key for recipient
encrypted_key = recipient_public_key.public_encrypt(
sym_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Sign the ciphertext for authentication
signature = @private_key.sign('SHA256', ciphertext)
{
encrypted_key: Base64.strict_encode64(encrypted_key),
ciphertext: Base64.strict_encode64(ciphertext),
iv: Base64.strict_encode64(iv),
tag: Base64.strict_encode64(tag),
signature: Base64.strict_encode64(signature)
}
end
def receive_message(message, sender_public_key)
# Verify signature first
ciphertext = Base64.strict_decode64(message[:ciphertext])
signature = Base64.strict_decode64(message[:signature])
unless sender_public_key.verify('SHA256', signature, ciphertext)
raise "Signature verification failed"
end
# Decrypt symmetric key
encrypted_key = Base64.strict_decode64(message[:encrypted_key])
sym_key = @private_key.private_decrypt(
encrypted_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Decrypt message
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = sym_key
cipher.iv = Base64.strict_decode64(message[:iv])
cipher.auth_tag = Base64.strict_decode64(message[:tag])
cipher.update(ciphertext) + cipher.final
end
end
# Usage
alice = SecureMessaging.new(OpenSSL::PKey::RSA.new(2048))
bob = SecureMessaging.new(OpenSSL::PKey::RSA.new(2048))
message = alice.send_message("Secret meeting at noon", bob.public_key)
decrypted = bob.receive_message(message, alice.public_key)
Key rotation involves re-encrypting data with new keys while maintaining availability. Asymmetric keys enable rotation without re-encrypting all data: generate new asymmetric keys and re-encrypt only the symmetric keys.
class DataVault
def initialize
@master_key = OpenSSL::PKey::RSA.new(4096)
@data_keys = {}
end
def store_data(id, plaintext)
# Generate unique data key for this item
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
data_key = cipher.random_key
iv = cipher.random_iv
cipher.key = data_key
cipher.iv = iv
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
# Encrypt data key with master key
encrypted_data_key = @master_key.public_key.public_encrypt(
data_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
@data_keys[id] = {
encrypted_key: encrypted_data_key,
iv: iv,
tag: tag,
ciphertext: ciphertext
}
end
def retrieve_data(id)
stored = @data_keys[id]
return nil unless stored
# Decrypt data key
data_key = @master_key.private_decrypt(
stored[:encrypted_key],
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Decrypt data
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = data_key
cipher.iv = stored[:iv]
cipher.auth_tag = stored[:tag]
cipher.update(stored[:ciphertext]) + cipher.final
end
def rotate_master_key(new_master_key)
# Re-encrypt all data keys with new master key
@data_keys.each do |id, stored|
# Decrypt with old key
data_key = @master_key.private_decrypt(
stored[:encrypted_key],
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
# Encrypt with new key
stored[:encrypted_key] = new_master_key.public_key.public_encrypt(
data_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
end
@master_key = new_master_key
end
end
Design Considerations
Symmetric encryption suits scenarios where all parties can securely share a key before communication begins. Database encryption at rest uses symmetric encryption because the database controls both encryption and decryption. Full disk encryption employs symmetric algorithms because the same system performs both operations. VPN tunnels use symmetric encryption after initial key establishment because both endpoints maintain the shared key.
The key distribution problem limits symmetric encryption. Each pair of communicating parties needs a unique key to prevent one party from reading another's messages. A system with N parties requires N(N-1)/2 keys for pairwise communication. Ten parties need 45 keys; a hundred parties need 4,950 keys. Key management complexity grows quadratically with participants.
Asymmetric encryption solves key distribution by eliminating shared secrets. Each party generates one keypair and publishes their public key. Anyone can encrypt messages to that party without prior coordination. A system with N parties needs only N keypairs. The recipient's private key never leaves their control, eliminating secure key transmission requirements.
Performance differences favor symmetric encryption for bulk data. AES encryption on modern processors with hardware acceleration achieves multiple gigabytes per second. RSA encryption processes kilobytes per second. A 1 MB file takes milliseconds with AES-256 but several seconds with RSA-2048. The size limitation in asymmetric encryption compounds the performance problem.
Hybrid encryption provides the security model of asymmetric encryption with symmetric performance. TLS protocol exemplifies this approach: the handshake uses asymmetric encryption to authenticate servers and establish a symmetric session key, then switches to symmetric encryption for data transfer. Applications encrypting large amounts of data for specific recipients should adopt this pattern.
# Poor: Multiple asymmetric encryption operations
def slow_encrypt_large_data(data, public_key)
# Cannot encrypt data larger than key size
# Must split into chunks - very slow
raise "Data too large for RSA" if data.bytesize > 190
public_key.public_encrypt(data, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
end
# Better: Hybrid approach
def fast_encrypt_large_data(data, public_key)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
sym_key = cipher.random_key
iv = cipher.random_iv
cipher.key = sym_key
cipher.iv = iv
# Fast symmetric encryption for data
ciphertext = cipher.update(data) + cipher.final
tag = cipher.auth_tag
# Single asymmetric operation for key
encrypted_key = public_key.public_encrypt(
sym_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
{ encrypted_key: encrypted_key, iv: iv, tag: tag, ciphertext: ciphertext }
end
Digital signatures require asymmetric cryptography. The signer must prove possession of the private key without revealing it. Symmetric authentication using HMACs requires shared secrets, which means the verifier could forge signatures. Asymmetric signatures enable non-repudiation: only the private key holder could have created the signature.
Key lifetime and rotation differ between approaches. Symmetric keys used for long periods increase exposure: if compromised, all historical data encrypted with that key becomes readable. Rotating symmetric keys requires re-encrypting all data or maintaining multiple key versions. Asymmetric keypairs support longer lifetimes because the private key never transmits, reducing compromise risk. Data keys can be re-encrypted without touching the actual data.
Certificate infrastructure depends on asymmetric cryptography. Certificate authorities sign public keys to create certificates, binding identities to keys. Verifiers check signatures using the CA's public key. This trust model enables secure communication with unknown parties. Symmetric cryptography cannot provide this property because shared secrets require prior relationships.
Algorithm selection involves multiple factors. AES-256 provides strong security with excellent performance on modern hardware. ChaCha20 offers better performance on devices without AES hardware acceleration. RSA-2048 remains secure but RSA-4096 provides longer-term security at performance cost. Elliptic curves like P-256 or Ed25519 offer equivalent security to larger RSA keys with better performance.
Forward secrecy requires generating temporary keys for each session. Diffie-Hellman key exchange establishes shared secrets without transmitting them. Each party generates an ephemeral keypair, exchanges public keys, and computes a shared secret through mathematical operations. After the session, discarding the ephemeral private keys makes the session key unrecoverable even if long-term keys are compromised.
Security Implications
Key generation quality determines encryption security. Weak random number generation undermines any algorithm. Operating system random number generators like /dev/urandom provide cryptographically secure randomness. Ruby's OpenSSL::Random uses the system's secure generator. Never seed encryption keys from predictable sources like timestamps or sequential counters.
# Secure key generation
key = OpenSSL::Random.random_bytes(32)
# Insecure - predictable
bad_key = OpenSSL::Digest::SHA256.digest(Time.now.to_s)
# Insecure - not cryptographically random
bad_key = Random.new.bytes(32)
Symmetric key exposure compromises all data encrypted with that key. Keys must be protected with the same security level as the plaintext. Storing keys in application code, configuration files, or version control exposes them. Environment variables offer marginal improvement but remain vulnerable to process inspection. Hardware security modules or key management services provide proper protection.
Private key compromise in asymmetric systems allows impersonation and decryption of past and future messages encrypted with the corresponding public key. Private keys demand exceptional protection. File system permissions should restrict access. Encryption at rest for private keys adds another layer. Hardware tokens or TPMs store private keys in tamper-resistant hardware.
Padding schemes affect security. PKCS#1 v1.5 padding for RSA suffers from vulnerabilities allowing plaintext recovery through timing attacks. OAEP padding provides security against known attacks. Always specify padding explicitly rather than relying on defaults.
# Insecure - uses weak PKCS#1 v1.5 padding
encrypted = public_key.public_encrypt(data)
# Secure - explicitly use OAEP
encrypted = public_key.public_encrypt(
data,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
IV reuse with the same key in CBC mode allows attackers to deduce information about plaintexts. GCM mode with reused IVs catastrophically fails, potentially revealing the authentication key. Generate a fresh random IV for each encryption operation. Store IVs alongside ciphertext. IVs do not require secrecy but must be unpredictable and never reused.
Authentication prevents tampering. Encryption alone does not guarantee integrity. An attacker who modifies CBC ciphertext causes predictable changes to plaintext. Authenticated encryption modes like GCM combine encryption and authentication. For modes without built-in authentication, compute an HMAC over the ciphertext using a separate key.
# Unauthenticated - vulnerable to tampering
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = key
cipher.iv = iv
ciphertext = cipher.update(plaintext) + cipher.final
# Authenticated - detects tampering
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = key
cipher.iv = iv
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
Key derivation from passwords requires key strengthening. Passwords contain insufficient entropy for direct use as encryption keys. KDFs like PBKDF2, bcrypt, or Argon2 increase computational cost to slow brute-force attacks. Use high iteration counts: PBKDF2 should iterate at least 100,000 times; adjust based on acceptable computation time and security requirements.
# Derive encryption key from password
password = "user password"
salt = OpenSSL::Random.random_bytes(16)
key = OpenSSL::KDF.pbkdf2_hmac(
password,
salt: salt,
iterations: 100_000,
length: 32,
hash: 'sha256'
)
# Store salt with ciphertext; never store password
Certificate validation prevents man-in-the-middle attacks in asymmetric systems. Applications must verify certificates against trusted root CAs, check expiration dates, validate hostnames, and honor revocation lists. Ruby's OpenSSL performs these checks when properly configured. Disabling verification for convenience eliminates security.
Algorithm obsolescence requires monitoring. DES, RC4, and MD5 are broken. SHA-1 is deprecated. RSA-1024 is insufficient. Systems must support algorithm upgrades without complete redesign. Crypto-agility means supporting multiple algorithms and transitioning between them as security research evolves.
Side-channel attacks extract keys through timing variations, power consumption, or electromagnetic radiation. Constant-time operations prevent timing attacks. Hardware encryption accelerators reduce side-channel vulnerability. Ruby code relying on OpenSSL benefits from C implementations designed to resist timing attacks.
Common Pitfalls
Encrypting data without authenticating it allows attackers to modify ciphertext and observe application behavior, potentially recovering plaintext. Padding oracle attacks exploit this vulnerability in CBC mode. Authentication must protect ciphertext integrity.
Hardcoded keys in source code become public when code is shared or repositories are accessed. Compromised keys require redeploying applications and re-encrypting all data. Keys belong in secure configuration or key management systems, never in code.
# Wrong - key in source code
def encrypt(data)
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = "this_is_a_bad_key_placement_32b"
# ...
end
# Right - key from secure source
def encrypt(data, key)
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = key
# ...
end
key = ENV['ENCRYPTION_KEY'] || KeyManagementService.fetch_key
Reusing IVs breaks security guarantees in most cipher modes. Developers sometimes use zero IVs or derive IVs from plaintexts to avoid storing them. Both approaches compromise security. Generate random IVs and store them with ciphertext.
Encrypting small value spaces allows brute-force attacks. A credit card encrypted with a strong algorithm remains vulnerable if attackers encrypt all possible card numbers and compare ciphertexts. Include additional random data or use format-preserving encryption for constrained domains.
Failing to handle decryption failures securely leaks information. Different error messages for authentication failures versus decryption failures allow attacks. Return generic failure messages and log details separately.
def decrypt_data(ciphertext, key, iv, tag)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = key
cipher.iv = iv
cipher.auth_tag = tag
begin
cipher.update(ciphertext) + cipher.final
rescue OpenSSL::Cipher::CipherError => e
# Wrong - reveals whether authentication or decryption failed
raise "Authentication failed: #{e.message}"
# Right - generic error, log details separately
logger.error("Decryption failed: #{e.message}")
raise "Decryption failed"
end
end
Using ECB mode exposes patterns in plaintext. ECB encrypts identical plaintext blocks to identical ciphertext blocks. Images encrypted with ECB mode remain recognizable. Always use CBC, CTR, GCM, or other modes that prevent pattern leakage.
Insufficient RSA key sizes compromise long-term security. RSA-1024 is broken by well-resourced attackers. RSA-2048 provides adequate security for the near future. RSA-4096 offers longer-term protection at performance cost. Generate new keys rather than continuing with short keys.
Omitting salt in password-based key derivation allows rainbow table attacks. The same password always produces the same key, allowing precomputed attack tables. Generate random salts for each key derivation and store them with the ciphertext.
Implementing cryptography rather than using established libraries introduces vulnerabilities. Timing attacks, padding vulnerabilities, and implementation flaws plague custom crypto code. Use OpenSSL through Ruby's standard library instead of implementing algorithms.
Misunderstanding key vs password leads to weak encryption. Users cannot remember 256-bit keys. Applications must derive keys from passwords using KDFs. Directly using passwords as keys provides minimal security.
Reference
Symmetric vs Asymmetric Comparison
| Aspect | Symmetric | Asymmetric |
|---|---|---|
| Key Structure | Single shared key | Public and private keypair |
| Key Distribution | Requires secure channel | Public key can be distributed openly |
| Speed | Very fast, gigabytes/second | Slow, kilobytes/second |
| Data Size Limit | Unlimited | Limited to key size minus padding |
| Key Management | O(N²) keys for N parties | O(N) keys for N parties |
| Primary Use Case | Bulk data encryption | Key exchange, digital signatures |
| Authentication | Requires HMAC or authenticated mode | Built-in with signatures |
| Forward Secrecy | Requires ephemeral keys | Supports with ephemeral keys |
Common Symmetric Algorithms
| Algorithm | Key Size | Block Size | Notes |
|---|---|---|---|
| AES-128 | 128 bits | 128 bits | Fast, widely supported |
| AES-192 | 192 bits | 128 bits | Stronger than AES-128 |
| AES-256 | 256 bits | 128 bits | Maximum AES security |
| ChaCha20 | 256 bits | Stream cipher | Better on devices without AES hardware |
| 3DES | 168 bits | 64 bits | Legacy, deprecated |
Common Asymmetric Algorithms
| Algorithm | Key Size | Security Level | Notes |
|---|---|---|---|
| RSA-2048 | 2048 bits | 112 bits | Current minimum recommendation |
| RSA-3072 | 3072 bits | 128 bits | Recommended for long-term security |
| RSA-4096 | 4096 bits | 140 bits | Maximum typical RSA key size |
| ECC P-256 | 256 bits | 128 bits | Equivalent to RSA-3072 |
| ECC P-384 | 384 bits | 192 bits | Equivalent to RSA-7680 |
| Ed25519 | 256 bits | 128 bits | Fast signatures, compact keys |
Cipher Modes
| Mode | Type | Authentication | Parallelizable | IV Requirements |
|---|---|---|---|---|
| CBC | Block | No | Decrypt only | Random, unique per message |
| CTR | Stream | No | Both | Random, unique per message |
| GCM | Stream | Yes | Both | Random, never reuse with same key |
| CCM | Block | Yes | No | Random, unique per message |
| ECB | Block | No | Both | None, insecure, do not use |
Ruby OpenSSL Cipher Methods
| Method | Purpose | Returns |
|---|---|---|
| Cipher.new(algorithm) | Create cipher object | Cipher instance |
| encrypt | Set encryption mode | nil |
| decrypt | Set decryption mode | nil |
| random_key | Generate random key | String |
| random_iv | Generate random IV | String |
| key= | Set encryption key | nil |
| iv= | Set initialization vector | nil |
| auth_data= | Set GCM additional data | nil |
| update(data) | Process data chunk | String |
| final | Complete operation | String |
| auth_tag | Get GCM authentication tag | String |
| auth_tag= | Set GCM authentication tag | nil |
Ruby OpenSSL RSA Methods
| Method | Purpose | Returns |
|---|---|---|
| RSA.new(bits) | Generate keypair | RSA instance |
| RSA.new(pem) | Load key from PEM | RSA instance |
| public_key | Extract public key | RSA instance |
| to_pem | Export as PEM format | String |
| public_encrypt(data, padding) | Encrypt with public key | String |
| private_decrypt(data, padding) | Decrypt with private key | String |
| sign(digest, data) | Create signature | String |
| verify(digest, signature, data) | Verify signature | Boolean |
Padding Schemes
| Scheme | Security | Use Case |
|---|---|---|
| PKCS1_OAEP_PADDING | Strong | RSA encryption |
| PKCS1_PADDING | Weak, deprecated | Legacy systems only |
| PKCS1_PSS_PADDING | Strong | RSA signatures |
| NO_PADDING | Dangerous | Never use without expert knowledge |
Key Derivation Parameters
| Parameter | Recommended Value | Purpose |
|---|---|---|
| PBKDF2 iterations | 100,000+ | Slow brute-force attacks |
| Salt length | 16+ bytes | Prevent rainbow tables |
| Output key length | 32 bytes for AES-256 | Match cipher requirements |
| Hash function | SHA-256 or stronger | Derive key material |
Hybrid Encryption Pattern
# Standard hybrid encryption implementation
def hybrid_encrypt(plaintext, public_key)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
sym_key = cipher.random_key
iv = cipher.random_iv
cipher.key = sym_key
cipher.iv = iv
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
encrypted_key = public_key.public_encrypt(
sym_key,
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
{ encrypted_key: encrypted_key, ciphertext: ciphertext, iv: iv, tag: tag }
end
def hybrid_decrypt(package, private_key)
sym_key = private_key.private_decrypt(
package[:encrypted_key],
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = sym_key
cipher.iv = package[:iv]
cipher.auth_tag = package[:tag]
cipher.update(package[:ciphertext]) + cipher.final
end