ssoper-acts_as_encryptable 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog ADDED
@@ -0,0 +1,12 @@
1
+ == 3-26-2009
2
+
3
+ * Changed how encrypted data is saved, now put on separate column in the same table as encrypted model
4
+ * Added tests
5
+ * Can configure name of encrypted column
6
+ * Added documentation
7
+ * Log warning if default sample keys are being used while in production mode
8
+
9
+
10
+ == 3-25-2009
11
+
12
+ * Initial build
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Sean Soper
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,61 @@
1
+ = Acts As Encryptable
2
+
3
+ Encrypt and decrypt your data using asymmetric keys
4
+
5
+
6
+ == Installation
7
+
8
+ Run this if the gem isn't installed already
9
+ gem install ssoper-acts_as_encryptable --source=http://gems.github.com
10
+
11
+ Or place in your environment.rb
12
+ config.gem 'ssoper-acts_as_encryptable', :source => 'http://gems.github.com'
13
+
14
+
15
+ == Configuration
16
+
17
+ After installing be sure to add a column to any tables that need to be encrypted
18
+ class AddEncryptionFieldsToCreditCards < ActiveRecord::Migration
19
+ def self.up
20
+ add_column :credit_cards, :encrypted, :text
21
+ end
22
+
23
+ def self.down
24
+ remove_column :credit_cards, :encrypted
25
+ end
26
+ end
27
+
28
+ Or you can use the migration helper to generate the migration for the model
29
+ ./script/generate acts_as_encryptable_migration credit_cards
30
+
31
+ And in your model
32
+ class CreditCard < ActiveRecord::Base
33
+ attr_accessor :first_name, :last_name, :number
34
+ acts_as_encryptable :first_name, :last_name, :number
35
+ end
36
+
37
+
38
+ == Usage
39
+
40
+ Encrypt your data
41
+ card = CreditCard.new
42
+ card.first_name = 'Test'
43
+ card.last_name = 'User'
44
+ card.number = '1234567890'
45
+ card.save
46
+
47
+ Decrypt your data
48
+ card = CreditCard.last
49
+ card.decrypt!
50
+ => { :first_name => 'Test', :last_name => 'User', :number => '1234567890' }
51
+
52
+
53
+ == Tests
54
+
55
+ > rake test
56
+
57
+
58
+ == Acknowledgements
59
+
60
+ * Tobias Lütke for his blog post on asymmetric encryption using Ruby's native SSL libraries
61
+ * Paul Barry for helping simplify the functionality
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ namespace :gem do
5
+
6
+ task :default => :build
7
+
8
+ desc 'Build the acts_as_encryptable gem'
9
+ task :build do
10
+ Dir['*.gem'].each do |gem_filename|
11
+ sh "rm -rf #{gem_filename}"
12
+ end
13
+ sh "gem build acts_as_encryptable.gemspec"
14
+ end
15
+
16
+ desc 'Install the acts_as_encryptable gem'
17
+ task :install do
18
+ gem_filename = Dir['*.gem'].first
19
+ sh "sudo gem install --local #{gem_filename}"
20
+ end
21
+
22
+ end
23
+
24
+ task :default => ['gem:build', 'gem:install']
25
+
26
+ namespace :test do
27
+ Rake::TestTask.new(:unit) do |t|
28
+ t.libs << 'test'
29
+ t.pattern = 'test/unit/*_test.rb'
30
+ t.verbose = true
31
+ end
32
+ end
33
+
34
+ task :test do
35
+ Rake::Task['test:unit'].invoke
36
+ end
@@ -0,0 +1,36 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "acts_as_encryptable"
3
+ s.version = "1.0.4"
4
+ s.date = "2009-03-27"
5
+ s.author = "Sean Soper"
6
+ s.email = "sean.soper@gmail.com"
7
+ s.summary = "Encrypt and decrypt your data using asymmetric keys"
8
+ s.description = "Allow your models to encrypt an arbitrary amount of data using asymmetric keys"
9
+ s.homepage = "http://github.com/ssoper/acts_as_encryptable"
10
+ s.require_path = "lib"
11
+ s.files = %w{ acts_as_encryptable.gemspec
12
+ lib/acts_as_encryptable.rb
13
+ lib/acts_as_encryptable/base.rb
14
+ lib/acts_as_encryptable/crypto.rb
15
+ generators/acts_as_encryptable_migration/acts_as_encryptable_migration_generator.rb
16
+ sample_keys/rsa_key.pub
17
+ sample_keys/rsa_key
18
+ test/test_helper.rb
19
+ test/unit/base_test.rb
20
+ test/unit/crypto_test.rb
21
+ MIT-LICENSE
22
+ Rakefile
23
+ README.rdoc
24
+ Changelog }
25
+ s.has_rdoc = true
26
+ s.extra_rdoc_files = %w{ MIT-LICENSE
27
+ README.rdoc }
28
+ s.rdoc_options = ["--line-numbers",
29
+ "--inline-source",
30
+ "--title",
31
+ "acts_as_encryptable",
32
+ "--main",
33
+ "README.rdoc"]
34
+ s.rubygems_version = "1.3.1"
35
+ s.add_dependency "capistrano", ">= 2.5.0"
36
+ end
@@ -0,0 +1,25 @@
1
+ class ActsAsEncryptableMigrationGenerator < Rails::Generator::NamedBase
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration:migration.rb', 'db/migrate', {
5
+ :assigns => migration_local_assigns,
6
+ :migration_file_name => "add_encryption_field_to_#{model_name}"
7
+ }
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def model_name
14
+ return ARGV.first
15
+ end
16
+
17
+ def migration_local_assigns
18
+ returning(assigns = {}) do
19
+ assigns[:migration_action] = "add"
20
+ assigns[:class_name] = "add_encryption_field_to_#{model_name}"
21
+ assigns[:table_name] = model_name
22
+ assigns[:attributes] = [Rails::Generator::GeneratedAttribute.new("encrypted", "text")]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,2 @@
1
+ require 'acts_as_encryptable/base'
2
+ require 'acts_as_encryptable/crypto'
@@ -0,0 +1,90 @@
1
+ module ActsAsEncryptable
2
+ module Base
3
+ 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"
4
+ MAX_CHUNK_SIZE = 118
5
+ ENCRYPTED_CHUNK_SIZE = 175
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def acts_as_encryptable(*args)
13
+ self.class_eval do
14
+ has_many :encrypted_chunks, :as => :encryptable
15
+ before_save :encrypt!
16
+
17
+ sample_key_public = File.join(File.dirname(__FILE__), '/../../sample_keys/rsa_key.pub')
18
+ sample_key_private = File.join(File.dirname(__FILE__), '/../../sample_keys/rsa_key')
19
+
20
+ options = {
21
+ :public_key => sample_key_public,
22
+ :private_key => sample_key_private,
23
+ :column => :encrypted
24
+ }
25
+ options.merge!(args.pop) if args.last.is_a? Hash
26
+
27
+ if (sample_key_public == options[:public_key]) and
28
+ (ENV['RAILS_ENV'] && ENV['RAILS_ENV'] == 'production')
29
+ RAILS_DEFAULT_LOGGER.warn WARNING_MSG
30
+ end
31
+
32
+ write_inheritable_attribute(:encrypted_fields, args.uniq)
33
+ class_inheritable_reader :encrypted_fields
34
+
35
+ write_inheritable_attribute(:public_key, ActsAsEncryptable::Crypto::Key.from_file(options[:public_key]))
36
+ class_inheritable_reader :public_key
37
+
38
+ write_inheritable_attribute(:private_key, ActsAsEncryptable::Crypto::Key.from_file(options[:private_key]))
39
+ class_inheritable_reader :private_key
40
+
41
+ write_inheritable_attribute(:encrypted_column, options[:column])
42
+ class_inheritable_reader :encrypted_column
43
+
44
+ include ActsAsEncryptable::Base::InstanceMethods
45
+ extend ActsAsEncryptable::Base::SingletonMethods
46
+ end
47
+ end
48
+ end
49
+
50
+ module SingletonMethods
51
+ def chunkize(str, chunk_size)
52
+ (0..((str.length/chunk_size.to_f).ceil - 1)).each do |x|
53
+ start, stop = x * chunk_size, (x + 1) * chunk_size
54
+ start += 1 if start > 0
55
+ yield str[start..stop]
56
+ end
57
+ end
58
+ end
59
+
60
+ module InstanceMethods
61
+ def encrypt!
62
+ yaml_data = self.class.encrypted_fields.inject({}) do |result, field|
63
+ result.merge!(field => instance_variable_get("@#{field}".to_sym))
64
+ end.to_yaml
65
+
66
+ chunks = ''
67
+ self.class.chunkize(yaml_data, MAX_CHUNK_SIZE) do |chunk|
68
+ chunks << self.class.public_key.encrypt(chunk)
69
+ end
70
+
71
+ self.send("#{self.class.encrypted_column}=", chunks)
72
+ end
73
+
74
+ def decrypt!
75
+ encrypted_data = self.send("#{self.class.encrypted_column}")
76
+
77
+ chunks = ''
78
+ self.class.chunkize(encrypted_data, ENCRYPTED_CHUNK_SIZE) do |chunk|
79
+ chunks << self.class.private_key.decrypt(chunk)
80
+ end
81
+
82
+ YAML::load(chunks).each do |k, v|
83
+ instance_variable_set("@#{k}".to_sym, v)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ ActiveRecord::Base.send(:include, ActsAsEncryptable::Base)
@@ -0,0 +1,46 @@
1
+ require 'openssl'
2
+
3
+ module ActsAsEncryptable
4
+
5
+ # Originally posted by Tobias Lütke
6
+ # http://blog.leetsoft.com/2006/03/14/simple-encryption
7
+ module Crypto
8
+ def self.create_keys(priv = "rsa_key", pub = "#{priv}.pub", bits = 1024)
9
+ private_key = OpenSSL::PKey::RSA.new(bits)
10
+ File.open(priv, "w+") { |fp| fp << private_key.to_s }
11
+ File.open(pub, "w+") { |fp| fp << private_key.public_key.to_s }
12
+ private_key
13
+ end
14
+
15
+ class Key
16
+ def initialize(data)
17
+ @public = (data =~ /^-----BEGIN (RSA|DSA) PRIVATE KEY-----$/).nil?
18
+ @key = OpenSSL::PKey::RSA.new(data)
19
+ end
20
+
21
+ def self.from_file(filename)
22
+ self.new File.read( filename )
23
+ end
24
+
25
+ def encrypt(text)
26
+ Base64.encode64(@key.send("#{key_type}_encrypt", text))
27
+ end
28
+
29
+ def decrypt(text)
30
+ @key.send("#{key_type}_decrypt", Base64.decode64(text))
31
+ end
32
+
33
+ def private?
34
+ !@public
35
+ end
36
+
37
+ def public?
38
+ @public
39
+ end
40
+
41
+ def key_type
42
+ @public ? :public : :private
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIICWwIBAAKBgQDN2qBMNriTzfnGTcPkkPWR/Ep6jAcOBGjmFj15hPzshjgk++dr
3
+ Z74AVlE/3EsCZ1mY16T1UDz8c8izVr8AhDaixhZBrRkx4xzipjHZ98JJwQhVQQIA
4
+ 85j2jH+/sYHLetr4VbSNTfQBXcOmWltW7l8ABePXt0uEUbE2NYuPQNOu0QIDAQAB
5
+ AoGAE4MZlp/JNxlbB5TvcIbdAA1t8de8A3QfjU+mXBJi9vhx8e9+rAuVUurboLX8
6
+ 1il9sKMgG7CTV0qSR419ZUsi8ndJW5Lg4V3jLDJ4iSnNH4L7DWs7ZyoYq4Z0Rkav
7
+ lZJbvFo0dsl9ntWAgioemEhul7d+OYI2/Rzrn1Kulqb/EQECQQDxWB5iphmyhIiq
8
+ rSF344TXoFKHEKL6+EQbknPh2WkhFxzkx53/oKm3IszjbMkixOrmIYbxRyhXWL7c
9
+ WJ1fBOkpAkEA2lrI8pHo7k5qkFc9RBpjUiPqEhl9ZO+W8tS0XS45uCpxM+Z3Di9k
10
+ tafzkPIU11oeGrvYa6m5SEkgN34O1DhFaQJAYzkXRPeFGR/kEEeduuyPcRc41s7A
11
+ Mu5fEfbkLbZ0wmX+OxDWpIIpRGHKWrYe+2x6JqMiF5BpxX92+KB2EtqyAQJAG4U9
12
+ tnT1arOvcqnMKv04b23fXpCf4UzhNZHhea0N0UxoICZ38u2+P7b/V9FrFwlgqfXq
13
+ /QbTN20gBl549/5voQJAQPC7y93eEL73CY6FTNqETMrIT8dVmaAnQkvbk55Ratqv
14
+ XwxN+/Ec8Nvu12fTO+dYrf7IW6kbZ0zZ3pCQUIYsQQ==
15
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,5 @@
1
+ -----BEGIN RSA PUBLIC KEY-----
2
+ MIGJAoGBAM3aoEw2uJPN+cZNw+SQ9ZH8SnqMBw4EaOYWPXmE/OyGOCT752tnvgBW
3
+ UT/cSwJnWZjXpPVQPPxzyLNWvwCENqLGFkGtGTHjHOKmMdn3wknBCFVBAgDzmPaM
4
+ f7+xgct62vhVtI1N9AFdw6ZaW1buXwAF49e3S4RRsTY1i49A067RAgMBAAE=
5
+ -----END RSA PUBLIC KEY-----
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'activerecord'
4
+ require 'fileutils'
5
+
6
+ ENV['RAILS_ENV'] = 'test'
7
+ require File.dirname(__FILE__) + '/../lib/acts_as_encryptable'
8
+
9
+ class Test::Unit::TestCase
10
+
11
+ def establish_connection(db_file = nil)
12
+ db_file = File.join('/tmp/acts_as_encryptable_tests.sqlite') unless db_file
13
+ ActiveRecord::Base.configurations = { 'ActiveRecord::Base' => { :adapter => 'sqlite3', :database => db_file, :timeout => 5000 } }
14
+ ActiveRecord::Base.establish_connection('ActiveRecord::Base')
15
+ ActiveRecord::Base.connection.execute('drop table if exists credit_cards')
16
+ ActiveRecord::Base.connection.execute('create table credit_cards (id integer, encrypted text)')
17
+ ActiveRecord::Base.connection.execute('drop table if exists people')
18
+ ActiveRecord::Base.connection.execute('create table people (id integer, important_data text)')
19
+ ActiveRecord::Base.connection
20
+ end
21
+
22
+ end
@@ -0,0 +1,69 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class BaseTest < Test::Unit::TestCase
4
+
5
+ class CreditCard < ActiveRecord::Base
6
+ attr_accessor :name_on_card, :number, :expiration
7
+ acts_as_encryptable :name_on_card, :number, :expiration
8
+ end
9
+
10
+ class Person < ActiveRecord::Base
11
+ attr_accessor :first_name, :last_name, :ssn
12
+ acts_as_encryptable :first_name, :last_name, :ssn, :column => 'important_data'
13
+ end
14
+
15
+ def setup
16
+ @connection = establish_connection
17
+ end
18
+
19
+ def test_a_credit_card
20
+ card = CreditCard.new(valid_credit_card)
21
+ assert card.save
22
+ end
23
+
24
+ def test_data_is_encrypted
25
+ test_a_credit_card
26
+ card = CreditCard.last
27
+ assert !card.name_on_card
28
+ assert !card.number
29
+ assert !card.expiration
30
+ end
31
+
32
+ def test_data_is_decrypted
33
+ test_a_credit_card
34
+ card = CreditCard.last
35
+ card.decrypt!
36
+ assert card.name_on_card == valid_credit_card[:name_on_card]
37
+ assert card.number == valid_credit_card[:number]
38
+ assert card.expiration == valid_credit_card[:expiration]
39
+ end
40
+
41
+ def test_set_encrypted_column_name
42
+ person = Person.new(valid_person)
43
+ assert person.save!
44
+ person = Person.last
45
+ person.decrypt!
46
+ assert person.first_name = valid_person[:first_name]
47
+ assert person.last_name = valid_person[:last_name]
48
+ assert person.ssn = valid_person[:ssn]
49
+ end
50
+
51
+ private
52
+
53
+ def valid_credit_card
54
+ {
55
+ :name_on_card => 'Test User',
56
+ :number => '1234567890123456',
57
+ :expiration => (Date.today + 1.year).strftime("%i/%y")
58
+ }
59
+ end
60
+
61
+ def valid_person
62
+ {
63
+ :first_name => 'Test',
64
+ :last_name => 'User',
65
+ :ssn => '111223333'
66
+ }
67
+ end
68
+
69
+ end
@@ -0,0 +1,24 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class CryptoTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @text = "I am a secret"
7
+ @public_key = File.join(File.dirname(__FILE__), '/../../sample_keys/rsa_key.pub')
8
+ @private_key = File.join(File.dirname(__FILE__), '/../../sample_keys/rsa_key')
9
+ end
10
+
11
+ def test_encryption
12
+ public_key = ActsAsEncryptable::Crypto::Key.from_file(@public_key)
13
+ encrypted = public_key.encrypt(@text)
14
+ assert encrypted != @text
15
+ end
16
+
17
+ def test_decryption
18
+ public_key = ActsAsEncryptable::Crypto::Key.from_file(@public_key)
19
+ private_key = ActsAsEncryptable::Crypto::Key.from_file(@private_key)
20
+ encrypted = public_key.encrypt(@text)
21
+ assert @text == private_key.decrypt(encrypted)
22
+ end
23
+
24
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ssoper-acts_as_encryptable
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Sean Soper
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: capistrano
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.5.0
24
+ version:
25
+ description: Allow your models to encrypt an arbitrary amount of data using asymmetric keys
26
+ email: sean.soper@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - MIT-LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - acts_as_encryptable.gemspec
36
+ - lib/acts_as_encryptable.rb
37
+ - lib/acts_as_encryptable/base.rb
38
+ - lib/acts_as_encryptable/crypto.rb
39
+ - generators/acts_as_encryptable_migration/acts_as_encryptable_migration_generator.rb
40
+ - sample_keys/rsa_key.pub
41
+ - sample_keys/rsa_key
42
+ - test/test_helper.rb
43
+ - test/unit/base_test.rb
44
+ - test/unit/crypto_test.rb
45
+ - MIT-LICENSE
46
+ - Rakefile
47
+ - README.rdoc
48
+ - Changelog
49
+ has_rdoc: true
50
+ homepage: http://github.com/ssoper/acts_as_encryptable
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --line-numbers
54
+ - --inline-source
55
+ - --title
56
+ - acts_as_encryptable
57
+ - --main
58
+ - README.rdoc
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ requirements: []
74
+
75
+ rubyforge_project:
76
+ rubygems_version: 1.2.0
77
+ signing_key:
78
+ specification_version: 2
79
+ summary: Encrypt and decrypt your data using asymmetric keys
80
+ test_files: []
81
+