Thursday, April 26, 2007

[PLUGIN RELEASE] ActsAsSecure

Introduction

ActsAsSecure adds an ability to store ActiveRecord model's fields encrypted in a DB. When a model is marked with acts_as_secure, the :binary type fields are recognized as needed to be stored encrypted. The plugin does before_save/after_save/after_find encryption/decryption thus making it transparent for a code using secured models.

The plugin supports a master key approach as well as individual records encryption keys. It does not contain any crypto provider but allows to plug in any external one as long as it supports encrypt/decrypt methods.

The fields are converted to a YAML form before encryption. After description they are restored via YAML.load. Since fields are stored encrypted, the find usage is very limited.

Usage

Master Key Provider Usage

class SecureModel < ActiveRecord::Base
acts_as_secure :crypto_provider => MasterKeyProviderClassOrInstance
end

SecureModel.create()
SecureModel.find(:first)

Individual Keys Provider Usage
class SecureModel < ActiveRecord::Base
acts_as_secure
end

SecureModel.with_crypto_provider(SomeProvider.new(some_param)) { SecureModel.find(:first) }
SecureModel.with_crypto_provider(SomeProvider.new(some_param)) { SecureModel.create() }

Other Options

acts_as_secure :storage_type => :text -- changes the secure storage type from :binary to the supplied one
acts_as_secure :except => [:field1, :field2] -- disables the secure behavior for the :binary type fields in a supplied list


Example

Let's define three models: Fruit (not secure), SecretFruit (uses the master key approach), and UberSecretFruit (uses the individual key approach).

Rot13CryptoProvider represents a master key provider. SaltedRot13CryptoProvider depends on a salt individual for each record.

Models
class Fruit < ActiveRecord::Base
has_one :secret_fruit
has_one :uber_secret_fruit
end

class CreateFruits < ActiveRecord::Migration
def self.up
create_table :fruits do |t|
t.column :name, :string
end
end
end

class Rot13CryptoProvider
class << self
def encrypt(arg)
arg.tr("A-Za-z", "N-ZA-Mn-za-m")
end
alias_method :decrypt, :encrypt
end
end

class SecretFruit < ActiveRecord::Base
acts_as_secure :crypto_provider => Rot13CryptoProvider
belongs_to :fruit
end

class CreateSecretFruits < ActiveRecord::Migration
def self.up
create_table :secret_fruits do |t|
t.column :name, :binary
t.column :fruit_id, :integer
end
end
end

class SaltedRot13CryptoProvider
def initialize(salt)
@salt = salt
end
def encrypt(arg)
@salt + arg.tr("A-Za-z", "N-ZA-Mn-za-m")
end
def decrypt(arg)
arg[@salt.size .. -1].tr("A-Za-z", "N-ZA-Mn-za-m")
end
end

class UberSecretFruit < ActiveRecord::Base
acts_as_secure
belongs_to :fruit
end

class CreateUberSecretFruits < ActiveRecord::Migration
def self.up
create_table :uber_secret_fruits do |t|
t.column :name, :binary
t.column :fruit_id, :integer
end
end
end



Usage

>> f = Fruit.create(:name => 'passion fruit')
>> SecretFruit.create(:name => 'maracuya', :fruit => f)
>> puts f.secret_fruit.name
maracuya
>> secret = readline.chomp
uber_secret
>> crypto_provider = SaltedRot13CryptoProvider.new(secret)
>> UberSecretFruit.with_crypto_provider(crypto_provider) { UberSecretFruit.create(:name => 'Passiflora edulis', :fruit => f) }
>> UberSecretFruit.with_crypto_provider(crypto_provider) { puts f.uber_secret_fruit.name }
Passiflora edulis


DB
> select * from secret_fruits;
+----+---------------+----------+
| id | name | fruit_id |
+----+---------------+----------+
| 1 | --- znenphln | 1 |
+----+---------------+----------+

> select * from uber_secret_fruits;
+----+-----------------------------------+----------+
| id | name | fruit_id |
+----+-----------------------------------+----------+
| 1 | uber_secret--- Cnffvsyben rqhyvf | 1 |
+----+-----------------------------------+----------+


Installation

As plugin:
script/plugin install svn://rubyforge.org/var/svn/acts-as-secure/trunk/vendor/plugins/acts_as_secure


License

ActsAsSecure released under the MIT license.


Support

The plugin RubyForge page is http://rubyforge.org/projects/acts-as-secure

3 comments:

Anonymous said...

Thank you for this excellent plugin. I am wondering: how can we encrypt data and still have some full-text search feature on the encrypted fields?

Maybe doing a brute decode-and-lookup all records, and then cache the search result for further use of that query. Cache is only rebuilt when records are added/modified/removed. But the first search may be time-consuming, depending on a how many records.

Another interesting approach is that one: http://ieeexplore.ieee.org/xpl/freeabs_all.jsp?arnumber=1635501

When a record is added, keywords are extracted from data before encryption, and only this keyword index is searched.

Then you just have to adjust a fine balance between extracting enough keywords for the searches to keep enough accuracy and keeping the keywords ensembles meaningless enough so they do not reveal the content of the encrypted fields.

Any other point of view on the matter?

Val Aleksenko said...

Jonathan:

I would guess that the majority of use cases for this plugin would not require searching against the encrypted data (and that is the case for us).

I lost my IEEE membership a few years ago so I could not read the article. I'd think that is possible to extract some keywords and store it unencrypted with the record (i.e. metadata) and make it searchable. Another approach would be to utilize a search engine outside the DB (solar or ferret for the rails world) and to drop it there for ingestion before saving a secure record. Then it could be marked in the search results as secure, thus not showing any content but providing the link to the document. The index still needs some protection so you could not retrieve the sensitive information from there but since the service itself could be pushed down the stack for better protection and the content is fragmented in indexes, it makes is much harder to retrieve. Just a thought.

Aaron is going to blog more about how we utilize solar-based search for Revolution Pages

jl said...

I have been using your plugin and without it I couldn't make my project happen. Recently I am using to_xml in this project. I am not sure where the problem is but I am assuming on the to_xml side. What is happening is that when you output a activerecord, after running thru acts_as_secure, the binary fields are still showing up jumbled. To get around this I have set up a .xml.erb file and outputing my own xml. I have included an example below. The last name should show up as Lehman but shows up as you see it below. Don't know if you can help but thought I would pass it along.

<person>
<created_at type="datetime">2008-06-30T21:12:15-04:00</created_at>
<first_name>Jason</first_name>
<id type="integer">4</id>
<last_name encoding="base64" type="binary">TGVweWFu
</last_name>
<studio_id type="integer">1</studio_id>
<updated_at type="datetime">2008-06-30T21:12:24-04:00</updated_at>
<user_id type="integer">1</user_id>
</person>