CrackedRuby CrackedRuby

Symmetric vs Asymmetric Encryption

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