pluginaweek-encrypted_attributes 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,48 @@
1
+ == master
2
+
3
+ == 0.4.0 / 2009-05-02
4
+
5
+ * Replace dynamic :salt option with :embed_salt option and utilizing the #encrypts block
6
+ * Allow a block to be used for dynamically defining encryption options
7
+ * Add :before / :after callbacks for hooking into the encryption process
8
+ * Allow the callback when encryption occurs to be customized [Sergio Salvatore]
9
+ * Define source attribute accessor if different than target and not defined [Sergio Salvatore]
10
+ * Update tests to work with Rails 2.3
11
+
12
+ == 0.3.0 / 2008-12-14
13
+
14
+ * Remove the PluginAWeek namespace
15
+
16
+ == 0.2.0 / 2008-12-4
17
+
18
+ * Update to be compatible with encrypted_strings 0.2.1
19
+
20
+ == 0.1.3 / 2008-10-26
21
+
22
+ * Change how the base module is included to prevent namespacing conflicts
23
+
24
+ == 0.1.2 / 2008-07-05
25
+
26
+ * Leave salt stringification up to PluginAWeek::EncryptedString::ShaEncryptor
27
+
28
+ == 0.1.1 / 2008-06-22
29
+
30
+ * Remove log files from gems
31
+
32
+ == 0.1.0 / 2008-05-05
33
+
34
+ * Don't extend encrypted_string's encryptors, instead creating subclasses
35
+ * Don't assume anything about attribute confirmations
36
+ * Support storing the salt for SHA encryption in the same string as the original value
37
+ * Update documentation
38
+
39
+ == 0.0.2 / 2007-09-26
40
+
41
+ * Move test fixtures out of the test application root directory
42
+ * Convert dos newlines to unix newlines
43
+
44
+ == 0.0.1 / 2007-08-05
45
+
46
+ * Official public release
47
+ * Add documentation
48
+ * Refactor unit test names
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006-2009 Aaron Pfeifer
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,110 @@
1
+ = encrypted_attributes
2
+
3
+ +encrypted_attributes+ adds support for automatically encrypting ActiveRecord
4
+ attributes.
5
+
6
+ == Resources
7
+
8
+ API
9
+
10
+ * http://api.pluginaweek.org/encrypted_attributes
11
+
12
+ Bugs
13
+
14
+ * http://pluginaweek.lighthouseapp.com/projects/13269-encrypted_attributes
15
+
16
+ Development
17
+
18
+ * http://github.com/pluginaweek/encrypted_attributes
19
+
20
+ Source
21
+
22
+ * git://github.com/pluginaweek/encrypted_attributes.git
23
+
24
+ == Description
25
+
26
+ Encrypting attributes can be repetitive especially when doing so throughout
27
+ various models and various projects. encrypted_attributes, in association
28
+ with the encrypted_strings library, helps make encrypting ActiveRecord
29
+ attributes easier by automating the process.
30
+
31
+ The options that +encrypts+ takes includes all of the encryption options for
32
+ the specific type of cipher being used from the encrypted_strings library.
33
+ Therefore, if setting the key for asymmetric encryption, this would be passed
34
+ into the +encrypts+ method. Examples of this are show in the Usage section.
35
+
36
+ == Usage
37
+
38
+ === Encryption Modes
39
+
40
+ SHA, symmetric, and asymmetric encryption modes are supported (default is SHA):
41
+
42
+ class User < ActiveRecord::Base
43
+ encrypts :password, :salt => 'secret'
44
+ # encrypts :password, :mode => :symmetric, :password => 'secret'
45
+ # encrypts :password, :mode => :asymmetric, :public_key_file => '/keys/public', :private_key_file => '/keys/private'
46
+ end
47
+
48
+ === Dynamic Configuration
49
+
50
+ The encryption configuration can be dynamically set like so:
51
+
52
+ class User < ActiveRecord::Base
53
+ encrypts :password, :mode => :sha do |user|
54
+ {:salt => "#{user.login}-#{Time.now}", :embed_salt => true}
55
+ end
56
+ end
57
+
58
+ In this case, the salt and password values are combined and stored in the
59
+ attribute being encrypted. Therefore, there's no need to add a second column
60
+ for storing the salt value.
61
+
62
+ To store the dynamic salt in a separate column:
63
+
64
+ class User < ActiveRecord::Base
65
+ encrypts :password, :mode => :sha, :before => :create_salt do |user|
66
+ {:salt => user.salt}
67
+ end
68
+
69
+ def create_salt
70
+ self.salt = "#{login}-#{Time.now}"
71
+ end
72
+ end
73
+
74
+ === Targeted Encryption
75
+
76
+ If you want to store the encrypted value in a different attribute than the
77
+ attribute being encrypted:
78
+
79
+ class User < ActiveRecord::Base
80
+ encrypts :password, :to => :crypted_password
81
+ end
82
+
83
+ === Conditional Encryption
84
+
85
+ Like ActiveRecord validations, +encrypts+ can take <tt>:if</tt> and <tt>:unless</tt>
86
+ parameters that determine whether the encryption should occur. For example,
87
+
88
+ class User < ActiveRecord::Base
89
+ encrypts :password, :if => lambda {Rails.env != 'development'}
90
+ end
91
+
92
+ === Additional information
93
+
94
+ For more examples of actual migrations and models that encrypt attributes,
95
+ see the actual API and unit tests. Also, see encrypted_strings for more
96
+ information about the various options that can be passed in.
97
+
98
+ == Testing
99
+
100
+ Before you can run any tests, the following gem must be installed:
101
+ * plugin_test_helper[http://github.com/pluginaweek/plugin_test_helper]
102
+
103
+ To run against a specific version of Rails:
104
+
105
+ rake test RAILS_FRAMEWORK_ROOT=/path/to/rails
106
+
107
+ == Dependencies
108
+
109
+ * Rails 2.1 or later
110
+ * encrypted_strings[http://github.com/pluginaweek/encrypted_strings]
data/Rakefile ADDED
@@ -0,0 +1,97 @@
1
+ require 'rake/testtask'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/contrib/sshpublisher'
5
+
6
+ spec = Gem::Specification.new do |s|
7
+ s.name = 'encrypted_attributes'
8
+ s.version = '0.4.0'
9
+ s.platform = Gem::Platform::RUBY
10
+ s.summary = 'Adds support for automatically encrypting ActiveRecord attributes'
11
+ s.description = s.summary
12
+
13
+ s.files = FileList['{lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
14
+ s.require_path = 'lib'
15
+ s.has_rdoc = true
16
+ s.test_files = Dir['test/**/*_test.rb']
17
+ s.add_dependency 'encrypted_strings', '>= 0.3.0'
18
+
19
+ s.author = 'Aaron Pfeifer'
20
+ s.email = 'aaron@pluginaweek.org'
21
+ s.homepage = 'http://www.pluginaweek.org'
22
+ s.rubyforge_project = 'pluginaweek'
23
+ end
24
+
25
+ desc 'Default: run all tests.'
26
+ task :default => :test
27
+
28
+ desc "Test the #{spec.name} plugin."
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.test_files = spec.test_files
32
+ t.verbose = true
33
+ end
34
+
35
+ begin
36
+ require 'rcov/rcovtask'
37
+ namespace :test do
38
+ desc "Test the #{spec.name} plugin with Rcov."
39
+ Rcov::RcovTask.new(:rcov) do |t|
40
+ t.libs << 'lib'
41
+ t.test_files = spec.test_files
42
+ t.rcov_opts << '--exclude="^(?!lib/)"'
43
+ t.verbose = true
44
+ end
45
+ end
46
+ rescue LoadError
47
+ end
48
+
49
+ desc "Generate documentation for the #{spec.name} plugin."
50
+ Rake::RDocTask.new(:rdoc) do |rdoc|
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = spec.name
53
+ rdoc.template = '../rdoc_template.rb'
54
+ rdoc.options << '--line-numbers'
55
+ rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb')
56
+ end
57
+
58
+ desc 'Generate a gemspec file.'
59
+ task :gemspec do
60
+ File.open("#{spec.name}.gemspec", 'w') do |f|
61
+ f.write spec.to_ruby
62
+ end
63
+ end
64
+
65
+ Rake::GemPackageTask.new(spec) do |p|
66
+ p.gem_spec = spec
67
+ p.need_tar = true
68
+ p.need_zip = true
69
+ end
70
+
71
+ desc 'Publish the beta gem.'
72
+ task :pgem => [:package] do
73
+ Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{spec.name}-#{spec.version}.gem").upload
74
+ end
75
+
76
+ desc 'Publish the API documentation.'
77
+ task :pdoc => [:rdoc] do
78
+ Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{spec.name}", 'rdoc').upload
79
+ end
80
+
81
+ desc 'Publish the API docs and gem'
82
+ task :publish => [:pgem, :pdoc, :release]
83
+
84
+ desc 'Publish the release files to RubyForge.'
85
+ task :release => [:gem, :package] do
86
+ require 'rubyforge'
87
+
88
+ ruby_forge = RubyForge.new.configure
89
+ ruby_forge.login
90
+
91
+ %w(gem tgz zip).each do |ext|
92
+ file = "pkg/#{spec.name}-#{spec.version}.#{ext}"
93
+ puts "Releasing #{File.basename(file)}..."
94
+
95
+ ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
96
+ end
97
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'encrypted_attributes'
@@ -0,0 +1,30 @@
1
+ module EncryptedAttributes
2
+ # Adds support for embedding salts in the encrypted value
3
+ class ShaCipher < EncryptedStrings::ShaCipher
4
+ # Encrypts a string using a Secure Hash Algorithm (SHA), specifically SHA-1.
5
+ #
6
+ # Configuration options:
7
+ # * <tt>:salt</tt> - Random bytes used as one of the inputs for generating
8
+ # the encrypted string
9
+ # * <tt>:embed_salt</tt> - Whether to embed the salt directly within the
10
+ # encrypted value. Default is false. This is useful for storing both
11
+ # the salt and the encrypted value in the same attribute.
12
+ def initialize(value, options = {}) #:nodoc:
13
+ if @embed_salt = options.delete(:embed_salt)
14
+ # The salt is at the end of the value
15
+ salt = value[40..-1]
16
+ options[:salt] = salt unless salt.blank?
17
+ end
18
+
19
+ super(options)
20
+ end
21
+
22
+ # Encrypts the data, embedding the salt at the end of the string if
23
+ # configured to do so
24
+ def encrypt(data)
25
+ encrypted_data = super
26
+ encrypted_data << salt if @embed_salt
27
+ encrypted_data
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,207 @@
1
+ require 'encrypted_strings'
2
+ require 'encrypted_attributes/sha_cipher'
3
+
4
+ module EncryptedAttributes
5
+ module MacroMethods
6
+ # Encrypts the given attribute.
7
+ #
8
+ # Configuration options:
9
+ # * <tt>:mode</tt> - The mode of encryption to use. Default is <tt>:sha</tt>.
10
+ # See EncryptedStrings for other possible modes.
11
+ # * <tt>:to</tt> - The attribute to write the encrypted value to. Default
12
+ # is the same attribute being encrypted.
13
+ # * <tt>:before</tt> - The callback to invoke every time *before* the
14
+ # attribute is encrypted
15
+ # * <tt>:after</tt> - The callback to invoke every time *after* the
16
+ # attribute is encrypted
17
+ # * <tt>:on</tt> - The ActiveRecord callback to use when triggering the
18
+ # encryption. By default, this will encrypt on <tt>before_validation</tt>.
19
+ # See ActiveRecord::Callbacks for a list of possible callbacks.
20
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
21
+ # if the encryption should occur. The method, proc or string should return
22
+ # or evaluate to a true or false value.
23
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
24
+ # determine if the encryption should not occur. The method, proc or string
25
+ # should return or evaluate to a true or false value.
26
+ #
27
+ # For additional configuration options used during the actual encryption,
28
+ # see the individual cipher class for the specified mode.
29
+ #
30
+ # == Encryption timeline
31
+ #
32
+ # By default, attributes are encrypted immediately before a record is
33
+ # validated. This means that you can still validate the presence of the
34
+ # encrypted attribute, but other things like password length cannot be
35
+ # validated without either (a) decrypting the value first or (b) using a
36
+ # different encryption target. For example,
37
+ #
38
+ # class User < ActiveRecord::Base
39
+ # encrypts :password, :to => :crypted_password
40
+ #
41
+ # validates_presence_of :password, :crypted_password
42
+ # validates_length_of :password, :maximum => 16
43
+ # end
44
+ #
45
+ # In the above example, the actual encrypted password will be stored in
46
+ # the +crypted_password+ attribute. This means that validations can
47
+ # still run against the model for the original password value.
48
+ #
49
+ # user = User.new(:password => 'secret')
50
+ # user.password # => "secret"
51
+ # user.crypted_password # => nil
52
+ # user.valid? # => true
53
+ # user.crypted_password # => "8152bc582f58c854f580cb101d3182813dec4afe"
54
+ #
55
+ # user = User.new(:password => 'longer_than_the_maximum_allowed')
56
+ # user.valid? # => false
57
+ # user.crypted_password # => "e80a709f25798f87d9ca8005a7f64a645964d7c2"
58
+ # user.errors[:password] # => "is too long (maximum is 16 characters)"
59
+ #
60
+ # == Encryption mode examples
61
+ #
62
+ # SHA encryption:
63
+ #
64
+ # class User < ActiveRecord::Base
65
+ # encrypts :password
66
+ # # encrypts :password, :salt => 'secret'
67
+ # end
68
+ #
69
+ # Symmetric encryption:
70
+ #
71
+ # class User < ActiveRecord::Base
72
+ # encrypts :password, :mode => :symmetric
73
+ # # encrypts :password, :mode => :symmetric, :key => 'custom'
74
+ # end
75
+ #
76
+ # Asymmetric encryption:
77
+ #
78
+ # class User < ActiveRecord::Base
79
+ # encrypts :password, :mode => :asymmetric
80
+ # # encrypts :password, :mode => :asymmetric, :public_key_file => '/keys/public', :private_key_file => '/keys/private'
81
+ # end
82
+ #
83
+ # == Dynamic configuration
84
+ #
85
+ # For better security, the encryption options (such as the salt value)
86
+ # can be based on values in each individual record. In order to
87
+ # dynamically configure the encryption options so that individual records
88
+ # can be referenced, an optional block can be specified.
89
+ #
90
+ # For example,
91
+ #
92
+ # class User < ActiveRecord::Base
93
+ # encrypts :password, :mode => :sha, :before => :create_salt do |user|
94
+ # {:salt => user.salt}
95
+ # end
96
+ #
97
+ # private
98
+ # def create_salt
99
+ # self.salt = "#{login}-#{Time.now}"
100
+ # end
101
+ # end
102
+ #
103
+ # In the above example, the SHA encryption's <tt>salt</tt> is configured
104
+ # dynamically based on the user's login and the time at which it was
105
+ # encrypted. This helps improve the security of the user's password.
106
+ def encrypts(attr_name, options = {}, &config)
107
+ config ||= options
108
+ attr_name = attr_name.to_s
109
+ to_attr_name = (options.delete(:to) || attr_name).to_s
110
+
111
+ # Figure out what cipher is being configured for the attribute
112
+ mode = options.delete(:mode) || :sha
113
+ class_name = "#{mode.to_s.classify}Cipher"
114
+ if EncryptedAttributes.const_defined?(class_name)
115
+ cipher_class = EncryptedAttributes.const_get(class_name)
116
+ else
117
+ cipher_class = EncryptedStrings.const_get(class_name)
118
+ end
119
+
120
+ # Define encryption hooks
121
+ define_callbacks("before_encrypt_#{attr_name}", "after_encrypt_#{attr_name}")
122
+ send("before_encrypt_#{attr_name}", options.delete(:before)) if options.include?(:before)
123
+ send("after_encrypt_#{attr_name}", options.delete(:after)) if options.include?(:after)
124
+
125
+ # Set the encrypted value on the configured callback
126
+ callback = options.delete(:on) || :before_validation
127
+ send(callback, :if => options.delete(:if), :unless => options.delete(:unless)) do |record|
128
+ record.send(:write_encrypted_attribute, attr_name, to_attr_name, cipher_class, config)
129
+ true
130
+ end
131
+
132
+ # Define virtual source attribute
133
+ if attr_name != to_attr_name && !column_names.include?(attr_name)
134
+ attr_reader attr_name unless method_defined?(attr_name)
135
+ attr_writer attr_name unless method_defined?("#{attr_name}=")
136
+ end
137
+
138
+ # Define the reader when reading the encrypted attribute from the database
139
+ define_method(to_attr_name) do
140
+ read_encrypted_attribute(to_attr_name, cipher_class, config)
141
+ end
142
+
143
+ unless included_modules.include?(EncryptedAttributes::InstanceMethods)
144
+ include EncryptedAttributes::InstanceMethods
145
+ end
146
+ end
147
+ end
148
+
149
+ module InstanceMethods #:nodoc:
150
+ private
151
+ # Encrypts the given attribute to a target location using the encryption
152
+ # options configured for that attribute
153
+ def write_encrypted_attribute(attr_name, to_attr_name, cipher_class, options)
154
+ value = send(attr_name)
155
+
156
+ # Only encrypt values that actually have content and have not already
157
+ # been encrypted
158
+ unless value.blank? || value.encrypted?
159
+ callback("before_encrypt_#{attr_name}")
160
+
161
+ # Create the cipher configured for this attribute
162
+ cipher = create_cipher(cipher_class, options, value)
163
+
164
+ # Encrypt the value
165
+ value = cipher.encrypt(value)
166
+ value.cipher = cipher
167
+
168
+ # Update the value based on the target attribute
169
+ send("#{to_attr_name}=", value)
170
+
171
+ callback("after_encrypt_#{attr_name}")
172
+ end
173
+ end
174
+
175
+ # Reads the given attribute from the database, adding contextual
176
+ # information about how it was encrypted so that equality comparisons
177
+ # can be used
178
+ def read_encrypted_attribute(to_attr_name, cipher_class, options)
179
+ value = read_attribute(to_attr_name)
180
+
181
+ # Make sure we set the cipher for equality comparison when reading
182
+ # from the database. This should only be done if the value is *not*
183
+ # blank, is *not* encrypted, and hasn't changed since it was read from
184
+ # the database. The dirty checking is important when the encypted value
185
+ # is written to the same attribute as the unencrypted value (i.e. you
186
+ # don't want to encrypt when a new value has been set)
187
+ unless value.blank? || value.encrypted? || attribute_changed?(to_attr_name)
188
+ # Create the cipher configured for this attribute
189
+ value.cipher = create_cipher(cipher_class, options, value)
190
+ end
191
+
192
+ value
193
+ end
194
+
195
+ # Creates a new cipher with the given configuration options
196
+ def create_cipher(klass, options, value)
197
+ options = options.is_a?(Proc) ? options.call(self) : options.dup
198
+
199
+ # Only use the contextual information for this plugin's ciphers
200
+ klass.parent == EncryptedAttributes ? klass.new(value, options) : klass.new(options)
201
+ end
202
+ end
203
+ end
204
+
205
+ ActiveRecord::Base.class_eval do
206
+ extend EncryptedAttributes::MacroMethods
207
+ end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ validates_presence_of :login
3
+ end
@@ -0,0 +1,9 @@
1
+ require 'config/boot'
2
+
3
+ Rails::Initializer.run do |config|
4
+ config.plugin_paths << '..'
5
+ config.plugins = %w(encrypted_strings encrypted_attributes)
6
+ config.cache_classes = false
7
+ config.whiny_nils = true
8
+ config.action_controller.session = {:key => 'rails_session', :secret => 'd229e4d22437432705ab3985d4d246'}
9
+ end
@@ -0,0 +1,11 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.string :login, :password, :crypted_password, :salt
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_table :users
10
+ end
11
+ end
data/test/factory.rb ADDED
@@ -0,0 +1,41 @@
1
+ module Factory
2
+ # Build actions for the model
3
+ def self.build(model, &block)
4
+ name = model.to_s.underscore
5
+
6
+ define_method("#{name}_attributes", block)
7
+ define_method("valid_#{name}_attributes") {|*args| valid_attributes_for(model, *args)}
8
+ define_method("new_#{name}") {|*args| new_record(model, *args)}
9
+ define_method("create_#{name}") {|*args| create_record(model, *args)}
10
+ end
11
+
12
+ # Get valid attributes for the model
13
+ def valid_attributes_for(model, attributes = {})
14
+ name = model.to_s.underscore
15
+ send("#{name}_attributes", attributes)
16
+ attributes.stringify_keys!
17
+ attributes
18
+ end
19
+
20
+ # Build an unsaved record
21
+ def new_record(model, *args)
22
+ attributes = valid_attributes_for(model, *args)
23
+ record = model.new(attributes)
24
+ attributes.each {|attr, value| record.send("#{attr}=", value) if model.accessible_attributes && !model.accessible_attributes.include?(attr) || model.protected_attributes && model.protected_attributes.include?(attr)}
25
+ record
26
+ end
27
+
28
+ # Build and save/reload a record
29
+ def create_record(model, *args)
30
+ record = new_record(model, *args)
31
+ record.save!
32
+ record.reload
33
+ record
34
+ end
35
+
36
+ build User do |attributes|
37
+ attributes.reverse_merge!(
38
+ :login => 'admin'
39
+ )
40
+ end
41
+ end
data/test/keys/private ADDED
@@ -0,0 +1,9 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIBOwIBAAJBAL/xeY6aqFx6z1ThNOwgPgxv3tsonTlCj8VkN3Ikumg6SzBuLxlV
3
+ i9gFQZ7K9Pv9o/7+xUTYODqBpVhwgLBeu2cCAwEAAQJAHyjFMfg7Yp/xLndMzxRA
4
+ 3mX+yJckRtpeWo31TktWE3syks1r9OrfmxKiStM9kFRubeBHTihZrW92TYkROLxh
5
+ uQIhAPuftVTJZFDNxeYDKIMIMqwR8KZgtuf25cv4pTxYwPqLAiEAw0gNwDJHBkvo
6
+ da4402pZNQmBA6qCSf0svDXqoEoaShUCIGBma340Oe6LJ0pb42Vv+pnZtazIWMq9
7
+ 2IQwmn1oM2bJAiEAhgP869mVRIzzi091UCG79tn+4DU0FPLasI+P5VD1mcECIQDb
8
+ 3ndvbPcElVvdJgabxyWJJsNtBBNZYPsuc6NrQyShOw==
9
+ -----END RSA PRIVATE KEY-----
data/test/keys/public ADDED
@@ -0,0 +1,4 @@
1
+ -----BEGIN RSA PUBLIC KEY-----
2
+ MEgCQQC/8XmOmqhces9U4TTsID4Mb97bKJ05Qo/FZDdyJLpoOkswbi8ZVYvYBUGe
3
+ yvT7/aP+/sVE2Dg6gaVYcICwXrtnAgMBAAE=
4
+ -----END RSA PUBLIC KEY-----
@@ -0,0 +1,13 @@
1
+ # Load the plugin testing framework
2
+ $:.unshift("#{File.dirname(__FILE__)}/../../plugin_test_helper/lib")
3
+ require 'rubygems'
4
+ require 'plugin_test_helper'
5
+
6
+ # Run the migrations
7
+ ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate")
8
+
9
+ # Mixin the factory helper
10
+ require File.expand_path("#{File.dirname(__FILE__)}/factory")
11
+ Test::Unit::TestCase.class_eval do
12
+ include Factory
13
+ end
@@ -0,0 +1,446 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class EncryptedAttributesTest < ActiveSupport::TestCase
4
+ def setup
5
+ User.encrypts :password
6
+ end
7
+
8
+ def test_should_use_sha_by_default
9
+ user = create_user(:login => 'admin', :password => 'secret')
10
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
11
+ end
12
+
13
+ def test_should_encrypt_on_invalid_model
14
+ user = new_user(:login => nil, :password => 'secret')
15
+ assert !user.valid?
16
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
17
+ end
18
+
19
+ def test_should_not_encrypt_if_attribute_is_nil
20
+ user = create_user(:login => 'admin', :password => nil)
21
+ assert_nil user.password
22
+ end
23
+
24
+ def test_should_not_encrypt_if_attribute_is_blank
25
+ user = create_user(:login => 'admin', :password => '')
26
+ assert_equal '', user.password
27
+ end
28
+
29
+ def test_should_not_encrypt_if_already_encrypted
30
+ user = create_user(:login => 'admin', :password => 'secret'.encrypt)
31
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
32
+ end
33
+
34
+ def test_should_return_encrypted_attribute_for_saved_record
35
+ user = create_user(:login => 'admin', :password => 'secret')
36
+ user = User.find(user.id)
37
+ assert user.password.encrypted?
38
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
39
+ end
40
+
41
+ def test_should_not_encrypt_attribute_if_updating_without_any_changes
42
+ user = create_user(:login => 'admin', :password => 'secret')
43
+ user.login = 'Administrator'
44
+ user.save!
45
+ assert user.password.encrypted?
46
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
47
+ end
48
+
49
+ def test_should_encrypt_attribute_if_updating_with_changes
50
+ user = create_user(:login => 'admin', :password => 'secret')
51
+ user.password = 'shhh'
52
+ user.save!
53
+ assert user.password.encrypted?
54
+ assert_equal '162cf5debf84cbc2af13da848544c3e2c515b4d3', "#{user.password}"
55
+ end
56
+
57
+ def teardown
58
+ User.class_eval do
59
+ @before_validation_callbacks = nil
60
+ end
61
+ end
62
+ end
63
+
64
+ class EncryptedAttributesWithDifferentTargetTest < ActiveSupport::TestCase
65
+ def setup
66
+ User.encrypts :password, :to => :crypted_password
67
+ end
68
+
69
+ def test_should_not_encrypt_if_attribute_is_nil
70
+ user = create_user(:login => 'admin', :password => nil)
71
+ assert_nil user.password
72
+ assert_nil user.crypted_password
73
+ end
74
+
75
+ def test_should_not_encrypt_if_attribute_is_blank
76
+ user = create_user(:login => 'admin', :password => '')
77
+ assert_equal '', user.password
78
+ assert_nil user.crypted_password
79
+ end
80
+
81
+ def test_should_not_encrypt_if_already_encrypted
82
+ user = create_user(:login => 'admin', :crypted_password => 'secret'.encrypt)
83
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.crypted_password}"
84
+ end
85
+
86
+ def test_should_return_encrypted_attribute_for_saved_record
87
+ user = create_user(:login => 'admin', :password => 'secret')
88
+ user = User.find(user.id)
89
+ assert user.crypted_password.encrypted?
90
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.crypted_password}"
91
+ end
92
+
93
+ def test_should_not_encrypt_attribute_if_updating_without_any_changes
94
+ user = create_user(:login => 'admin', :password => 'secret')
95
+ user.login = 'Administrator'
96
+ user.save!
97
+ assert user.crypted_password.encrypted?
98
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.crypted_password}"
99
+ end
100
+
101
+ def test_should_encrypt_attribute_if_updating_with_changes
102
+ user = create_user(:login => 'admin', :password => 'secret')
103
+ user.password = 'shhh'
104
+ user.save!
105
+ assert user.crypted_password.encrypted?
106
+ assert_equal '162cf5debf84cbc2af13da848544c3e2c515b4d3', "#{user.crypted_password}"
107
+ end
108
+
109
+ def teardown
110
+ User.class_eval do
111
+ @before_validation_callbacks = nil
112
+ end
113
+ end
114
+ end
115
+
116
+ class EncryptedAttributesWithVirtualAttributeSourceTest < ActiveSupport::TestCase
117
+ def setup
118
+ User.encrypts :raw_password, :to => :crypted_password
119
+ end
120
+
121
+ def test_should_define_source_reader
122
+ assert User.method_defined?(:raw_password)
123
+ end
124
+
125
+ def test_should_define_source_writer
126
+ assert User.method_defined?(:raw_password=)
127
+ end
128
+
129
+ def test_should_encrypt_from_virtual_attribute
130
+ user = create_user(:login => 'admin', :raw_password => 'secret')
131
+ assert user.crypted_password.encrypted?
132
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.crypted_password}"
133
+ end
134
+
135
+ def teardown
136
+ User.class_eval do
137
+ @before_validation_callbacks = nil
138
+
139
+ remove_method(:raw_password)
140
+ remove_method(:raw_password=)
141
+ end
142
+ end
143
+ end
144
+
145
+ class EncryptedAttributesWithConflictingVirtualAttributeSourceTest < ActiveSupport::TestCase
146
+ def setup
147
+ User.class_eval do
148
+ def raw_password
149
+ 'raw_password'
150
+ end
151
+
152
+ def raw_password=(value)
153
+ self.password = value
154
+ end
155
+ end
156
+
157
+ User.encrypts :raw_password, :to => :crypted_password
158
+ @user = User.new
159
+ end
160
+
161
+ def test_should_not_define_source_reader
162
+ assert_equal 'raw_password', @user.raw_password
163
+ end
164
+
165
+ def test_should_not_define_source_writer
166
+ @user.raw_password = 'raw_password'
167
+ assert_equal 'raw_password', @user.password
168
+ end
169
+
170
+ def teardown
171
+ User.class_eval do
172
+ @before_validation_callbacks = nil
173
+
174
+ remove_method(:raw_password)
175
+ remove_method(:raw_password=)
176
+ end
177
+ end
178
+ end
179
+
180
+ class EncryptedAttributesWithCustomCallbackTest < ActiveSupport::TestCase
181
+ def setup
182
+ User.encrypts :password, :on => :before_save
183
+ end
184
+
185
+ def test_should_not_encrypt_on_validation
186
+ user = new_user(:login => 'admin', :password => 'secret')
187
+ user.valid?
188
+ assert_equal 'secret', user.password
189
+ end
190
+
191
+ def test_should_encrypt_on_create
192
+ user = new_user(:login => 'admin', :password => 'secret')
193
+ user.save
194
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
195
+ end
196
+
197
+ def teardown
198
+ User.class_eval do
199
+ @before_save_callbacks = nil
200
+ end
201
+ end
202
+ end
203
+
204
+ class EncryptedAttributesWithConditionalsTest < ActiveSupport::TestCase
205
+ def test_should_not_encrypt_if_if_conditional_is_false
206
+ User.encrypts :password, :if => lambda {|user| false}
207
+ user = create_user(:login => 'admin', :password => 'secret')
208
+ assert_equal 'secret', user.password
209
+ end
210
+
211
+ def test_should_encrypt_if_if_conditional_is_true
212
+ User.encrypts :password, :if => lambda {|user| true}
213
+ user = create_user(:login => 'admin', :password => 'secret')
214
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
215
+ end
216
+
217
+ def test_should_not_encrypt_if_unless_conditional_is_true
218
+ User.encrypts :password, :unless => lambda {|user| true}
219
+ user = create_user(:login => 'admin', :password => 'secret')
220
+ assert_equal 'secret', user.password
221
+ end
222
+
223
+ def test_should_encrypt_if_unless_conditional_is_false
224
+ User.encrypts :password, :unless => lambda {|user| false}
225
+ user = create_user(:login => 'admin', :password => 'secret')
226
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
227
+ end
228
+
229
+ def teardown
230
+ User.class_eval do
231
+ @before_validation_callbacks = nil
232
+ end
233
+ end
234
+ end
235
+
236
+ class EncryptedAttributesWithBeforeCallbacksTest < ActiveSupport::TestCase
237
+ def setup
238
+ @password = nil
239
+ @ran_callback = false
240
+ User.encrypts :password, :before => lambda {|user| @ran_callback = true; @password = user.password.to_s}
241
+
242
+ create_user(:login => 'admin', :password => 'secret')
243
+ end
244
+
245
+ def test_should_run_callback
246
+ assert @ran_callback
247
+ end
248
+
249
+ def test_should_not_have_encrypted_yet
250
+ assert_equal 'secret', @password
251
+ end
252
+
253
+ def teardown
254
+ User.class_eval do
255
+ @before_validation_callbacks = nil
256
+ @before_encrypt_password_callbacks = nil
257
+ end
258
+ end
259
+ end
260
+
261
+ class EncryptedAttributesWithAfterCallbacksTest < ActiveSupport::TestCase
262
+ def setup
263
+ @password = nil
264
+ @ran_callback = false
265
+ User.encrypts :password, :after => lambda {|user| @ran_callback = true; @password = user.password.to_s}
266
+
267
+ create_user(:login => 'admin', :password => 'secret')
268
+ end
269
+
270
+ def test_should_run_callback
271
+ assert @ran_callback
272
+ end
273
+
274
+ def test_should_have_encrypted_already
275
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', @password
276
+ end
277
+
278
+ def teardown
279
+ User.class_eval do
280
+ @before_validation_callbacks = nil
281
+ @after_encrypt_password_callbacks = nil
282
+ end
283
+ end
284
+ end
285
+
286
+ class EncryptedAttributesWithDynamicConfigurationTest < ActiveSupport::TestCase
287
+ def setup
288
+ @salt = nil
289
+ User.encrypts :password, :before => lambda {|user| user.salt = user.login} do |user|
290
+ {:salt => @salt = user.salt}
291
+ end
292
+
293
+ @user = create_user(:login => 'admin', :password => 'secret')
294
+ end
295
+
296
+ def test_should_use_dynamic_configuration_during_write
297
+ assert_equal 'a55d037f385cad22efe7862e07b805938d150154', "#{@user[:password]}"
298
+ end
299
+
300
+ def test_should_use_dynamic_configuration_during_read
301
+ user = User.find(@user.id)
302
+ assert_equal 'a55d037f385cad22efe7862e07b805938d150154', "#{user.password}"
303
+ end
304
+
305
+ def test_should_build_configuration_after_before_callbacks_invoked
306
+ assert_equal 'admin', @salt
307
+ end
308
+
309
+ def teardown
310
+ User.class_eval do
311
+ @before_validation_callbacks = nil
312
+ @before_encrypt_password_callbacks = nil
313
+ end
314
+ end
315
+ end
316
+
317
+ class ShaEncryptionTest < ActiveSupport::TestCase
318
+ def setup
319
+ User.encrypts :password, :mode => :sha
320
+ @user = create_user(:login => 'admin', :password => 'secret')
321
+ end
322
+
323
+ def test_should_encrypt_password
324
+ assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{@user.password}"
325
+ end
326
+
327
+ def test_should_be_encrypted
328
+ assert @user.password.encrypted?
329
+ end
330
+
331
+ def test_should_use_sha_cipher
332
+ assert_instance_of EncryptedAttributes::ShaCipher, @user.password.cipher
333
+ end
334
+
335
+ def test_should_use_default_salt
336
+ assert_equal 'salt', @user.password.cipher.salt
337
+ end
338
+
339
+ def test_should_be_able_to_check_password
340
+ assert_equal 'secret', @user. password
341
+ end
342
+
343
+ def teardown
344
+ User.class_eval do
345
+ @before_validation_callbacks = nil
346
+ end
347
+ end
348
+ end
349
+
350
+ class ShaWithEmbeddedSaltEncryptionTest < ActiveSupport::TestCase
351
+ def setup
352
+ User.encrypts :password, :mode => :sha, :salt => 'admin', :embed_salt => true
353
+ @user = create_user(:login => 'admin', :password => 'secret')
354
+ end
355
+
356
+ def test_should_encrypt_password
357
+ assert_equal 'a55d037f385cad22efe7862e07b805938d150154admin', "#{@user.password}"
358
+ end
359
+
360
+ def test_should_be_encrypted
361
+ assert @user.password.encrypted?
362
+ end
363
+
364
+ def test_should_use_sha_cipher
365
+ assert_instance_of EncryptedAttributes::ShaCipher, @user.password.cipher
366
+ end
367
+
368
+ def test_should_use_custom_salt
369
+ assert_equal 'admin', @user.password.cipher.salt
370
+ end
371
+
372
+ def test_should_be_able_to_check_password
373
+ assert_equal 'secret', @user.password
374
+ end
375
+
376
+ def teardown
377
+ User.class_eval do
378
+ @before_validation_callbacks = nil
379
+ end
380
+ end
381
+ end
382
+
383
+ class SymmetricEncryptionTest < ActiveSupport::TestCase
384
+ def setup
385
+ User.encrypts :password, :mode => :symmetric, :password => 'key'
386
+ @user = create_user(:login => 'admin', :password => 'secret')
387
+ end
388
+
389
+ def test_should_encrypt_password
390
+ assert_equal "zfKtnSa33tc=\n", @user.password
391
+ end
392
+
393
+ def test_should_be_encrypted
394
+ assert @user.password.encrypted?
395
+ end
396
+
397
+ def test_should_use_sha_cipher
398
+ assert_instance_of EncryptedStrings::SymmetricCipher, @user.password.cipher
399
+ end
400
+
401
+ def test_should_use_custom_password
402
+ assert_equal 'key', @user.password.cipher.password
403
+ end
404
+
405
+ def test_should_be_able_to_check_password
406
+ assert_equal 'secret', @user.password
407
+ end
408
+
409
+ def teardown
410
+ User.class_eval do
411
+ @before_validation_callbacks = nil
412
+ end
413
+ end
414
+ end
415
+
416
+ class AsymmetricEncryptionTest < ActiveSupport::TestCase
417
+ def setup
418
+ User.encrypts :password, :mode => :asymmetric,
419
+ :private_key_file => File.dirname(__FILE__) + '/../keys/private',
420
+ :public_key_file => File.dirname(__FILE__) + '/../keys/public'
421
+ @user = create_user(:login => 'admin', :password => 'secret')
422
+ end
423
+
424
+ def test_should_encrypt_password
425
+ assert_not_equal 'secret', "#{@user.password}"
426
+ assert_equal 90, @user.password.length
427
+ end
428
+
429
+ def test_should_be_encrypted
430
+ assert @user.password.encrypted?
431
+ end
432
+
433
+ def test_should_use_sha_cipher
434
+ assert_instance_of EncryptedStrings::AsymmetricCipher, @user.password.cipher
435
+ end
436
+
437
+ def test_should_be_able_to_check_password
438
+ assert_equal 'secret', @user.password
439
+ end
440
+
441
+ def teardown
442
+ User.class_eval do
443
+ @before_validation_callbacks = nil
444
+ end
445
+ end
446
+ end
@@ -0,0 +1,43 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class ShaCipherWithoutEmbeddingTest < Test::Unit::TestCase
4
+ def setup
5
+ @cipher = EncryptedAttributes::ShaCipher.new('dc0fc7c07bba982a8d8f18fe138dbea912df5e0e', :salt => 'custom_salt')
6
+ end
7
+
8
+ def test_should_use_configured_salt
9
+ assert_equal 'custom_salt', @cipher.salt
10
+ end
11
+
12
+ def test_should_not_embed_salt_in_encrypted_string
13
+ assert_equal 'dc0fc7c07bba982a8d8f18fe138dbea912df5e0e', @cipher.encrypt('secret')
14
+ end
15
+ end
16
+
17
+ class ShaCipherWithNoSaltEmbeddedTest < Test::Unit::TestCase
18
+ def setup
19
+ @cipher = EncryptedAttributes::ShaCipher.new('dc0fc7c07bba982a8d8f18fe138dbea912df5e0e', :embed_salt => true, :salt => 'custom_salt')
20
+ end
21
+
22
+ def test_should_use_configured_salt
23
+ assert_equal 'custom_salt', @cipher.salt
24
+ end
25
+
26
+ def test_should_embed_salt_in_encrypted_string
27
+ assert_equal 'dc0fc7c07bba982a8d8f18fe138dbea912df5e0ecustom_salt', @cipher.encrypt('secret')
28
+ end
29
+ end
30
+
31
+ class ShaCipherWithSaltEmbeddedTest < Test::Unit::TestCase
32
+ def setup
33
+ @cipher = EncryptedAttributes::ShaCipher.new('dc0fc7c07bba982a8d8f18fe138dbea912df5e0ecustom_salt', :embed_salt => true, :salt => 'ignored_salt')
34
+ end
35
+
36
+ def test_should_use_remaining_characters_after_password_for_salt
37
+ assert_equal 'custom_salt', @cipher.salt
38
+ end
39
+
40
+ def test_should_embed_salt_in_encrypted_string
41
+ assert_equal 'dc0fc7c07bba982a8d8f18fe138dbea912df5e0ecustom_salt', @cipher.encrypt('secret')
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pluginaweek-encrypted_attributes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Pfeifer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-08 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: encrypted_strings
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.3.0
24
+ version:
25
+ description: Adds support for automatically encrypting ActiveRecord attributes
26
+ email: aaron@pluginaweek.org
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - lib/encrypted_attributes
35
+ - lib/encrypted_attributes/sha_cipher.rb
36
+ - lib/encrypted_attributes.rb
37
+ - test/unit
38
+ - test/unit/encrypted_attributes_test.rb
39
+ - test/unit/sha_cipher_test.rb
40
+ - test/keys
41
+ - test/keys/private
42
+ - test/keys/public
43
+ - test/factory.rb
44
+ - test/app_root
45
+ - test/app_root/app
46
+ - test/app_root/app/models
47
+ - test/app_root/app/models/user.rb
48
+ - test/app_root/db
49
+ - test/app_root/db/migrate
50
+ - test/app_root/db/migrate/001_create_users.rb
51
+ - test/app_root/config
52
+ - test/app_root/config/environment.rb
53
+ - test/test_helper.rb
54
+ - CHANGELOG.rdoc
55
+ - init.rb
56
+ - LICENSE
57
+ - Rakefile
58
+ - README.rdoc
59
+ has_rdoc: true
60
+ homepage: http://www.pluginaweek.org
61
+ post_install_message:
62
+ rdoc_options: []
63
+
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ requirements: []
79
+
80
+ rubyforge_project: pluginaweek
81
+ rubygems_version: 1.2.0
82
+ signing_key:
83
+ specification_version: 2
84
+ summary: Adds support for automatically encrypting ActiveRecord attributes
85
+ test_files:
86
+ - test/unit/encrypted_attributes_test.rb
87
+ - test/unit/sha_cipher_test.rb