powerhome-attr_encrypted 1.0.1

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.
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 }