attr_encryption 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: acd5e4eab70392658a813fc269594f9172725646
4
+ data.tar.gz: b517d95ae98b5cd7537ff0cf40ed52275c632b85
5
+ SHA512:
6
+ metadata.gz: caf52c3ef1935397886f25c58290c78a3344632c8373bd223a9cbd5d1398a87ed1be5351fbd4fb33142f892ff7c86761a50fc57b935e9f1c61a0026ee0a2373c
7
+ data.tar.gz: 87e5a93df2785e93b0d83af5bee82808cff9753f755ff8e27fbb3fe3083751070752c9adafa5f6f8d7a1f27d0a4220493244378cb797440e026aa811b966c36b
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ gem "activesupport", ">= 3.2.14"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec"
10
+ gem "rdoc", "~> 3.12"
11
+ gem "bundler", "~> 1.0"
12
+ gem "jeweler", "~> 1.8.7"
13
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,68 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.2.14)
5
+ i18n (~> 0.6, >= 0.6.4)
6
+ multi_json (~> 1.0)
7
+ addressable (2.3.5)
8
+ builder (3.2.2)
9
+ diff-lcs (1.2.4)
10
+ faraday (0.8.8)
11
+ multipart-post (~> 1.2.0)
12
+ git (1.2.6)
13
+ github_api (0.10.1)
14
+ addressable
15
+ faraday (~> 0.8.1)
16
+ hashie (>= 1.2)
17
+ multi_json (~> 1.4)
18
+ nokogiri (~> 1.5.2)
19
+ oauth2
20
+ hashie (2.0.5)
21
+ highline (1.6.19)
22
+ httpauth (0.2.0)
23
+ i18n (0.6.5)
24
+ jeweler (1.8.7)
25
+ builder
26
+ bundler (~> 1.0)
27
+ git (>= 1.2.5)
28
+ github_api (= 0.10.1)
29
+ highline (>= 1.6.15)
30
+ nokogiri (= 1.5.10)
31
+ rake
32
+ rdoc
33
+ json (1.8.0)
34
+ jwt (0.1.8)
35
+ multi_json (>= 1.5)
36
+ multi_json (1.8.0)
37
+ multi_xml (0.5.5)
38
+ multipart-post (1.2.0)
39
+ nokogiri (1.5.10)
40
+ oauth2 (0.9.2)
41
+ faraday (~> 0.8)
42
+ httpauth (~> 0.2)
43
+ jwt (~> 0.1.4)
44
+ multi_json (~> 1.0)
45
+ multi_xml (~> 0.5)
46
+ rack (~> 1.2)
47
+ rack (1.5.2)
48
+ rake (10.1.0)
49
+ rdoc (3.12.2)
50
+ json (~> 1.4)
51
+ rspec (2.14.1)
52
+ rspec-core (~> 2.14.0)
53
+ rspec-expectations (~> 2.14.0)
54
+ rspec-mocks (~> 2.14.0)
55
+ rspec-core (2.14.5)
56
+ rspec-expectations (2.14.3)
57
+ diff-lcs (>= 1.1.3, < 2.0)
58
+ rspec-mocks (2.14.3)
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ activesupport (>= 3.2.14)
65
+ bundler (~> 1.0)
66
+ jeweler (~> 1.8.7)
67
+ rdoc (~> 3.12)
68
+ rspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Dave
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,34 @@
1
+ = attr_encryption
2
+
3
+ This gem enhances ActiveRecord models with the ability to quickly and easily
4
+ define which attributes of the model are to be encrypted. Once the attributes
5
+ have been identified, the gem takes care of all the details.
6
+
7
+ For example, if you want to encrypt the card number of a CreditCard model, you
8
+ would simply code the following:
9
+
10
+ class CreditCard < ActiveRecord::Base
11
+ attr_encrypted :card_number
12
+ end
13
+
14
+ All that is required at this point is a column on the 'credit_cards' table named
15
+ 'card_number_enc'.
16
+
17
+ There are a variety of options that can be applied to the attr_encrypted call to customize
18
+ how the encryption proceeds, but more on that later.
19
+
20
+ == Contributing to attr_encryption
21
+
22
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
23
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
24
+ * Fork the project.
25
+ * Start a feature/bugfix branch.
26
+ * Commit and push until you are happy with your contribution.
27
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
28
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
29
+
30
+ == Copyright
31
+
32
+ Copyright (c) 2013 Providigm. See LICENSE.txt for
33
+ further details.
34
+
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "attr_encryption"
18
+ gem.homepage = "http://github.com/GitHubAdmin/attr_encryption"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Extends Object and ActiveRecord::Base objects to support encrypted attributes}
21
+ gem.description = %Q{Provides an extension for Ruby and rails object to support a flexible means of encrypting attributes.}
22
+ gem.email = "dave.sieh@providigm.com"
23
+ gem.authors = ["Dave Sieh"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'rdoc/task'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "attr_encryption #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,345 @@
1
+ require 'singleton'
2
+ require 'attr_encryption/date_extensions'
3
+ require 'attr_encryption/mysql_encryption'
4
+ require 'attr_encryption/mysql_encryptor'
5
+
6
+ # Adds attr_accessors that encrypt and decrypt an object's attributes
7
+ module AttrEncryption
8
+
9
+ def self.extended(base) # :nodoc:
10
+ base.class_eval do
11
+ include InstanceMethods
12
+ attr_writer :attr_encrypted_options
13
+ @attr_encrypted_options, @encrypted_attributes = {}, {}
14
+ end
15
+ end
16
+
17
+ # Generates attr_accessors that encrypt and decrypt attributes transparently
18
+ #
19
+ # Options (any other options you specify are passed to the encryptor's encrypt and decrypt methods)
20
+ #
21
+ # :attribute => The name of the referenced encrypted attribute. For example
22
+ # <tt>attr_accessor :email, :attribute => :ee</tt> would generate an
23
+ # attribute named 'ee' to store the encrypted email. This is useful when defining
24
+ # one attribute to encrypt at a time or when the :prefix and :suffix options
25
+ # aren't enough. Defaults to nil.
26
+ #
27
+ # :type => The data type of the value to be encrypted/decrypted. Can be 'date', 'datetime', 'binary' or 'text'.
28
+ # When encrypting, all values will use their string value (value.to_s). When decrypting,
29
+ # the type of the value will determine what is returned. For example:
30
+ #
31
+ # type = 'date': Date.parse(decrypted_value)
32
+ # type = 'time': DateTime.parse(decrypted_value)
33
+ # type = 'binary': decrypted_value
34
+ # type = 'text': decrypted_value.force_encoding('utf-8')
35
+ #
36
+ # :prefix => A prefix used to generate the name of the referenced encrypted attributes.
37
+ # For example <tt>attr_accessor :email, :password, :prefix => 'crypted_'</tt> would
38
+ # generate attributes named 'crypted_email' and 'crypted_password' to store the
39
+ # encrypted email and password. Defaults to ''.
40
+ #
41
+ # :suffix => A suffix used to generate the name of the referenced encrypted attributes.
42
+ # For example <tt>attr_accessor :email, :password, :suffix => '_encrypted'</tt>
43
+ # would generate attributes named 'email_encrypted' and 'password_encrypted' to store the
44
+ # encrypted email. Defaults to '_enc'.
45
+ #
46
+ # :preencrypt => The symbol identifying a method that should be run on an attribute immediately prior
47
+ # to marshalling and encrypting. This could be used for things like stripping white-space
48
+ # from values or other sorts of pre-processing. Defaults to nil.
49
+ #
50
+ # :key => The encryption key. This option may not be required if you're using a custom encryptor. If you pass
51
+ # a symbol representing an instance method then the :key option will be replaced with the result of the
52
+ # method before being passed to the encryptor. Objects that respond to :call are evaluated as well (including procs).
53
+ # Any other key types will be passed directly to the encryptor. TODO (DJS): We'll see if we need this.
54
+ #
55
+ # :encode => If set to true, attributes will be encoded as well as encrypted. This is useful if you're
56
+ # planning on storing the encrypted attributes in a database. The default encoding is 'm' (base64),
57
+ # however this can be overwritten by setting the :encode option to some other encoding string instead of
58
+ # just 'true'. See http://www.ruby-doc.org/core/classes/Array.html#M002245 for more encoding directives.
59
+ # Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel. TODO(DJS): We'll see if we need this.
60
+ #
61
+ # :default_encoding => Defaults to 'm' (base64). TODO(DJS): Hmmm. See above
62
+ #
63
+ # :marshal => If set to true, attributes will be marshaled as well as encrypted. This is useful if you're planning
64
+ # on encrypting something other than a string. Defaults to false unless you're using it with ActiveRecord
65
+ # or DataMapper. TODO(DJS): Don't want to use this by default in our encryption since we want to be able to query...
66
+ #
67
+ # :marshaler => The object to use for marshaling. Defaults to Marshal.
68
+ #
69
+ # :dump_method => The dump method name to call on the <tt>:marshaler</tt> object to. Defaults to 'dump'.
70
+ #
71
+ # :load_method => The load method name to call on the <tt>:marshaler</tt> object. Defaults to 'load'.
72
+ #
73
+ # :encryptor => The object to use for encrypting. Defaults to Encryptor. TODO(DJS): Need to changed this to indicate our encryptor
74
+ #
75
+ # :encrypt_method => The encrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'encrypt'. TODO(DJS): Verify this.
76
+ #
77
+ # :decrypt_method => The decrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'decrypt'. TODO(DJS): Verify this.
78
+ #
79
+ # :if => Attributes are only encrypted if this option evaluates to true. If you pass a symbol representing an instance
80
+ # method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
81
+ # Defaults to true.
82
+ #
83
+ # :unless => Attributes are only encrypted if this option evaluates to false. If you pass a symbol representing an instance
84
+ # method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
85
+ # Defaults to false.
86
+ #
87
+ # You can specify your own default options
88
+ #
89
+ # TODO(DJS): Need to rework the examples.
90
+ # class User
91
+ # # now all attributes will be encoded and marshaled by default
92
+ # attr_encrypted_options.merge!(:encode => true, :marshal => true, :some_other_option => true)
93
+ # attr_encrypted :configuration, :key => 'my secret key'
94
+ # end
95
+ #
96
+ #
97
+ # Example
98
+ #
99
+ # class User
100
+ # attr_encrypted :email, :credit_card, :key => 'some secret key'
101
+ # attr_encrypted :configuration, :key => 'some other secret key', :marshal => true
102
+ # end
103
+ #
104
+ # @user = User.new
105
+ # @user.encrypted_email # nil
106
+ # @user.email? # false
107
+ # @user.email = 'test@example.com'
108
+ # @user.email? # true
109
+ # @user.encrypted_email # returns the encrypted version of 'test@example.com'
110
+ #
111
+ # @user.configuration = { :time_zone => 'UTC' }
112
+ # @user.encrypted_configuration # returns the encrypted version of configuration
113
+ #
114
+ # See README for more examples
115
+ def attr_encrypted(*attributes)
116
+ options = {
117
+ :prefix => '',
118
+ :suffix => '_enc',
119
+ :if => true,
120
+ :unless => false,
121
+ :encode => false,
122
+ :key => $encryption_key,
123
+ :type => 'text',
124
+ :default_encoding => 'm',
125
+ :preenrypt => nil,
126
+ :marshal => false,
127
+ :marshaler => Marshal,
128
+ :dump_method => 'dump',
129
+ :load_method => 'load',
130
+ :encryptor => MySQLEncryptor.instance,
131
+ :encrypt_method => 'encrypt',
132
+ :decrypt_method => 'decrypt'
133
+ }.merge!(attr_encrypted_options).merge!(attributes.last.is_a?(Hash) ? attributes.pop : {})
134
+
135
+ options[:encode] = options[:default_encoding] if options[:encode] == true
136
+
137
+ attributes.each do |attribute|
138
+ encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
139
+
140
+ instance_methods_as_symbols = instance_methods.collect { |method| method.to_sym }
141
+ attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
142
+ attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
143
+
144
+ define_method(attribute) do
145
+ cached_value = instance_variable_get("@#{attribute}")
146
+ value = cached_value && options[:type] == 'date' && cached_value.is_a?(Date) ? cached_value : nil
147
+ value || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
148
+ # instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
149
+ end
150
+
151
+ define_method("#{attribute}=") do |value|
152
+ send("#{encrypted_attribute_name}=", encrypt(attribute, value))
153
+ instance_variable_set("@#{attribute}", value)
154
+ end
155
+
156
+ define_method("#{attribute}?") do
157
+ value = send(attribute)
158
+ value.respond_to?(:empty?) ? !value.empty? : !!value
159
+ end
160
+
161
+ encrypted_attributes[attribute.to_sym] = options.merge(:attribute => encrypted_attribute_name)
162
+ end
163
+ end
164
+ alias_method :attr_encryptor, :attr_encrypted
165
+
166
+ # Default options to use with calls to <tt>attr_encrypted</tt>
167
+ #
168
+ # It will inherit existing options from its superclass
169
+ def attr_encrypted_options
170
+ @attr_encrypted_options ||= superclass.attr_encrypted_options.dup
171
+ end
172
+
173
+ # Checks if an attribute is configured with <tt>attr_encrypted</tt>
174
+ #
175
+ # Example
176
+ #
177
+ # class User
178
+ # attr_accessor :name
179
+ # attr_encrypted :email
180
+ # end
181
+ #
182
+ # User.attr_encrypted?(:name) # false
183
+ # User.attr_encrypted?(:email) # true
184
+ def attr_encrypted?(attribute)
185
+ encrypted_attributes.has_key?(attribute.to_sym)
186
+ end
187
+
188
+ # Decrypts a value for the attribute specified
189
+ #
190
+ # Example
191
+ #
192
+ # class User
193
+ # attr_encrypted :email
194
+ # end
195
+ #
196
+ # email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
197
+ def decrypt(attribute, encrypted_value, options = {})
198
+ options = encrypted_attributes[attribute.to_sym].merge(options)
199
+ if options[:if] && !options[:unless] && !encrypted_value.nil? && !(encrypted_value.is_a?(String) && encrypted_value.empty?)
200
+ encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
201
+ value = options[:encryptor].send(options[:decrypt_method], options.merge!(:value => encrypted_value))
202
+ value = options[:marshaler].send(options[:load_method], value) if options[:marshal]
203
+ value
204
+ else
205
+ encrypted_value
206
+ end
207
+ end
208
+
209
+ # Encrypts a value for the attribute specified
210
+ #
211
+ # Example
212
+ #
213
+ # class User
214
+ # attr_encrypted :email
215
+ # end
216
+ #
217
+ # encrypted_email = User.encrypt(:email, 'test@example.com')
218
+ def encrypt(attribute, value, options = {})
219
+ options = encrypted_attributes[attribute.to_sym].merge(options)
220
+ if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?)
221
+ value = options[:preencrypt] ? (value.is_a?(String) ? value.send(options[:preencrypt]) : value) : value
222
+ value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
223
+ encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(:value => value))
224
+ encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
225
+ encrypted_value
226
+ else
227
+ cleanse_value value, options
228
+ end
229
+ end
230
+
231
+ # Cleans up the value to ensure we don't get empty strings the db when we should be getting nils.
232
+ def cleanse_value(value, options)
233
+ return nil if value.is_a?(String) && value.empty? && options[:type] == 'date'
234
+ value
235
+ end
236
+
237
+ # Contains a hash of encrypted attributes with virtual attribute names as keys
238
+ # and their corresponding options as values
239
+ #
240
+ # Example
241
+ #
242
+ # class User
243
+ # attr_encrypted :email, :key => 'my secret key'
244
+ # end
245
+ #
246
+ # User.encrypted_attributes # { :email => { :attribute => 'encrypted_email', :key => 'my secret key' } }
247
+ def encrypted_attributes
248
+ @encrypted_attributes ||= superclass.encrypted_attributes.dup
249
+ end
250
+
251
+ # Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
252
+ # if attribute was configured with attr_encrypted
253
+ #
254
+ # Example
255
+ #
256
+ # class User
257
+ # attr_encrypted :email, :key => 'my secret key'
258
+ # end
259
+ #
260
+ # User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
261
+ def method_missing(method, *arguments, &block)
262
+ if method.to_s =~ /\A((en|de)crypt)_(.+)\z/ && attr_encrypted?($3)
263
+ send($1, $3, *arguments)
264
+ else
265
+ super
266
+ end
267
+ end
268
+
269
+ module InstanceMethods
270
+ # Decrypts a value for the attribute specified using options evaluated in the current object's scope
271
+ #
272
+ # Example
273
+ #
274
+ # class User
275
+ # attr_accessor :secret_key
276
+ # attr_encrypted :email, :key => :secret_key
277
+ #
278
+ # def initialize(secret_key)
279
+ # self.secret_key = secret_key
280
+ # end
281
+ # end
282
+ #
283
+ # @user = User.new('some-secret-key')
284
+ # @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
285
+ def decrypt(attribute, encrypted_value)
286
+ self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
287
+ end
288
+
289
+ # Encrypts a value for the attribute specified using options evaluated in the current object's scope
290
+ #
291
+ # Example
292
+ #
293
+ # class User
294
+ # attr_accessor :secret_key
295
+ # attr_encrypted :email, :key => :secret_key
296
+ #
297
+ # def initialize(secret_key)
298
+ # self.secret_key = secret_key
299
+ # end
300
+ # end
301
+ #
302
+ # @user = User.new('some-secret-key')
303
+ # @user.encrypt(:email, 'test@example.com')
304
+ def encrypt(attribute, value)
305
+ self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
306
+ end
307
+
308
+ def unencrypted_attributes
309
+ attributes.each_with_object({}) do |a, new_hash|
310
+ key = a.first
311
+ value = a.last
312
+ if key =~ /\A(.+)_enc\z/
313
+ key = $1
314
+ value = decrypt(key.to_sym, value)
315
+ end
316
+ new_hash[key] = value
317
+ end
318
+ end
319
+
320
+ protected
321
+
322
+ # Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
323
+ def evaluated_attr_encrypted_options_for(attribute)
324
+ self.class.encrypted_attributes[attribute.to_sym].inject({}) { |hash, (option, value)| hash.merge!(option => (option == :preencrypt) ? value : evaluate_attr_encrypted_option(value)) }
325
+ end
326
+
327
+ # Evaluates symbol (method reference) or proc (responds to call) options
328
+ #
329
+ # If the option is not a symbol or proc then the original option is returned
330
+ def evaluate_attr_encrypted_option(option)
331
+ if option.is_a?(Symbol) && respond_to?(option)
332
+ send(option)
333
+ elsif option.respond_to?(:call)
334
+ option.call(self)
335
+ else
336
+ option
337
+ end
338
+ end
339
+ end
340
+ end
341
+
342
+ Object.extend AttrEncryption
343
+
344
+ # require File.expand_path('attr_encryption/adapters/active_record.rb', File.dirname(__FILE__))
345
+ require 'attr_encryption/adapters/active_record.rb'
@@ -0,0 +1,58 @@
1
+ if defined?(ActiveRecord::Base)
2
+ module AttrEncryption
3
+ module Adapters
4
+ module ActiveRecord
5
+ def self.extended(base) # :nodoc:
6
+ base.class_eval do
7
+ # TODO(DJS): We don't want encoding by define in OUR ActiveRecord models
8
+ # attr_encrypted_options[:encode] = true
9
+ class << self; alias_method_chain :method_missing, :attr_encrypted; end
10
+ end
11
+ end
12
+
13
+ protected
14
+
15
+ # Ensures the attribute methods for db fields have been defined before calling the original
16
+ # <tt>attr_encrypted</tt> method
17
+ def attr_encrypted(*attrs)
18
+ define_attribute_methods rescue nil
19
+ super
20
+ attrs.reject { |attr| attr.is_a?(Hash) }.each { |attr| alias_method "#{attr}_before_type_cast", attr }
21
+ end
22
+
23
+ # Allows you to use dynamic methods like <tt>find_by_email</tt> or <tt>scoped_by_email</tt> for
24
+ # encrypted attributes
25
+ #
26
+ # NOTE: This only works when the <tt>:key</tt> option is specified as a string (see the README)
27
+ #
28
+ # This is useful for encrypting fields like email addresses. Your user's email addresses
29
+ # are encrypted in the database, but you can still look up a user by email for logging in
30
+ #
31
+ # Example
32
+ #
33
+ # class User < ActiveRecord::Base
34
+ # attr_encrypted :email, :key => 'secret key'
35
+ # end
36
+ #
37
+ # User.find_by_email_and_password('test@example.com', 'testing')
38
+ # # results in a call to
39
+ # User.find_by_encrypted_email_and_password('the_encrypted_version_of_test@example.com', 'testing')
40
+ def method_missing_with_attr_encrypted(method, *args, &block)
41
+ if match = /\A(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)\z/.match(method.to_s)
42
+ attribute_names = match.captures.last.split('_and_')
43
+ attribute_names.each_with_index do |attribute, index|
44
+ if attr_encrypted?(attribute)
45
+ args[index] = send("encrypt_#{attribute}", args[index])
46
+ attribute_names[index] = encrypted_attributes[attribute.to_sym][:attribute]
47
+ end
48
+ end
49
+ method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym
50
+ end
51
+ method_missing_without_attr_encrypted(method, *args, &block)
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ ActiveRecord::Base.extend AttrEncryption::Adapters::ActiveRecord
58
+ end
@@ -0,0 +1,13 @@
1
+ class Date
2
+
3
+ def self.safe_parse(date)
4
+ return nil if date.blank?
5
+ return date if date.is_a?(Date) || date.is_a?(Time)
6
+ begin
7
+ Date.parse(date);
8
+ rescue Exception => e;
9
+ nil
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,73 @@
1
+ module MySQLEncryption
2
+ # Mimics MySQL's AES_ENCRYPT() and AES_DECRYPT() encryption functions
3
+ $encryption_key ||= "hzjd88hqxejak31"
4
+
5
+ def mysql_encrypt(s, key=$encryption_key)
6
+ return nil if s.blank?
7
+ do_encrypt(s, mysql_key(key))
8
+ end
9
+
10
+ def mysql_decrypt(s, key=$encryption_key)
11
+ return nil if s.blank?
12
+ do_decrypt(s, mysql_key(key))
13
+ end
14
+
15
+ protected
16
+
17
+ def aes(m,k,t)
18
+ (aes = OpenSSL::Cipher::AES128.new("ECB").send(m)).key = k
19
+ aes.update(t) << aes.final
20
+ end
21
+
22
+ def do_encrypt(text, key)
23
+ aes(:encrypt, key, text)
24
+ end
25
+
26
+ def do_decrypt(text, key)
27
+ aes(:decrypt, key, text)
28
+ end
29
+
30
+ #
31
+ # This method returns a key based on the specified key that is
32
+ # 16 bytes in length. If the specified key is shorter than 16 bytes
33
+ # it is zero-padded to 16 bytes. If the specified key is longer
34
+ # 16 bytes, the bytes of the original key are folded back on itself
35
+ # using the XOR operator. This ensures that all the bytes in the
36
+ # original key are used, but the resulting key remains 16 bytes long.
37
+ #
38
+ # Sheesh.
39
+ #
40
+ def mysql_key(key)
41
+ return nil if key.nil?
42
+ final_key = "\0" * 16
43
+ key.bytes.each_with_index do |b, i|
44
+ buf = (final_key[i%16].bytes.first ^ b)
45
+ final_key[i%16] = buf.chr
46
+ end
47
+ final_key
48
+ end
49
+
50
+ end
51
+
52
+ =begin
53
+ Copyright (c) 2009 Felipe Coury
54
+
55
+ Permission is hereby granted, free of charge, to any person obtaining
56
+ a copy of this software and associated documentation files (the
57
+ "Software"), to deal in the Software without restriction, including
58
+ without limitation the rights to use, copy, modify, merge, publish,
59
+ distribute, sublicense, and/or sell copies of the Software, and to
60
+ permit persons to whom the Software is furnished to do so, subject to
61
+ the following conditions:
62
+
63
+ The above copyright notice and this permission notice shall be
64
+ included in all copies or substantial portions of the Software.
65
+
66
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
67
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
68
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
69
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
70
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
71
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
72
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
73
+ =end
@@ -0,0 +1,33 @@
1
+ class MySQLEncryptor
2
+ include MySQLEncryption
3
+ include Singleton
4
+
5
+ def encrypt(encrypt_options)
6
+ value_to_encrypt = encrypt_options[:value].nil? ? nil : encrypt_options[:type] == 'date' ? encrypt_date(encrypt_options[:value]) : encrypt_options[:value].to_s
7
+ mysql_encrypt(value_to_encrypt, encrypt_options[:key])
8
+ end
9
+
10
+ def decrypt(encrypt_options)
11
+ decrypted_value = mysql_decrypt(encrypt_options[:value], encrypt_options[:key])
12
+ return decrypted_value if decrypted_value.nil?
13
+
14
+ case encrypt_options[:type]
15
+ when 'text'
16
+ decrypted_value.force_encoding('utf-8')
17
+ when 'date'
18
+ Date.parse(decrypted_value)
19
+ when 'datetime'
20
+ DateTime.parse(decrypted_value)
21
+ when 'binary'
22
+ decrypted_value # no processing
23
+ else
24
+ raise "Invalid type specified for post-processing decrypted value"
25
+ end
26
+ end
27
+
28
+ def encrypt_date(value)
29
+ dt = value.is_a?(String) ? Date.safe_parse(value) : value
30
+ dt.strftime("%Y%m%d") if dt.present?
31
+ end
32
+
33
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyClass
4
+ include MySQLEncryption
5
+ end
6
+
7
+ describe 'MySQLEncryption' do
8
+
9
+ before(:each) do
10
+ @cut = DummyClass.new
11
+ @key = "eaduccitranatereheenalaistater"
12
+ end
13
+
14
+ context "Public Methods" do
15
+
16
+ context '#mysql_encrypt' do
17
+
18
+ it "should return nil if a nil string is specified" do
19
+ expect(@cut.mysql_encrypt(nil, @key)).to be_nil
20
+ end
21
+
22
+ it "should call do_encrypt with a mysql'ized key and the string" do
23
+ plain_text = "Plain Text"
24
+ @cut.should_receive(:do_encrypt).
25
+ with(plain_text, "abcdefghijklmnop").
26
+ and_return("encrypted_value")
27
+ @cut.should_receive(:mysql_key).
28
+ with("abcdefghijklmnop").
29
+ and_return("abcdefghijklmnop")
30
+ expect(@cut.mysql_encrypt(plain_text, "abcdefghijklmnop")).to eq("encrypted_value")
31
+ end
32
+
33
+ end
34
+
35
+ context '#mysql_decrypt' do
36
+
37
+ it "should return nil if a nil string is specified" do
38
+ expect(@cut.mysql_decrypt(nil, @key)).to be_nil
39
+ end
40
+
41
+ it "should call do_decrypt with a mysql'ized key and the string" do
42
+ plain_text = "Plain Text"
43
+ @cut.should_receive(:do_decrypt).
44
+ with(plain_text, "abcdefghijklmnop").
45
+ and_return("encrypted_value")
46
+ @cut.should_receive(:mysql_key).
47
+ with("abcdefghijklmnop").
48
+ and_return("abcdefghijklmnop")
49
+ expect(@cut.mysql_decrypt(plain_text, "abcdefghijklmnop")).to eq("encrypted_value")
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ context 'Protected Methods' do
57
+
58
+ context '#aes' do
59
+
60
+ it "should be able to encrypt, then decrypt a string using a specified key" do
61
+ plain_text = 'plain text'
62
+ encrypted_string = @cut.send(:aes, :encrypt, @key, plain_text)
63
+ unencrypted_string = @cut.send(:aes, :decrypt, @key, encrypted_string)
64
+ expect(unencrypted_string).to eq(plain_text)
65
+ end
66
+
67
+ end
68
+
69
+ context '#do_en/decrypt' do
70
+
71
+ it "should be able to encrypt, then decrypt a string using a specified key" do
72
+ plain_text = 'plain text'
73
+ encrypted_string = @cut.send(:do_encrypt, plain_text, @key)
74
+ unencrypted_string = @cut.send(:do_decrypt, encrypted_string, @key)
75
+ expect(unencrypted_string).to eq(plain_text)
76
+ end
77
+ end
78
+
79
+ context '#mysql_key' do
80
+
81
+ it "should return nil if a nil key was specified" do
82
+ expect(@cut.send(:mysql_key, nil)).to be_nil
83
+ end
84
+
85
+ it "should return a final key that is 16 bytes long, each byte being the null-value if a blank key is specified" do
86
+ expect(@cut.send(:mysql_key, "")).to eq("\0" * 16)
87
+ end
88
+
89
+ it "should right-pad a specified key with null bytes if the specified key is less than 16 bytes" do
90
+ key = "abcdefghijklmop" # 15-byte key
91
+ expect(@cut.send(:mysql_key, key)).to eq("abcdefghijklmop\0")
92
+ end
93
+
94
+ it "should simply echo back the same key if the specified key is 16 bytes" do
95
+ key = "abcdefghijklmopq" # 16-byte key
96
+ expect(@cut.send(:mysql_key, key)).to eq("abcdefghijklmopq")
97
+ end
98
+
99
+ it "should XOR the leading bytes of the resulting key with the trailing bytes of the specified key over 16 bytes" do
100
+ key = "abcdefghijklmopqrs" # 18-byte key
101
+ expect(@cut.send(:mysql_key, key)).to eq(("a".bytes.first ^ "r".bytes.first).chr +
102
+ ("b".bytes.first ^ "s".bytes.first).chr +
103
+ "cdefghijklmopq")
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,132 @@
1
+ require 'spec_helper'
2
+
3
+ describe MySQLEncryptor do
4
+
5
+ before(:each) do
6
+ @cut = MySQLEncryptor.instance
7
+ end
8
+
9
+ context '#encrypt' do
10
+
11
+ it "should return nil if the value passed in the options is nil" do
12
+ @cut.should_receive(:mysql_encrypt).
13
+ with(nil, 'the_key_value').
14
+ and_return(nil)
15
+
16
+ expect(@cut.encrypt(value: nil, key: 'the_key_value')).
17
+ to be_nil
18
+ end
19
+
20
+ it "should convert the value to a string before having the mysql_encrypt method encrypt it" do
21
+ @cut.should_receive(:mysql_encrypt).
22
+ with("12", 'the_key_value').
23
+ and_return("encrypted_12")
24
+
25
+ expect(@cut.encrypt(value: 12, key: 'the_key_value')).
26
+ to eq("encrypted_12")
27
+ end
28
+
29
+ it "should convert the value to a date string before encryption when the type is specified as a 'date'" do
30
+ date = Date.today
31
+ date_str = date.strftime("%Y%m%d")
32
+ @cut.should_receive(:encrypt_date).
33
+ with(date).and_return(date_str)
34
+ @cut.should_receive(:mysql_encrypt).
35
+ with(date_str, 'the_key_value').
36
+ and_return("encrypted_date")
37
+
38
+ expect(@cut.encrypt(value: date, key: 'the_key_value', type: 'date')).
39
+ to eq("encrypted_date")
40
+ end
41
+
42
+ end
43
+
44
+ context '#decrypt' do
45
+
46
+ it "should return nil if mysql_decrypt returns a nil" do
47
+ @cut.should_receive(:mysql_decrypt).
48
+ with(nil, 'the_key').
49
+ and_return(nil)
50
+
51
+ expect(@cut.decrypt(value: nil, key: 'the_key')).
52
+ to be_nil
53
+ end
54
+
55
+ it "should force the encoding of the decrypted string if the type is 'text'" do
56
+ decrypted_value = 'decrypted_value'
57
+ decrypted_value.should_receive(:force_encoding).
58
+ with('utf-8').
59
+ and_return(decrypted_value)
60
+
61
+ @cut.should_receive(:mysql_decrypt).
62
+ with('encrypted_value', 'the_key').
63
+ and_return(decrypted_value)
64
+
65
+ expect(@cut.decrypt(value: 'encrypted_value', key: 'the_key', type: 'text')).
66
+ to eq(decrypted_value)
67
+ end
68
+
69
+ it "should return a Date object if the type of the encrypted value is 'date'" do
70
+ date = Date.today
71
+ date_str = date.strftime("%Y%m%d")
72
+
73
+ @cut.should_receive(:mysql_decrypt).
74
+ with('encrypted_date', 'the_key').
75
+ and_return(date_str)
76
+
77
+ expect(@cut.decrypt(value: 'encrypted_date', key: 'the_key', type: 'date')).
78
+ to eq(date)
79
+ end
80
+
81
+ it "should return a DateTime object if the type of the encrypted value is 'datetime'" do
82
+ date = DateTime.now.utc
83
+ date_str = date.strftime("%Y-%m-%d %H:%M:%S")
84
+
85
+ @cut.should_receive(:mysql_decrypt).
86
+ with('encrypted_datetime', 'the_key').
87
+ and_return(date_str)
88
+
89
+ expect(@cut.decrypt(value: 'encrypted_datetime', key: 'the_key', type: 'datetime').strftime("%Y-%m-%d %H:%M:%S")).
90
+ to eq(date_str)
91
+ end
92
+
93
+ it "should return the raw-unprocessed encrypted data if the value is binary" do
94
+ @cut.should_receive(:mysql_decrypt).
95
+ with('encrypted_binary', 'the_key').
96
+ and_return("unencrypted_binary")
97
+
98
+ expect(@cut.decrypt(value: 'encrypted_binary', key: 'the_key', type: 'binary')).
99
+ to eq("unencrypted_binary")
100
+ end
101
+
102
+ it "should raise a runtime exception if the specified data type is not recognized" do
103
+ @cut.should_receive(:mysql_decrypt).
104
+ with('value', 'the_key').
105
+ and_return("unencrypted_binary")
106
+ expect { @cut.decrypt(value: 'value', key: 'the_key', type: 'bad_type') }.
107
+ to raise_error(RuntimeError)
108
+ end
109
+
110
+ end
111
+
112
+ context '#encrypt_date' do
113
+
114
+ it "should return nil if a nil value is specified" do
115
+ expect(@cut.encrypt_date(nil)).
116
+ to be_nil
117
+ end
118
+
119
+ it "should parse specified value as a date if a string is specified, then format it appropriately" do
120
+ expect(@cut.encrypt_date("2013/09/08")).
121
+ to eq("20130908")
122
+ end
123
+
124
+ it "should use the date specified and format it appropriately" do
125
+ date = Date.today
126
+ date_str = date.strftime("%Y%m%d")
127
+ expect(@cut.encrypt_date(date)).
128
+ to eq(date_str)
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'active_support/core_ext/object'
5
+ require 'attr_encryption'
6
+ require 'openssl'
7
+
8
+ # Requires supporting files with custom matchers and macros, etc,
9
+ # in ./support/ and its subdirectories.
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
11
+
12
+ RSpec.configure do |config|
13
+
14
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attr_encryption
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Dave Sieh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 3.2.14
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 3.2.14
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rdoc
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '3.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '3.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: jeweler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 1.8.7
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 1.8.7
83
+ description: Provides an extension for Ruby and rails object to support a flexible
84
+ means of encrypting attributes.
85
+ email: dave.sieh@providigm.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files:
89
+ - LICENSE.txt
90
+ - README.rdoc
91
+ files:
92
+ - .document
93
+ - .rspec
94
+ - Gemfile
95
+ - Gemfile.lock
96
+ - LICENSE.txt
97
+ - README.rdoc
98
+ - Rakefile
99
+ - VERSION
100
+ - lib/attr_encryption.rb
101
+ - lib/attr_encryption/adapters/active_record.rb
102
+ - lib/attr_encryption/date_extensions.rb
103
+ - lib/attr_encryption/mysql_encryption.rb
104
+ - lib/attr_encryption/mysql_encryptor.rb
105
+ - spec/mysql_encryption_spec.rb
106
+ - spec/mysql_encryptor_spec.rb
107
+ - spec/spec_helper.rb
108
+ homepage: http://github.com/GitHubAdmin/attr_encryption
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.3.0
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Extends Object and ActiveRecord::Base objects to support encrypted attributes
132
+ test_files: []