HIGH bleichenbacher attackhanamicockroachdb

Bleichenbacher Attack in Hanami with Cockroachdb

Bleichenbacher Attack in Hanami with Cockroachdb — how this specific combination creates or exposes the vulnerability

A Bleichenbacher attack is a padding oracle attack against RSA encryption, where an attacker submits many ciphertexts and observes error differences to gradually decrypt data or recover the plaintext. Hanami applications that use RSA-OAEP or RSAES-PKCS1-v1_5 and perform decryption on the server side can be vulnerable when error handling is inconsistent. When such an application uses CockroachDB as the backend, the storage and retrieval of encrypted values can inadvertently expose timing or error behavior that aids an attacker.

In Hanami, if encrypted data (for example, a serialized session or a token) is stored in a CockroachDB column and later decrypted in Ruby code, two conditions can create a padding oracle:

  • The application returns distinct errors for malformed ciphertext versus decryption failures (e.g., padding errors versus record-not-found).
  • Cockroachdb queries or ActiveModel-like queries produce different response times or errors depending on whether a record exists, and this difference is observable in the application’s error messages or HTTP status codes.

An attacker can exploit this by iteratively submitting modified ciphertexts and observing whether the service responds with a padding error or another error (e.g., record not found). If the decryption routine is implemented in Hanami services that read encrypted fields from Cockroachdb, and the error messages or timing differ, the attacker can gradually learn plaintext without having access to the private key. This is particularly risky when encrypted values are stored in columns that are indexed or frequently queried, because the interaction between Cockroachdb query behavior and Hanami’s error handling may amplify distinguishing information.

For example, a crafted request to an endpoint like /users/:id/preferences that decrypts a cookie-stored token using an RSA private key may return a 400 with Invalid padding for certain ciphertexts and a 404 or 500 for others, depending on whether the record exists in Cockroachdb. These differences allow a Bleichenbacher-style adaptive chosen-ciphertext attack to converge on the plaintext. The presence of Cockroachdb can affect timing due to network latency, index lookups, or transaction retries, which may make the oracle slightly more detectable but does not remove the root cause, which is the server-side decryption with distinguishable errors.

Cockroachdb-Specific Remediation in Hanami — concrete code fixes

Remediation focuses on ensuring that decryption operations do not leak information via errors or timing, and that data stored in Cockroachdb does not enable oracle behavior. Below are concrete Hanami patterns and Cockroachdb interactions to fix the issue.

1. Use constant-time decryption and uniform error handling

Never distinguish between missing records and bad padding. Return the same HTTP status and generic message for any decryption or validation failure.

module Security::Crypto
  RSA_PRIVATE_KEY = OpenSSL::PKey::RSA.new(File.read('priv/keys/server.pem'))

  def self.decrypt_symmetric(data:, key:)
    # Use a constant-time comparison where possible and ensure errors do not vary by input
    begin
      # Example with RSA decryption; ensure padding errors do not bubble as distinct exceptions
      plain = RSA_PRIVATE_KEY.private_decrypt(data, OpenSSL::PKey::RSA::NO_PADDING)
      # Further validate and parse plain; do not raise custom errors that differ by cause
      JSON.parse(plain) # may raise JSON::ParserError — handle uniformly
    rescue OpenSSL::PKey::RSAError, JSON::ParserError => e
      # Log the error internally for diagnostics, but return a generic response to the caller
      Hanami.logger.error("Decryption or parsing failed: #{e.class}")
      raise Entity::Errors::InvalidPayloadError # a single, consistent error type
    end
  end
end

2. Avoid decrypting on query boundaries; decrypt after uniform data retrieval

Retrieve the row from Cockroachdb with a consistent query pattern, then decrypt in memory. Avoid branching logic based on presence before decryption.

# app/entities/user_preferences.rb
class UserPreferences
  include Hanami::Entity

  attribute :encrypted_data, Types::Strict::String
  alias_method :cipher_blob, :encrypted_data

  def plaintext_preferences(key)
    Security::Crypto.decrypt_symmetric(data: Base64.strict_decode64(cipher_blob), key: key)
  end
end

# app/repositories/user_preferences_repo.rb
class UserPreferencesRepo
  def initialize(relation: UserPreferencesRepository)
    @relation = relation
  end

  def find_for_user(user_id, decryption_key)
    # Always perform the same query; do not conditionally skip WHERE to avoid timing leaks
    row = @relation.where(user_id: user_id).limit(1).one
    return nil if row.nil?
    UserPreferences.new(row.to_h).plaintext_preferences(decryption_key)
  rescue Entity::Errors::InvalidPayloadError
    # Return nil or a default structure, never propagate padding-specific errors
    nil
  end
end

3. Use application-level encryption with AEAD where possible

If feasible, switch to an authenticated encryption mode (e.g., AES-GCM) so that decryption either succeeds and returns valid plaintext, or fails with a verifiable integrity error. This reduces the risk of padding oracles regardless of the database behavior.

# Example using AES-GCM (preferred over RSA where feasible)
module Security::Crypto
  def self.aead_encrypt(plaintext, key)
    cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
    cipher.key = key
    iv = cipher.random_iv
    cipher.encrypt
    ciphertext = cipher.update(plaintext) + cipher.final
    { ciphertext: ciphertext, iv: iv, tag: cipher.auth_tag }
  end

  def self.aead_decrypt(payload, key)
    decipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
    decipher.key = key
    decipher.iv = payload[:iv]
    decipher.auth_tag = payload[:tag]
    decipher.decrypt
    decipher.update(payload[:ciphertext]) + decipher.final
  rescue OpenSSL::Cipher::CipherError
    raise Entity::Errors::InvalidPayloadError
  end
end

4. Ensure Cockroachdb interactions do not introduce observable differences

Use parameterized queries consistently and avoid conditional queries that depend on the existence of a row before decryption. If you must check existence, perform the check in a way that does not change timing characteristics or error semantics (e.g., always fetch a row and handle nil in Ruby).

# Always use the same query shape; do not branch on presence before decrypting
rows = DB['SELECT id, encrypted_data FROM user_preferences WHERE user_id = $1 LIMIT 1', user_id].to_a
row = rows.first
if row
  begin
    prefs = UserPreferences.new(row).plaintext_preferences(master_key)
  rescue Entity::Errors::InvalidPayloadError
    prefs = nil
  end
else
  prefs = nil
end

Frequently Asked Questions

Can a Bleichenbacher attack work if errors are uniform but timing differences between Cockroachdb queries are significant?
Yes. Even with uniform error messages, measurable timing differences (e.g., index hit vs miss, transaction retries, or network latency to Cockroachdb) can enable a Bleichenbacher attack. Use constant-time cryptographic operations and avoid branching on sensitive data to reduce timing variance.
Does using Cockroachdb’s native encryption or column-level encryption mitigate padding oracle risks in Hanami?
Not by itself. If your application still performs RSA decryption in Ruby with distinguishable errors, server-side encryption at rest does not prevent a padding oracle. Focus on uniform error handling and prefer AEAD primitives where feasible.