Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ActiveRecord: Encryption silently fails when a db connection isn't already open #217

Closed
anicholson opened this issue Jun 10, 2016 · 12 comments

Comments

@anicholson
Copy link

anicholson commented Jun 10, 2016

We recently encountered a nasty bug where encryption silently failed:

Rails: No
AR Version: 4.2.1
attr_encrypted version: > 372ff14

Consider the following spec:

describe Credentials do

 before :each do
   @credentials = Credentials.create!(organisation_id: '23', credentials: 'test string')
 end

 it 'encrypts credentials' do # spec 1
   expect(@credentials.encrypted_credentials).to_not be_nil
   expect(@credentials.credentials).to_not be_nil
   expect(@credentials.credentials).to_not eq @credentials.encrypted_credentials
 end

 it 'saves encrypted credentials' do # spec 2
   credentials = Credentials.find_by_organisation_id('23').encrypted_credentials
   expect(credentials).to eq @credentials.encrypted_credentials
 end
end

describing the following model:

class Credentials < ActiveRecord::Base
 attr_encrypted :credentials, key: 'some_key' # REDACTED actual key
end

Spec 1 would consistently pass, but Spec 2 would consistently fail, because (surprisingly!) credentials == nil, not @credentials.encrypted_credentials.

After hours of head-scratching & digging around in the console, in desperation we traversed back up the attr_encrypted commit history looking for regressions.

We hit pay-dirt when we reverted past #160, when we got this error!

Projects/influx on store_encrypted_credentials [!?]
➔ be rspec spec/data_models/credentials_spec.rb
bundler: failed to load command: rspec (/Users/Lonnie/.rbenv/versions/2.3.1/bin/rspec)
ActiveRecord::ConnectionNotEstablished: No connection pool for Credentials
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-4.2.1/lib/active_record/connection_adapters/abstract/connection_pool.rb:566:in `retrieve_connection'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-4.2.1/lib/active_record/connection_handling.rb:113:in `retrieve_connection'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-4.2.1/lib/active_record/connection_handling.rb:87:in `connection'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-4.2.1/lib/active_record/model_schema.rb:230:in `table_exists?'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/attr_encrypted-28c4d7cd6622/lib/attr_encrypted/adapters/active_record.rb:59:in `attribute_instance_methods_as_symbols'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/attr_encrypted-28c4d7cd6622/lib/attr_encrypted.rb:130:in `block in attr_encrypted'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/attr_encrypted-28c4d7cd6622/lib/attr_encrypted.rb:125:in `each'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/attr_encrypted-28c4d7cd6622/lib/attr_encrypted.rb:125:in `attr_encrypted'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/attr_encrypted-28c4d7cd6622/lib/attr_encrypted/adapters/active_record.rb:51:in `attr_encrypted'
  /Users/Lonnie/Projects/influx/lib/data_models/credentials.rb:2:in `<class:Credentials>'
  /Users/Lonnie/Projects/influx/lib/data_models/credentials.rb:1:in `<top (required)>'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:274:in `require'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:274:in `block in require'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:240:in `load_dependency'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-4.2.1/lib/active_support/dependencies.rb:274:in `require'
  /Users/Lonnie/Projects/influx/config/environment.rb:14:in `block in <top (required)>'
  /Users/Lonnie/Projects/influx/config/environment.rb:14:in `each'
  /Users/Lonnie/Projects/influx/config/environment.rb:14:in `<top (required)>'
  /Users/Lonnie/Projects/influx/spec/spec_helper.rb:38:in `require'
  /Users/Lonnie/Projects/influx/spec/spec_helper.rb:38:in `<top (required)>'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/configuration.rb:1295:in `require'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/configuration.rb:1295:in `block in requires='
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/configuration.rb:1295:in `each'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/configuration.rb:1295:in `requires='
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/configuration_options.rb:109:in `block in process_options_into'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/configuration_options.rb:108:in `each'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/configuration_options.rb:108:in `process_options_into'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/configuration_options.rb:21:in `configure'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/runner.rb:105:in `setup'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/runner.rb:92:in `run'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/runner.rb:78:in `run'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib/rspec/core/runner.rb:45:in `invoke'
  /Users/Lonnie/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/exe/rspec:4:in `<top (required)>'
  /Users/Lonnie/.rbenv/versions/2.3.1/bin/rspec:23:in `load'
  /Users/Lonnie/.rbenv/versions/2.3.1/bin/rspec:23:in `<top (required)>'
Coverage report generated for RSpec to /Users/Lonnie/Projects/influx/coverage. 52 / 125 LOC (41.6%) covered.

Wha?!

And so we realised that attr_encrypted requires the db connection to be present before a model that uses it gets loaded. When we jiggled our environment.rb to connect to the db before the models loaded, the problem went away.

If #160 hadn't swallowed all the errors, we would have realised this much, much sooner. Is there a nicer way to achieve the same result?

@saghaulor
Copy link
Contributor

@anicholson Thank you for you very well written bug report!

I think swallowing all the errors is bad. I'm sorry I let that code slip in. I think if anything we should at the bare minimum remove the rescue.

I think it's reasonable to expect the DB to be connected. ActiveRecord does a lot of magic based on the DB. However, we definitely should be bubbling up issues.

Would you like to open a PR or shall I?

@anicholson
Copy link
Author

@saghaulor I'm happy to, but it'll be next couple days :)

@anicholson
Copy link
Author

I've got no beef with AE expecting the db to be connected, btw, just that it should be called out more :)

@anicholson
Copy link
Author

@saghaulor I'm not sure how to proceed without re-introducing an error for @DouweM (and anyone else who wants to precompile assets without a db present). Suggestions welcomed :)

@anicholson
Copy link
Author

ping @saghaulor :)

@bpinto
Copy link

bpinto commented Jul 28, 2016

@anicholson Maybe by using attr_accessor to define the methods?

@mstahl
Copy link

mstahl commented Oct 11, 2016

Hey this is happening to me too, in a Sinatra app that uses the Sinatra ActiveRecord extensions. My records store without throwing any errors/exceptions, the saved model has all its attributes set correctly, but all the encrypted_... and encrypted_..._iv columns are null in the database. Setting the columns to strings instead of binary fixed that issue, but then I get a "bad decrypt" error every time I try to use one of the encrypted attrs. I've included attr_encrypted in a fresh vanilla Rails app and it worked as advertised; is this an issue that only affects non-Rails users?

@mstahl
Copy link

mstahl commented Oct 11, 2016

I've just done some more digging. When I copypaste @anicholson's spec, with variable names changed around (weird coincidence that my model is also called a Credential... must be a common use case 😉 ), I actually get both specs passing as they're written. Where I get a failure is when I try to find a credential with the same id as @credential, then try to decrypt a field off of it, like username; I get an OpenSSL exception "bad decrypt". Here's my version of the same spec:

    # https://github.com/attr-encrypted/attr_encrypted/issues/217
    context 'attr_encrypted issue #217', :focus do
      before :each do
        @credential = Credential.create!(
          account_name: 'An Account Name',
          username: 'A Username',
          password: 'A Password',
        )
      end

      it 'encrypts credentials' do
        expect(@credential.encrypted_username).to_not be_nil
        expect(@credential.username).to_not be_nil
        expect(@credential.username).to_not eq @credential.encrypted_username
      end

      it 'saves encrypted credentials' do
        # This part passes just fine for me
        encrypted_username = Credential.find(@credential.id).encrypted_username
        expect(encrypted_username).to eq @credential.encrypted_username
        expect(@credential.reload.username).to eq 'A Username' # This passes
        expect(Credential.find(@credential.id).username).to eq 'A Username' # This fails with "bad decrypt" error
      end
    end

@jturkel
Copy link

jturkel commented Sep 15, 2017

We hit this issue in production. A brief DNS issue when our node started up caused DB connection errors when initializing the attr_encrypted columns for our ActiveRecord models. These errors were silently ignored due to #160 and attr_encrypted created regular Ruby instance variables for our encrypted attributes (rather than delegating to ActiveRecord for reading/write the attribute values). The end result was a server that returned nil for any encrypted columns (because it was reading from regular instance variables rather than ActiveRecord managed attributes) and wrote nil to the database for any encrypted columns (because it was writing regular instance variable rather than ActiveRecord managed attributes).

Perhaps we could avoid using the DB connection altogether during startup if we get rid of the attr_encrypted falling back to regular Ruby instance variables for encrypted attributes on ActiveRecord models? It looks like the methods generated in AttrEncrypted::Adapters::ActiveRecord.attr_encrypted (e.g. "#{attr}_changed?") already assume the attribute is an ActiveRecord managed attribute. This approach does mean that trying to encrypt an attribute that didn't exist would only raise an error when the attribute was read/written rather than declared. I'm happy to put together a PR if you're open to this change.

@chendo
Copy link

chendo commented Dec 12, 2017

We have the same issue which took a long time to track down. This was manifesting as a failed NULL constraint in a column for us so at least it wasn't silently dropping it, but was very confusing for us.

@naiyt
Copy link

naiyt commented Jan 11, 2018

@anicholson you mentioned "When we jiggled our environment.rb to connect to the db before the models loaded, the problem went away". What did you change in your environment.rb to get Rails to do that? I haven't had any luck finding any documentation on how to do that.

(I'm running into this issue, and haven't been able to find a workaround.)

@saghaulor
Copy link
Contributor

I believe that #294 should fix this issue. As such, I'm closing this issue. If it does not resolve your issue please feel free to reopen the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants