Skip to content

Commit

Permalink
changed out how encrypted data is stored, added tests, docs
Browse files Browse the repository at this point in the history
  • Loading branch information
ssoper committed Mar 27, 2009
1 parent fb1da84 commit 34b90c9
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 46 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
*.gem
12 changes: 11 additions & 1 deletion Changelog
@@ -1,2 +1,12 @@
3-25-2009
== 3-26-2009

* Changed how encrypted data is saved, now put on separate column in the same table as encrypted model
* Added tests
* Can configure name of encrypted column
* Added documentation
* Log warning if default sample keys are being used while in production mode


== 3-25-2009

* Initial build
34 changes: 29 additions & 5 deletions README.rdoc
Expand Up @@ -10,19 +10,33 @@ Run this if the gem isn't installed already

Or place in your environment.rb
config.gem 'ssoper-acts_as_encryptable', :source => 'http://gems.github.com'

After installing be sure to generate the migration for the EncryptedChunk model
./script/generate migration acts_as_encryptable_migration


== Usage
== Configuration

In your model
After installing be sure to add a column to any tables that need to be encrypted
class AddEncryptionFieldsToCreditCards < ActiveRecord::Migration
def self.up
add_column :credit_cards, :encrypted, :text
end

def self.down
remove_column :credit_cards, :encrypted
end
end

Or you can use the migration helper to generate the migration for the model
./script/generate acts_as_encryptable_migration credit_cards

And in your model
class CreditCard < ActiveRecord::Base
acts_as_encryptable :first_name, :last_name, :number
attr_accessor :first_name, :last_name, :number
end


== Usage

Encrypt your data
card = CreditCard.new
card.first_name = 'Test'
Expand All @@ -35,3 +49,13 @@ Decrypt your data
card.decrypt!
=> { :first_name => 'Test', :last_name => 'User', :number => '1234567890' }


== Tests

Run 'rake test'


== Acknowledgements

* Tobias Lütke for his blog post on asymmetric encryption using Ruby's native SSL libraries
* Paul Barry for helping me to simplify the functionality
23 changes: 19 additions & 4 deletions Rakefile
@@ -1,21 +1,36 @@
require 'rake'
require 'rake/testtask'

namespace :gem do

task :default => :build

desc 'Build the acts_as_encryptable gem'
task :build do
Dir['*.gem'].each do |gem_filename|
sh "rm -rf #{gem_filename}"
end
sh "gem build acts_as_encryptable.gemspec"
end

desc 'Install the acts_as_encryptable gem'
task :install do
gem_filename = Dir['*.gem'].first
sh "sudo gem install --local #{gem_filename}"
end

end

task :default => ['gem:build', 'gem:install']
task :default => ['gem:build', 'gem:install']

namespace :test do
Rake::TestTask.new(:unit) do |t|
t.libs << 'test'
t.pattern = 'test/unit/*_test.rb'
t.verbose = true
end
end

task :test do
Rake::Task['test:unit'].invoke
end
6 changes: 0 additions & 6 deletions TODO

This file was deleted.

12 changes: 5 additions & 7 deletions acts_as_encryptable.gemspec
@@ -1,20 +1,18 @@
Gem::Specification.new do |s|
s.name = "acts_as_encryptable"
s.version = "1.0.0"
s.date = "2009-03-25"
s.version = "1.0.1"
s.date = "2009-03-26"
s.author = "Sean Soper"
s.email = "sean.soper@gmail.com"
s.summary = "Encrypt and decrypt your important data"
s.description = "Encrypt your data into chunks stored in the database and attached to your model via a polymorphic relationship"
s.homepage = "http://github.com"
s.summary = "Encrypt and decrypt your data with ease"
s.description = "Encrypt and decrypt your data using 1024-bit asymmetric RSA keys"
s.homepage = "http://github.com/ssoper/acts_as_encryptable"
s.require_path = "lib"
s.files = %w{ acts_as_encryptable.gemspec
lib/acts_as_encryptable.rb
lib/acts_as_encryptable/base.rb
lib/acts_as_encryptable/crypto.rb
lib/acts_as_encryptable/encrypted_chunk.rb
generators/acts_as_encryptable_migration/acts_as_encryptable_migration_generator.rb
generators/acts_as_encryptable_migration/templates/migration.rb
sample_keys/rsa_key.pub
sample_keys/rsa_key
MIT-LICENSE
Expand Down
@@ -1,7 +1,25 @@
class ActsAsEncryptableMigrationGenerator < Rails::Generator::Base
def manifest
class ActsAsEncryptableMigrationGenerator < Rails::Generator::NamedBase
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "acts_as_encryptable_migration"
m.migration_template 'migration:migration.rb', 'db/migrate', {
:assigns => migration_local_assigns,
:migration_file_name => "add_encryption_field_to_#{model_name}"
}
end
end

private

def model_name
return ARGV.first
end

def migration_local_assigns
returning(assigns = {}) do
assigns[:migration_action] = "add"
assigns[:class_name] = "add_encryption_field_to_#{model_name}"
assigns[:table_name] = model_name
assigns[:attributes] = [Rails::Generator::GeneratedAttribute.new("encrypted", "text")]
end
end
end
1 change: 0 additions & 1 deletion lib/acts_as_encryptable.rb
@@ -1,3 +1,2 @@
require 'acts_as_encryptable/base'
require 'acts_as_encryptable/crypto'
require 'acts_as_encryptable/encrypted_chunk'
59 changes: 41 additions & 18 deletions lib/acts_as_encryptable/base.rb
@@ -1,5 +1,9 @@
module ActsAsEncryptable
module Base
WARNING_MSG = "\n \e[0m\e[1;36m[\e[37mActsAsEncryptable\e[36m] \e[1;31mUsing the provided sample keys in production mode is highly discouraged. Generate your own RSA keys using the provided crypto libraries.\e[0m\n\n"
MAX_CHUNK_SIZE = 118
ENCRYPTED_CHUNK_SIZE = 175

def self.included(base)
base.extend(ClassMethods)
end
Expand All @@ -10,12 +14,21 @@ def acts_as_encryptable(*args)
has_many :encrypted_chunks, :as => :encryptable
before_save :encrypt!

sample_key_public = File.join(File.dirname(__FILE__), '/../../sample_keys/rsa_key.pub')
sample_key_private = File.join(File.dirname(__FILE__), '/../../sample_keys/rsa_key')

options = {
:public_key => File.join(File.dirname(__FILE__), '/../../sample_keys/rsa_key.pub'),
:private_key => File.join(File.dirname(__FILE__), '/../../sample_keys/rsa_key')
:public_key => sample_key_public,
:private_key => sample_key_private,
:column => :encrypted
}
options.merge!(args.pop) if args.last.is_a? Hash

if (sample_key_public == options[:public_key]) and
(ENV['RAILS_ENV'] && ENV['RAILS_ENV'] == 'production')
RAILS_DEFAULT_LOGGER.warn WARNING_MSG
end

write_inheritable_attribute(:encrypted_fields, args.uniq)
class_inheritable_reader :encrypted_fields

Expand All @@ -25,43 +38,53 @@ def acts_as_encryptable(*args)
write_inheritable_attribute(:private_key, ActsAsEncryptable::Crypto::Key.from_file(options[:private_key]))
class_inheritable_reader :private_key

write_inheritable_attribute(:max_chunk_size, 115)
class_inheritable_reader :max_chunk_size
write_inheritable_attribute(:encrypted_column, options[:column])
class_inheritable_reader :encrypted_column

include ActsAsEncryptable::Base::InstanceMethods
extend ActsAsEncryptable::Base::SingletonMethods
end
end
end

module InstanceMethods
module SingletonMethods
def chunkize(str, chunk_size)
(0..((str.length/chunk_size.to_f).ceil - 1)).each do |x|
start, stop = x * chunk_size, (x + 1) * chunk_size
start += 1 if start > 0
yield str[start..stop]
end
end
end

module InstanceMethods
def encrypt!
yaml_data = self.class.encrypted_fields.inject({}) do |result, field|
result.merge!(field => instance_variable_get("@#{field}".to_sym))
end.to_yaml

chunks = []
(0..((yaml_data.length/self.class.max_chunk_size.to_f).ceil - 1)).each do |x|
start, stop = x * self.class.max_chunk_size, (x + 1) * self.class.max_chunk_size
start += 1 if start > 0
chunks << self.class.public_key.encrypt(yaml_data[start..stop])
chunks = ''
self.class.chunkize(yaml_data, MAX_CHUNK_SIZE) do |chunk|
chunks << self.class.public_key.encrypt(chunk)
end

self.encrypted_chunks = chunks.collect do |chunk|
EncryptedChunk.new(:data => chunk)
end
self.send("#{self.class.encrypted_column}=", chunks)
end

def decrypt!
yaml_data = self.encrypted_chunks.collect do |chunk|
self.class.private_key.decrypt(chunk.data)
end.join
encrypted_data = self.send("#{self.class.encrypted_column}")

chunks = ''
self.class.chunkize(encrypted_data, ENCRYPTED_CHUNK_SIZE) do |chunk|
chunks << self.class.private_key.decrypt(chunk)
end

YAML::load(yaml_data).each do |k, v|
YAML::load(chunks).each do |k, v|
instance_variable_set("@#{k}".to_sym, v)
end
end
end
end
end

ActiveRecord::Base.send(:include, ActsAsEncryptable::Base)
ActiveRecord::Base.send(:include, ActsAsEncryptable::Base)
2 changes: 1 addition & 1 deletion lib/acts_as_encryptable/crypto.rb
Expand Up @@ -43,4 +43,4 @@ def key_type
end
end
end
end
end
21 changes: 21 additions & 0 deletions test/test_helper.rb
@@ -0,0 +1,21 @@
require 'rubygems'
require 'test/unit'
require 'activerecord'

ENV['RAILS_ENV'] = 'test'
require File.dirname(__FILE__) + '/../lib/acts_as_encryptable'

class Test::Unit::TestCase

def establish_connection(db_file = nil)
db_file = File.join(File.dirname(__FILE__), '/tmp/tests.sqlite') unless db_file
ActiveRecord::Base.configurations = { 'ActiveRecord::Base' => { :adapter => 'sqlite3', :database => db_file, :timeout => 5000 } }
ActiveRecord::Base.establish_connection('ActiveRecord::Base')
ActiveRecord::Base.connection.execute('drop table if exists credit_cards')
ActiveRecord::Base.connection.execute('create table credit_cards (id integer, encrypted text)')
ActiveRecord::Base.connection.execute('drop table if exists people')
ActiveRecord::Base.connection.execute('create table people (id integer, important_data text)')
ActiveRecord::Base.connection
end

end
1 change: 1 addition & 0 deletions test/tmp/.gitignore
@@ -0,0 +1 @@
*.sqlite
69 changes: 69 additions & 0 deletions test/unit/base_test.rb
@@ -0,0 +1,69 @@
require File.dirname(__FILE__) + '/../test_helper'

class BaseTest < Test::Unit::TestCase

class CreditCard < ActiveRecord::Base
attr_accessor :name_on_card, :number, :expiration
acts_as_encryptable :name_on_card, :number, :expiration
end

class Person < ActiveRecord::Base
attr_accessor :first_name, :last_name, :ssn
acts_as_encryptable :first_name, :last_name, :ssn, :column => 'important_data'
end

def setup
@connection = establish_connection
end

def test_a_credit_card
card = CreditCard.new(valid_credit_card)
assert card.save
end

def test_data_is_encrypted
test_a_credit_card
card = CreditCard.last
assert !card.name_on_card
assert !card.number
assert !card.expiration
end

def test_data_is_decrypted
test_a_credit_card
card = CreditCard.last
card.decrypt!
assert card.name_on_card == valid_credit_card[:name_on_card]
assert card.number == valid_credit_card[:number]
assert card.expiration == valid_credit_card[:expiration]
end

def test_set_encrypted_column_name
person = Person.new(valid_person)
assert person.save!
person = Person.last
person.decrypt!
assert person.first_name = valid_person[:first_name]
assert person.last_name = valid_person[:last_name]
assert person.ssn = valid_person[:ssn]
end

private

def valid_credit_card
{
:name_on_card => 'Test User',
:number => '1234567890123456',
:expiration => (Date.today + 1.year).strftime("%i/%y")
}
end

def valid_person
{
:first_name => 'Test',
:last_name => 'User',
:ssn => '111223333'
}
end

end

0 comments on commit 34b90c9

Please sign in to comment.