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