powerhome-attr_encrypted 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +67 -0
  4. data/CHANGELOG.md +98 -0
  5. data/Gemfile +3 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +465 -0
  8. data/Rakefile +25 -0
  9. data/attr_encrypted.gemspec +63 -0
  10. data/certs/saghaulor.pem +21 -0
  11. data/checksum/attr_encrypted-3.0.0.gem.sha256 +1 -0
  12. data/checksum/attr_encrypted-3.0.0.gem.sha512 +1 -0
  13. data/checksum/attr_encrypted-3.0.1.gem.sha256 +1 -0
  14. data/checksum/attr_encrypted-3.0.1.gem.sha512 +1 -0
  15. data/checksum/attr_encrypted-3.0.2.gem.sha256 +1 -0
  16. data/checksum/attr_encrypted-3.0.2.gem.sha512 +1 -0
  17. data/checksum/attr_encrypted-3.0.3.gem.sha256 +1 -0
  18. data/checksum/attr_encrypted-3.0.3.gem.sha512 +1 -0
  19. data/checksum/attr_encrypted-3.1.0.gem.sha256 +1 -0
  20. data/checksum/attr_encrypted-3.1.0.gem.sha512 +1 -0
  21. data/lib/attr_encrypted.rb +473 -0
  22. data/lib/attr_encrypted/adapters/active_record.rb +157 -0
  23. data/lib/attr_encrypted/adapters/data_mapper.rb +24 -0
  24. data/lib/attr_encrypted/adapters/sequel.rb +16 -0
  25. data/lib/attr_encrypted/version.rb +19 -0
  26. data/test/active_record_test.rb +365 -0
  27. data/test/attr_encrypted_test.rb +490 -0
  28. data/test/compatibility_test.rb +109 -0
  29. data/test/data_mapper_test.rb +59 -0
  30. data/test/legacy_active_record_test.rb +120 -0
  31. data/test/legacy_attr_encrypted_test.rb +300 -0
  32. data/test/legacy_compatibility_test.rb +95 -0
  33. data/test/legacy_data_mapper_test.rb +57 -0
  34. data/test/legacy_sequel_test.rb +54 -0
  35. data/test/run.sh +12 -0
  36. data/test/sequel_test.rb +55 -0
  37. data/test/test_helper.rb +61 -0
  38. metadata +294 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+ require 'rdoc/task'
5
+ require "bundler/gem_tasks"
6
+
7
+ desc 'Test the attr_encrypted gem.'
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << 'lib'
10
+ t.pattern = 'test/**/*_test.rb'
11
+ t.warning = false
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the attr_encrypted gem.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'attr_encrypted'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README*')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
23
+
24
+ desc 'Default: run unit tests.'
25
+ task :default => :test
@@ -0,0 +1,63 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ lib = File.expand_path('../lib/', __FILE__)
4
+ $:.unshift lib unless $:.include?(lib)
5
+
6
+ require 'attr_encrypted/version'
7
+ require 'date'
8
+
9
+ Gem::Specification.new do |s|
10
+ s.name = 'powerhome-attr_encrypted'
11
+ s.version = AttrEncrypted::Version.string
12
+ s.date = Date.today
13
+
14
+ s.summary = "Power's version of the attr_encrypted gem"
15
+ s.description = 'Generates attr_accessors that encrypt and decrypt attributes transparently'
16
+
17
+ s.authors = ['Wade Winningham', 'Ben Langfeld']
18
+ s.email = ['wwinningham@powerhrg.com', 'ben@langfeld.me']
19
+ s.homepage = 'https://github.com/powerhome/attr_encrypted'
20
+ s.license = 'MIT'
21
+
22
+ s.require_paths = ['lib']
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- test/*`.split("\n")
26
+
27
+ s.required_ruby_version = '>= 2.0.0'
28
+
29
+ s.add_dependency('encryptor', ['~> 3.0.0'])
30
+ # support for testing with specific active record version
31
+ activerecord_version = if ENV.key?('ACTIVERECORD')
32
+ "~> #{ENV['ACTIVERECORD']}"
33
+ else
34
+ '>= 2.0.0'
35
+ end
36
+ s.add_development_dependency('activerecord', activerecord_version)
37
+ s.add_development_dependency('actionpack', activerecord_version)
38
+ s.add_development_dependency('datamapper')
39
+ s.add_development_dependency('rake')
40
+ s.add_development_dependency('minitest')
41
+ s.add_development_dependency('sequel')
42
+ s.add_development_dependency('pry-byebug')
43
+ if RUBY_VERSION < '2.1.0'
44
+ s.add_development_dependency('nokogiri', '< 1.7.0')
45
+ s.add_development_dependency('public_suffix', '< 3.0.0')
46
+ end
47
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby
48
+ s.add_development_dependency('activerecord-jdbcsqlite3-adapter')
49
+ s.add_development_dependency('jdbc-sqlite3', '< 3.8.7') # 3.8.7 is nice and broke
50
+ else
51
+ s.add_development_dependency('sqlite3', '~> 1.3.0', '>= 1.3.6')
52
+ end
53
+ s.add_development_dependency('dm-sqlite-adapter')
54
+ s.add_development_dependency('simplecov')
55
+ s.add_development_dependency('simplecov-rcov')
56
+ s.add_development_dependency("codeclimate-test-reporter", '<= 0.6.0')
57
+
58
+ s.post_install_message = "\n\n\nWARNING: Several insecure default options and features were deprecated in attr_encrypted v2.0.0.\n
59
+ Additionally, there was a bug in Encryptor v2.0.0 that insecurely encrypted data when using an AES-*-GCM algorithm.\n
60
+ This bug was fixed but introduced breaking changes between v2.x and v3.x.\n
61
+ Please see the README for more information regarding upgrading to attr_encrypted v3.0.0.\n\n\n"
62
+
63
+ end
@@ -0,0 +1,21 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDdDCCAlygAwIBAgIBATANBgkqhkiG9w0BAQUFADBAMRIwEAYDVQQDDAlzYWdo
3
+ YXVsb3IxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkWA2Nv
4
+ bTAeFw0xODAyMTIwMzMzMThaFw0xOTAyMTIwMzMzMThaMEAxEjAQBgNVBAMMCXNh
5
+ Z2hhdWxvcjEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYD
6
+ Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvOLqbSmj5txfw39a
7
+ Ki0g3BJWGrfGBiSRq9aThUGzoiaqyDo/m1WMQdgPioZG+P923okChEWFjhSymBQU
8
+ eMdys6JRPm5ortp5sh9gesOWoozqb8R55d8rr1V7pY533cCut53Kb1wiitjkfXjR
9
+ efT2HPh6nV6rYjGMJek/URaCNzsZo7HCkRsKdezP+BKr4V4wOka69tfJX5pcvFvR
10
+ iiqfaiP4RK12hYdsFnSVKiKP7SAFTFiYcohbL8TUW6ezQQqJCK0M6fu74EWVCnBS
11
+ gFVjj931BuD8qhuxMiB6uC6FKxemB5TRGBLzn7RcrOMAo2inMAopjkGeQJUAyVCm
12
+ J5US3wIDAQABo3kwdzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU
13
+ hJEuSZgvuuIhIsxQ/0pRQTBVzokwHgYDVR0RBBcwFYETc2FnaGF1bG9yQGdtYWls
14
+ LmNvbTAeBgNVHRIEFzAVgRNzYWdoYXVsb3JAZ21haWwuY29tMA0GCSqGSIb3DQEB
15
+ BQUAA4IBAQCsBS2cxqTmV4nXJEH/QbdgjVDAZbK6xf2gpM3vCRlYsy7Wz6GEoOpD
16
+ bzRkjxZwGNbhXShMUZwm6zahYQ/L1/HFztLoMBMkm8EIfPxH0PDrP4aWl0oyWxmU
17
+ OLm0/t9icSWRPPJ1tLJvuAaDdVpY5dEHd6VdnNJGQC5vHKRInt1kEyqEttIJ/xmJ
18
+ leSEFyMeoFsR+C/WPG9WSC+xN0eXqakCu6YUJoQzCn/7znv8WxpHEbeZjNIHq0qb
19
+ nbqZ/ZW1bwzj1T/NIbnMr37wqV29XwkI4+LbewMkb6/bDPYl0qZpAkCxKtGYCCJp
20
+ l6KPs9K/72yH00dxuAhiTXikTkcLXeQJ
21
+ -----END CERTIFICATE-----
@@ -0,0 +1 @@
1
+ 845fc3cb09a19c3ac76192aba443788f92c880744617bca99b16fd31ce843e07
@@ -0,0 +1 @@
1
+ 81a065442258cc3702aab62c7b2307a48ed3e0deb803600d11a7480cce0db7c43fd9929acd2755081042f8989236553fd694b6cb62776bbfc53f9165a22cbca1
@@ -0,0 +1 @@
1
+ 33140af4b223177db7a19efb2fa38472a299a745b29ca1c5ba9d3fa947390b77
@@ -0,0 +1 @@
1
+ 0c467cab98b9b2eb331f9818323a90ae01392d6cb03cf1f32faccc954d0fc54be65f0fc7bf751b0fce57925eef1c9e2af90181bc40d81ad93e21d15a001c53c6
@@ -0,0 +1 @@
1
+ c1256b459336d4a2012a0d0c70ce5cd3dac46acb5e78da6f77f6f104cb1e8b7b
@@ -0,0 +1 @@
1
+ dca0c8a729974c0e26fde4cd4216c7d0f66d9eca9f6cf0ccca64999f5180a00bf7c05b630c1d420ec1673141a2923946e8bd28b12e711faf64a4cd42c7a3ac9e
@@ -0,0 +1 @@
1
+ 6d84c64852c4bbc0926b92fe7a93295671a9e69cb2939b96fb1e4b5e8a5b33b6
@@ -0,0 +1 @@
1
+ 0f960e8a2f63c747c273241f7395dcceb0dd8a6f79349bee453db741fc7ea5ceb4342d7d5908e540e3b5acea2216ff38bef8c743e6e7c8559bacb4a731ab27c4
@@ -0,0 +1 @@
1
+ 4f0682604714ed4599cf00771ad27e82f0b51b0ed8644af51a43d21fbe129b59
@@ -0,0 +1 @@
1
+ dc757a50c53175dc05adbfccb25b536f7f1a060c7bd626bfc72ff577479c6fe28d3b65747033276bd4bce3dad741b67235f3e01ea61409f6c67959da65df445b
@@ -0,0 +1,473 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'encryptor'
4
+
5
+ # Adds attr_accessors that encrypt and decrypt an object's attributes
6
+ module AttrEncrypted
7
+ autoload :Version, 'attr_encrypted/version'
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.
26
+ # Defaults to nil.
27
+ #
28
+ # prefix: A prefix used to generate the name of the referenced encrypted attributes.
29
+ # For example <tt>attr_accessor :email, prefix: 'crypted_'</tt> would
30
+ # generate attributes named 'crypted_email' to store the encrypted
31
+ # email and password.
32
+ # Defaults to 'encrypted_'.
33
+ #
34
+ # suffix: A suffix used to generate the name of the referenced encrypted attributes.
35
+ # For example <tt>attr_accessor :email, prefix: '', suffix: '_encrypted'</tt>
36
+ # would generate attributes named 'email_encrypted' to store the
37
+ # encrypted email.
38
+ # Defaults to ''.
39
+ #
40
+ # key: The encryption key. This option may not be required if
41
+ # you're using a custom encryptor. If you pass a symbol
42
+ # representing an instance method then the :key option
43
+ # will be replaced with the result of the method before
44
+ # being passed to the encryptor. Objects that respond
45
+ # to :call are evaluated as well (including procs).
46
+ # Any other key types will be passed directly to the encryptor.
47
+ # Defaults to nil.
48
+ #
49
+ # encode: If set to true, attributes will be encoded as well as
50
+ # encrypted. This is useful if you're planning on storing
51
+ # the encrypted attributes in a database. The default
52
+ # encoding is 'm' (base64), however this can be overwritten
53
+ # by setting the :encode option to some other encoding
54
+ # string instead of just 'true'. See
55
+ # http://www.ruby-doc.org/core/classes/Array.html#M002245
56
+ # for more encoding directives.
57
+ # Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
58
+ #
59
+ # encode_iv: Defaults to true.
60
+
61
+ # encode_salt: Defaults to true.
62
+ #
63
+ # default_encoding: Defaults to 'm' (base64).
64
+ #
65
+ # marshal: If set to true, attributes will be marshaled as well
66
+ # as encrypted. This is useful if you're planning on
67
+ # encrypting something other than a string.
68
+ # Defaults to false.
69
+ #
70
+ # marshaler: The object to use for marshaling.
71
+ # Defaults to Marshal.
72
+ #
73
+ # dump_method: The dump method name to call on the <tt>:marshaler</tt> object to.
74
+ # Defaults to 'dump'.
75
+ #
76
+ # load_method: The load method name to call on the <tt>:marshaler</tt> object.
77
+ # Defaults to 'load'.
78
+ #
79
+ # encryptor: The object to use for encrypting.
80
+ # Defaults to Encryptor.
81
+ #
82
+ # encrypt_method: The encrypt method name to call on the <tt>:encryptor</tt> object.
83
+ # Defaults to 'encrypt'.
84
+ #
85
+ # decrypt_method: The decrypt method name to call on the <tt>:encryptor</tt> object.
86
+ # Defaults to 'decrypt'.
87
+ #
88
+ # if: Attributes are only encrypted if this option evaluates
89
+ # to true. If you pass a symbol representing an instance
90
+ # method then the result of the method will be evaluated.
91
+ # Any objects that respond to <tt>:call</tt> are evaluated as well.
92
+ # Defaults to true.
93
+ #
94
+ # unless: Attributes are only encrypted if this option evaluates
95
+ # to false. If you pass a symbol representing an instance
96
+ # method then the result of the method will be evaluated.
97
+ # Any objects that respond to <tt>:call</tt> are evaluated as well.
98
+ # Defaults to false.
99
+ #
100
+ # mode: Selects encryption mode for attribute: choose <tt>:single_iv_and_salt</tt> for compatibility
101
+ # with the old attr_encrypted API: the IV is derived from the encryption key by the underlying Encryptor class; salt is not used.
102
+ # The <tt>:per_attribute_iv_and_salt</tt> mode uses a per-attribute IV and salt. The salt is used to derive a unique key per attribute.
103
+ # A <tt>:per_attribute_iv</default> mode derives a unique IV per attribute; salt is not used.
104
+ # Defaults to <tt>:per_attribute_iv</tt>.
105
+ #
106
+ # allow_empty_value: Attributes which have nil or empty string values will not be encrypted unless this option
107
+ # has a truthy value.
108
+ #
109
+ # You can specify your own default options
110
+ #
111
+ # class User
112
+ # # Now all attributes will be encoded and marshaled by default
113
+ # attr_encrypted_options.merge!(encode: true, marshal: true, some_other_option: true)
114
+ # attr_encrypted :configuration, key: 'my secret key'
115
+ # end
116
+ #
117
+ #
118
+ # Example
119
+ #
120
+ # class User
121
+ # attr_encrypted :email, key: 'some secret key'
122
+ # attr_encrypted :configuration, key: 'some other secret key', marshal: true
123
+ # end
124
+ #
125
+ # @user = User.new
126
+ # @user.encrypted_email # nil
127
+ # @user.email? # false
128
+ # @user.email = 'test@example.com'
129
+ # @user.email? # true
130
+ # @user.encrypted_email # returns the encrypted version of 'test@example.com'
131
+ #
132
+ # @user.configuration = { time_zone: 'UTC' }
133
+ # @user.encrypted_configuration # returns the encrypted version of configuration
134
+ #
135
+ # See README for more examples
136
+ def attr_encrypted(*attributes)
137
+ options = attributes.last.is_a?(Hash) ? attributes.pop : {}
138
+ options = attr_encrypted_default_options.dup.merge!(attr_encrypted_options).merge!(options)
139
+
140
+ options[:encode] = options[:default_encoding] if options[:encode] == true
141
+ options[:encode_iv] = options[:default_encoding] if options[:encode_iv] == true
142
+ options[:encode_salt] = options[:default_encoding] if options[:encode_salt] == true
143
+
144
+ attributes.each do |attribute|
145
+ encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
146
+
147
+ instance_methods_as_symbols = attribute_instance_methods_as_symbols
148
+
149
+ if attribute_instance_methods_as_symbols_available?
150
+ attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
151
+ attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
152
+
153
+ iv_name = "#{encrypted_attribute_name}_iv".to_sym
154
+ attr_reader iv_name unless instance_methods_as_symbols.include?(iv_name)
155
+ attr_writer iv_name unless instance_methods_as_symbols.include?(:"#{iv_name}=")
156
+
157
+ salt_name = "#{encrypted_attribute_name}_salt".to_sym
158
+ attr_reader salt_name unless instance_methods_as_symbols.include?(salt_name)
159
+ attr_writer salt_name unless instance_methods_as_symbols.include?(:"#{salt_name}=")
160
+ end
161
+
162
+ define_method(attribute) do
163
+ instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
164
+ end
165
+
166
+ define_method("#{attribute}=") do |value|
167
+ send("#{encrypted_attribute_name}=", encrypt(attribute, value))
168
+ instance_variable_set("@#{attribute}", value)
169
+ end
170
+
171
+ define_method("#{attribute}?") do
172
+ value = send(attribute)
173
+ value.respond_to?(:empty?) ? !value.empty? : !!value
174
+ end
175
+
176
+ encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
177
+ end
178
+ end
179
+
180
+ alias_method :attr_encryptor, :attr_encrypted
181
+
182
+ # Default options to use with calls to <tt>attr_encrypted</tt>
183
+ #
184
+ # It will inherit existing options from its superclass
185
+ def attr_encrypted_options
186
+ @attr_encrypted_options ||= superclass.attr_encrypted_options.dup
187
+ end
188
+
189
+ def attr_encrypted_default_options
190
+ {
191
+ prefix: 'encrypted_',
192
+ suffix: '',
193
+ if: true,
194
+ unless: false,
195
+ encode: false,
196
+ encode_iv: true,
197
+ encode_salt: true,
198
+ default_encoding: 'm',
199
+ marshal: false,
200
+ marshaler: Marshal,
201
+ dump_method: 'dump',
202
+ load_method: 'load',
203
+ encryptor: Encryptor,
204
+ encrypt_method: 'encrypt',
205
+ decrypt_method: 'decrypt',
206
+ mode: :per_attribute_iv,
207
+ algorithm: 'aes-256-gcm',
208
+ allow_empty_value: false,
209
+ }
210
+ end
211
+
212
+ private :attr_encrypted_default_options
213
+
214
+ # Checks if an attribute is configured with <tt>attr_encrypted</tt>
215
+ #
216
+ # Example
217
+ #
218
+ # class User
219
+ # attr_accessor :name
220
+ # attr_encrypted :email
221
+ # end
222
+ #
223
+ # User.attr_encrypted?(:name) # false
224
+ # User.attr_encrypted?(:email) # true
225
+ def attr_encrypted?(attribute)
226
+ encrypted_attributes.has_key?(attribute.to_sym)
227
+ end
228
+
229
+ # Decrypts a value for the attribute specified
230
+ #
231
+ # Example
232
+ #
233
+ # class User
234
+ # attr_encrypted :email
235
+ # end
236
+ #
237
+ # email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
238
+ def decrypt(attribute, encrypted_value, options = {})
239
+ options = encrypted_attributes[attribute.to_sym].merge(options)
240
+ if options[:if] && !options[:unless] && not_empty?(encrypted_value)
241
+ encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
242
+ value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
243
+ if options[:marshal]
244
+ value = options[:marshaler].send(options[:load_method], value)
245
+ elsif defined?(Encoding)
246
+ encoding = Encoding.default_internal || Encoding.default_external
247
+ value = value.force_encoding(encoding.name)
248
+ end
249
+ value
250
+ else
251
+ encrypted_value
252
+ end
253
+ end
254
+
255
+ # Encrypts a value for the attribute specified
256
+ #
257
+ # Example
258
+ #
259
+ # class User
260
+ # attr_encrypted :email
261
+ # end
262
+ #
263
+ # encrypted_email = User.encrypt(:email, 'test@example.com')
264
+ def encrypt(attribute, value, options = {})
265
+ options = encrypted_attributes[attribute.to_sym].merge(options)
266
+ if options[:if] && !options[:unless] && (options[:allow_empty_value] || not_empty?(value))
267
+ value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
268
+ encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value))
269
+ encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
270
+ encrypted_value
271
+ else
272
+ value
273
+ end
274
+ end
275
+
276
+ def not_empty?(value)
277
+ !value.nil? && !(value.is_a?(String) && value.empty?)
278
+ end
279
+
280
+ # Contains a hash of encrypted attributes with virtual attribute names as keys
281
+ # and their corresponding options as values
282
+ #
283
+ # Example
284
+ #
285
+ # class User
286
+ # attr_encrypted :email, key: 'my secret key'
287
+ # end
288
+ #
289
+ # User.encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
290
+ def encrypted_attributes
291
+ @encrypted_attributes ||= superclass.encrypted_attributes.dup
292
+ end
293
+
294
+ # Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
295
+ # if attribute was configured with attr_encrypted
296
+ #
297
+ # Example
298
+ #
299
+ # class User
300
+ # attr_encrypted :email, key: 'my secret key'
301
+ # end
302
+ #
303
+ # User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
304
+ def method_missing(method, *arguments, &block)
305
+ if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
306
+ send($1, $3, *arguments)
307
+ else
308
+ super
309
+ end
310
+ end
311
+
312
+ module InstanceMethods
313
+ # Decrypts a value for the attribute specified using options evaluated in the current object's scope
314
+ #
315
+ # Example
316
+ #
317
+ # class User
318
+ # attr_accessor :secret_key
319
+ # attr_encrypted :email, key: :secret_key
320
+ #
321
+ # def initialize(secret_key)
322
+ # self.secret_key = secret_key
323
+ # end
324
+ # end
325
+ #
326
+ # @user = User.new('some-secret-key')
327
+ # @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
328
+ def decrypt(attribute, encrypted_value)
329
+ encrypted_attributes[attribute.to_sym][:operation] = :decrypting
330
+ encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
331
+ self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
332
+ end
333
+
334
+ # Encrypts a value for the attribute specified using options evaluated in the current object's scope
335
+ #
336
+ # Example
337
+ #
338
+ # class User
339
+ # attr_accessor :secret_key
340
+ # attr_encrypted :email, key: :secret_key
341
+ #
342
+ # def initialize(secret_key)
343
+ # self.secret_key = secret_key
344
+ # end
345
+ # end
346
+ #
347
+ # @user = User.new('some-secret-key')
348
+ # @user.encrypt(:email, 'test@example.com')
349
+ def encrypt(attribute, value)
350
+ encrypted_attributes[attribute.to_sym][:operation] = :encrypting
351
+ encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value)
352
+ self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
353
+ end
354
+
355
+ # Copies the class level hash of encrypted attributes with virtual attribute names as keys
356
+ # and their corresponding options as values to the instance
357
+ #
358
+ def encrypted_attributes
359
+ @encrypted_attributes ||= begin
360
+ duplicated= {}
361
+ self.class.encrypted_attributes.map { |key, value| duplicated[key] = value.dup }
362
+ duplicated
363
+ end
364
+ end
365
+
366
+ protected
367
+
368
+ # Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
369
+ def evaluated_attr_encrypted_options_for(attribute)
370
+ evaluated_options = Hash.new
371
+ attributes = encrypted_attributes[attribute.to_sym]
372
+ attribute_option_value = attributes[:attribute]
373
+
374
+ [:if, :unless, :value_present, :allow_empty_value].each do |option|
375
+ evaluated_options[option] = evaluate_attr_encrypted_option(attributes[option])
376
+ end
377
+
378
+ evaluated_options[:attribute] = attribute_option_value
379
+
380
+ evaluated_options.tap do |options|
381
+ if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value]
382
+ (attributes.keys - evaluated_options.keys).each do |option|
383
+ options[option] = evaluate_attr_encrypted_option(attributes[option])
384
+ end
385
+
386
+ unless options[:mode] == :single_iv_and_salt
387
+ load_iv_for_attribute(attribute, options)
388
+ end
389
+
390
+ if options[:mode] == :per_attribute_iv_and_salt
391
+ load_salt_for_attribute(attribute, options)
392
+ end
393
+ end
394
+ end
395
+ end
396
+
397
+ # Evaluates symbol (method reference) or proc (responds to call) options
398
+ #
399
+ # If the option is not a symbol or proc then the original option is returned
400
+ def evaluate_attr_encrypted_option(option)
401
+ if option.is_a?(Symbol) && respond_to?(option, true)
402
+ send(option)
403
+ elsif option.respond_to?(:call)
404
+ option.call(self)
405
+ else
406
+ option
407
+ end
408
+ end
409
+
410
+ def load_iv_for_attribute(attribute, options)
411
+ encrypted_attribute_name = options[:attribute]
412
+ encode_iv = options[:encode_iv]
413
+ iv = options[:iv] || send("#{encrypted_attribute_name}_iv")
414
+ if options[:operation] == :encrypting
415
+ begin
416
+ iv = generate_iv(options[:algorithm])
417
+ iv = [iv].pack(encode_iv) if encode_iv
418
+ send("#{encrypted_attribute_name}_iv=", iv)
419
+ rescue RuntimeError
420
+ end
421
+ end
422
+ if iv && !iv.empty?
423
+ iv = iv.unpack(encode_iv).first if encode_iv
424
+ options[:iv] = iv
425
+ end
426
+ end
427
+
428
+ def generate_iv(algorithm)
429
+ algo = OpenSSL::Cipher.new(algorithm)
430
+ algo.encrypt
431
+ algo.random_iv
432
+ end
433
+
434
+ def load_salt_for_attribute(attribute, options)
435
+ encrypted_attribute_name = options[:attribute]
436
+ encode_salt = options[:encode_salt]
437
+ salt = options[:salt] || send("#{encrypted_attribute_name}_salt")
438
+ if options[:operation] == :encrypting
439
+ salt = SecureRandom.random_bytes
440
+ salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt
441
+ send("#{encrypted_attribute_name}_salt=", salt)
442
+ end
443
+ if salt && !salt.empty?
444
+ salt = decode_salt_if_encoded(salt, encode_salt) if encode_salt
445
+ options[:salt] = salt
446
+ end
447
+ end
448
+
449
+ def prefix_and_encode_salt(salt, encoding)
450
+ prefix = '_'
451
+ prefix + [salt].pack(encoding)
452
+ end
453
+
454
+ def decode_salt_if_encoded(salt, encoding)
455
+ prefix = '_'
456
+ salt.slice(0).eql?(prefix) ? salt.slice(1..-1).unpack(encoding).first : salt
457
+ end
458
+ end
459
+
460
+ protected
461
+
462
+ def attribute_instance_methods_as_symbols
463
+ instance_methods.collect { |method| method.to_sym }
464
+ end
465
+
466
+ def attribute_instance_methods_as_symbols_available?
467
+ true
468
+ end
469
+
470
+ end
471
+
472
+
473
+ Dir[File.join(File.dirname(__FILE__), 'attr_encrypted', 'adapters', '*.rb')].each { |adapter| require adapter }