2

I have a rails 2 application that I am trying to modify so that before an attribute is written to my MySql DB, it is encoded, and on read, it is decoded (not all attributes, just pre-determined ones).

I have looked at some gems, specifically attr-encrypted, but it doesn't do exactly what I want (I am also trying to avoid re-naming any of my existing table columns, which appears to be a requirement for attr-encrypted).

I have added a before_save filter to my model to do the attribute modification before it is saved to the DB, and I have overridden my attribute getter to do the decode. While this works, I want to do the decode lower in the stack (i.e. right after DB read) in order to have everything function correctly, without requiring system wide changes (it also simplifies the logic when deciding when to encode/decode).

So what it means is that I want to do the following:

1) On DB read, do the reverse, so that if i do a Model.last, the value for my attribute would be the decoded value (without having to explicitly call the attribute getter).

2) Override the find_by_* methods so that doing a search by my encoded attribute will encode the search term first, then do the db query using that value.

How would I go about doing that?

Dave Schweisguth
  • 36,475
  • 10
  • 98
  • 121
Hawkeye001
  • 791
  • 2
  • 11
  • 25
  • as an aside... adding encryption for security on a rails 2 app sounds like security theater. You really need to update the app, as rails 2 is unsupported. – DGM Mar 18 '16 at 17:17

2 Answers2

0

Update: this method unfortunately does not work in Rails 2. Custom serializers were probably added in Rails 3.

Original answer follows:

I think you can try to use a custom serializer as described in this blog post. This feature should be present even in Rails 2 (otherwise I guess these SO questions regarding it would not exist).

Sample serializer which encodes the attribute into Base64:

# app/models/model.rb
class Model < ActiveRecord::Base
  serialize :my_attr, MyEncodingSerializer
end

# lib/my_encoding_serializer.rb
class MyEncodingSerializer
  require "base64"

  def self.load(value)
    # called when loading the value from DB
    value.present? ? Base64.decode64(value) : nil
  end

  def self.dump(value)
    # called when storing the value into DB
    value.present? ? Base64.encode64(value) : nil
  end
end

Test in the rails console:

>> Model.create(my_attr: "my secret text")
D, [2016-03-14T07:17:26.493598 #14757] DEBUG -- :    (0.1ms)  BEGIN
D, [2016-03-14T07:17:26.494676 #14757] DEBUG -- :   SQL (0.6ms)  INSERT INTO `models` (`my_attr`) VALUES ('bXkgc2VjcmV0IHRleHQ=\n')
D, [2016-03-14T07:17:26.499356 #14757] DEBUG -- :    (4.4ms)  COMMIT
=> #<Model id: 4, my_attr: "my secret text">

You can see that the my_attr value gets automatically encoded before saving to the DB.

Loading from DB of course works transparently too:

>> Model.last
D, [2016-03-14T07:19:01.414567 #14757] DEBUG -- :   Model Load (0.2ms)  SELECT  `models`.* FROM `models`  ORDER BY `models`.`id` DESC LIMIT 1
=> #<Model id: 4, my_attr: "my secret text">

All finder helpers should work too, for example:

>> Model.find_by_my_attr("other text")
D, [2016-03-14T07:20:06.125670 #14757] DEBUG -- :   Model Load (0.3ms)  SELECT  `models`.* FROM `models` WHERE `models`.`my_attr` = 'b3RoZXIgdGV4dA==\n' LIMIT 1
=> nil  # nothing found here for wrong my_attr value

>> Model.find_by_my_attr("my secret text")
D, [2016-03-14T07:21:04.601898 #14757] DEBUG -- :   Model Load (0.6ms)  SELECT  `models`.* FROM `models` WHERE `models`.`my_attr` = 'bXkgc2VjcmV0IHRleHQ=\n' LIMIT 1
=> #<Model id: 4, my_attr: "my secret text">   # FOUND!
Community
  • 1
  • 1
Matouš Borák
  • 15,606
  • 1
  • 42
  • 53
  • 1
    I tried this but it doesn't look like custom serializers are supported in Rails 2 (though I haven't seen concrete documentation on this). If I try this approach on Rails 4, it works, if I do the same exact thing in my Rails 2 project, I get an error that "ActiveRecord::SerializationTypeMismatch: attribute was supposed to be a CustomEncoder, but was a String" – Hawkeye001 Mar 14 '16 at 16:33
  • I see. When looking into [Rails 2 serialization source code](http://apidock.com/rails/v2.3.8/ActiveRecord/AttributeMethods/unserialize_attribute), it works different indeed. In Rails 2 the class (the 2nd argument to `serialize`) means the class to which the attribute is **supposed to be deserialized**. And the attribute is always serialized (stored in db) **as YAML**. So, you are right, and custom serializers won't work in Rails 2, sorry. – Matouš Borák Mar 14 '16 at 17:17
0

It looks like rails 2 has the 'after_initialize' callback which should get you what you want (at a bit of a performance hit):

class Model < ActiveRecord::Base
  after_initialize do |model|
    # your decryption code here
  end
end

http://guides.rubyonrails.org/v2.3.11/activerecord_validations_callbacks.html#after-initialize-and-after-find

10dot
  • 111
  • 6
  • 10dot, the after_initialize method seems to work. So what I have done is use the before_save to encode the data before its written to the DB, and after_initialize to decode it. The issue I have now is that when I retrieve a record, and then modify it, in memory the values are encoded. I tried adding an after_save filter to also do the decode, but calling self[:attr] = "decoded value" is causing an active record rollback to be initiated...Any idea what might be causing that? – Hawkeye001 Mar 22 '16 at 04:18
  • I think it's because after_save is still inside the transaction block for the db update, so a change will cause validation failure. Try calling .reload() on whatever active record object you're updating after the update returns... That should force the after_initialize callback to fire again (I think). – 10dot Mar 23 '16 at 00:36