attr_encrypted 1.3.3 → 3.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.
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
- require 'rake'
2
1
  require 'rake/testtask'
3
- require 'rake/rdoctask'
2
+ require 'rdoc/task'
3
+ require "bundler/gem_tasks"
4
4
 
5
5
  desc 'Test the attr_encrypted gem.'
6
6
  Rake::TestTask.new(:test) do |t|
@@ -18,16 +18,5 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
18
18
  rdoc.rdoc_files.include('lib/**/*.rb')
19
19
  end
20
20
 
21
- if RUBY_VERSION < '1.9.3'
22
- require 'rcov/rcovtask'
23
-
24
- task :rcov do
25
- sh "rcov -o coverage/rcov --exclude '^(?!lib)' " + FileList[ 'test/**/*_test.rb' ].join(' ')
26
- end
27
-
28
- desc 'Default: run unit tests under rcov.'
29
- task :default => :rcov
30
- else
31
- desc 'Default: run unit tests.'
32
- task :default => :test
33
- end
21
+ desc 'Default: run unit tests.'
22
+ 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 = 'attr_encrypted'
11
+ s.version = AttrEncrypted::Version.string
12
+ s.date = Date.today
13
+
14
+ s.summary = 'Encrypt and decrypt attributes'
15
+ s.description = 'Generates attr_accessors that encrypt and decrypt attributes transparently'
16
+
17
+ s.authors = ['Sean Huber', 'S. Brent Faulkner', 'William Monk', 'Stephen Aghaulor']
18
+ s.email = ['seah@shuber.io', 'sbfaulkner@gmail.com', 'billy.monk@gmail.com', 'saghaulor@gmail.com']
19
+ s.homepage = 'http://github.com/attr-encrypted/attr_encrypted'
20
+
21
+ s.has_rdoc = false
22
+ s.rdoc_options = ['--line-numbers', '--inline-source', '--main', 'README.rdoc']
23
+
24
+ s.require_paths = ['lib']
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- test/*`.split("\n")
28
+
29
+ s.required_ruby_version = '>= 2.0.0'
30
+
31
+ s.add_dependency('encryptor', ['~> 3.0.0'])
32
+ # support for testing with specific active record version
33
+ activerecord_version = if ENV.key?('ACTIVERECORD')
34
+ "~> #{ENV['ACTIVERECORD']}"
35
+ else
36
+ '>= 2.0.0'
37
+ end
38
+ s.add_development_dependency('activerecord', activerecord_version)
39
+ s.add_development_dependency('actionpack', activerecord_version)
40
+ s.add_development_dependency('datamapper')
41
+ s.add_development_dependency('rake')
42
+ s.add_development_dependency('minitest')
43
+ s.add_development_dependency('sequel')
44
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby
45
+ s.add_development_dependency('activerecord-jdbcsqlite3-adapter')
46
+ s.add_development_dependency('jdbc-sqlite3', '< 3.8.7') # 3.8.7 is nice and broke
47
+ else
48
+ s.add_development_dependency('sqlite3')
49
+ end
50
+ s.add_development_dependency('dm-sqlite-adapter')
51
+ s.add_development_dependency('simplecov')
52
+ s.add_development_dependency('simplecov-rcov')
53
+ s.add_development_dependency("codeclimate-test-reporter")
54
+
55
+ s.cert_chain = ['certs/saghaulor.pem']
56
+ s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/
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
+ bTAeFw0xNjAxMTEyMjQyMDFaFw0xNzAxMTAyMjQyMDFaMEAxEjAQBgNVBAMMCXNh
5
+ Z2hhdWxvcjEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYD
6
+ Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx0xdQYk2GwCpQ1n/
7
+ n2mPVYHLYqU5TAn/82t5kbqBUWjbcj8tHAi41tJ19+fT/hH0dog8JHvho1zmOr71
8
+ ZIqreJQo60TqP6oE9a5HncUpjqbRp7tOmHo9E+mOW1yT4NiXqFf1YINExQKy2XND
9
+ WPQ+T50ZNUsGMfHFWB4NAymejRWXlOEY3bvKW0UHFeNmouP5he51TjoP8uCc9536
10
+ 4AIWVP/zzzjwrFtC7av7nRw4Y+gX2bQjrkK2k2JS0ejiGzKBIEMJejcI2B+t79zT
11
+ kUQq9SFwp2BrKSIy+4kh4CiF20RT/Hfc1MbvTxSIl/bbIxCYEOhmtHExHi0CoCWs
12
+ YCGCXQIDAQABo3kwdzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU
13
+ SCpVzSBvYbO6B3oT3n3RCZmurG8wHgYDVR0RBBcwFYETc2FnaGF1bG9yQGdtYWls
14
+ LmNvbTAeBgNVHRIEFzAVgRNzYWdoYXVsb3JAZ21haWwuY29tMA0GCSqGSIb3DQEB
15
+ BQUAA4IBAQAeiGdC3e0WiZpm0cF/b7JC6hJYXC9Yv9VsRAWD9ROsLjFKwOhmonnc
16
+ +l/QrmoTjMakYXBCai/Ca3L+k5eRrKilgyITILsmmFxK8sqPJXUw2Jmwk/dAky6x
17
+ hHKVZAofT1OrOOPJ2USoZyhR/VI8epLaD5wUmkVDNqtZWviW+dtRa55aPYjRw5Pj
18
+ wuj9nybhZr+BbEbmZE//2nbfkM4hCuMtxxxilPrJ22aYNmeWU0wsPpDyhPYxOUgU
19
+ ZjeLmnSDiwL6doiP5IiwALH/dcHU67ck3NGf6XyqNwQrrmtPY0mv1WVVL4Uh+vYE
20
+ kHoFzE2no0BfBg78Re8fY69P5yES5ncC
21
+ -----END CERTIFICATE-----
@@ -0,0 +1 @@
1
+ 845fc3cb09a19c3ac76192aba443788f92c880744617bca99b16fd31ce843e07
@@ -0,0 +1 @@
1
+ 81a065442258cc3702aab62c7b2307a48ed3e0deb803600d11a7480cce0db7c43fd9929acd2755081042f8989236553fd694b6cb62776bbfc53f9165a22cbca1
@@ -14,81 +14,107 @@ module AttrEncrypted
14
14
 
15
15
  # Generates attr_accessors that encrypt and decrypt attributes transparently
16
16
  #
17
- # Options (any other options you specify are passed to the encryptor's encrypt and decrypt methods)
18
- #
19
- # :attribute => The name of the referenced encrypted attribute. For example
20
- # <tt>attr_accessor :email, :attribute => :ee</tt> would generate an
21
- # attribute named 'ee' to store the encrypted email. This is useful when defining
22
- # one attribute to encrypt at a time or when the :prefix and :suffix options
23
- # aren't enough. Defaults to nil.
24
- #
25
- # :prefix => A prefix used to generate the name of the referenced encrypted attributes.
26
- # For example <tt>attr_accessor :email, :password, :prefix => 'crypted_'</tt> would
27
- # generate attributes named 'crypted_email' and 'crypted_password' to store the
28
- # encrypted email and password. Defaults to 'encrypted_'.
29
- #
30
- # :suffix => A suffix used to generate the name of the referenced encrypted attributes.
31
- # For example <tt>attr_accessor :email, :password, :prefix => '', :suffix => '_encrypted'</tt>
32
- # would generate attributes named 'email_encrypted' and 'password_encrypted' to store the
33
- # encrypted email. Defaults to ''.
34
- #
35
- # :key => The encryption key. This option may not be required if you're using a custom encryptor. If you pass
36
- # a symbol representing an instance method then the :key option will be replaced with the result of the
37
- # method before being passed to the encryptor. Objects that respond to :call are evaluated as well (including procs).
38
- # Any other key types will be passed directly to the encryptor.
39
- #
40
- # :encode => If set to true, attributes will be encoded as well as encrypted. This is useful if you're
41
- # planning on storing the encrypted attributes in a database. The default encoding is 'm' (base64),
42
- # however this can be overwritten by setting the :encode option to some other encoding string instead of
43
- # just 'true'. See http://www.ruby-doc.org/core/classes/Array.html#M002245 for more encoding directives.
44
- # Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
17
+ # Options (any other options you specify are passed to the Encryptor's encrypt and decrypt methods)
18
+ #
19
+ # attribute: The name of the referenced encrypted attribute. For example
20
+ # <tt>attr_accessor :email, attribute: :ee</tt> would generate an
21
+ # attribute named 'ee' to store the encrypted email. This is useful when defining
22
+ # one attribute to encrypt at a time or when the :prefix and :suffix options
23
+ # aren't enough.
24
+ # Defaults to nil.
25
+ #
26
+ # prefix: A prefix used to generate the name of the referenced encrypted attributes.
27
+ # For example <tt>attr_accessor :email, prefix: 'crypted_'</tt> would
28
+ # generate attributes named 'crypted_email' to store the encrypted
29
+ # email and password.
30
+ # Defaults to 'encrypted_'.
31
+ #
32
+ # suffix: A suffix used to generate the name of the referenced encrypted attributes.
33
+ # For example <tt>attr_accessor :email, prefix: '', suffix: '_encrypted'</tt>
34
+ # would generate attributes named 'email_encrypted' to store the
35
+ # encrypted email.
36
+ # Defaults to ''.
37
+ #
38
+ # key: The encryption key. This option may not be required if
39
+ # you're using a custom encryptor. If you pass a symbol
40
+ # representing an instance method then the :key option
41
+ # will be replaced with the result of the method before
42
+ # being passed to the encryptor. Objects that respond
43
+ # to :call are evaluated as well (including procs).
44
+ # Any other key types will be passed directly to the encryptor.
45
+ # Defaults to nil.
46
+ #
47
+ # encode: If set to true, attributes will be encoded as well as
48
+ # encrypted. This is useful if you're planning on storing
49
+ # the encrypted attributes in a database. The default
50
+ # encoding is 'm' (base64), however this can be overwritten
51
+ # by setting the :encode option to some other encoding
52
+ # string instead of just 'true'. See
53
+ # http://www.ruby-doc.org/core/classes/Array.html#M002245
54
+ # for more encoding directives.
55
+ # Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
56
+ #
57
+ # encode_iv: Defaults to true.
58
+
59
+ # encode_salt: Defaults to true.
45
60
  #
46
- # :default_encoding => Defaults to 'm' (base64).
61
+ # default_encoding: Defaults to 'm' (base64).
47
62
  #
48
- # :marshal => If set to true, attributes will be marshaled as well as encrypted. This is useful if you're planning
49
- # on encrypting something other than a string. Defaults to false unless you're using it with ActiveRecord
50
- # or DataMapper.
63
+ # marshal: If set to true, attributes will be marshaled as well
64
+ # as encrypted. This is useful if you're planning on
65
+ # encrypting something other than a string.
66
+ # Defaults to false.
51
67
  #
52
- # :marshaler => The object to use for marshaling. Defaults to Marshal.
68
+ # marshaler: The object to use for marshaling.
69
+ # Defaults to Marshal.
53
70
  #
54
- # :dump_method => The dump method name to call on the <tt>:marshaler</tt> object to. Defaults to 'dump'.
71
+ # dump_method: The dump method name to call on the <tt>:marshaler</tt> object to.
72
+ # Defaults to 'dump'.
55
73
  #
56
- # :load_method => The load method name to call on the <tt>:marshaler</tt> object. Defaults to 'load'.
74
+ # load_method: The load method name to call on the <tt>:marshaler</tt> object.
75
+ # Defaults to 'load'.
57
76
  #
58
- # :encryptor => The object to use for encrypting. Defaults to Encryptor.
77
+ # encryptor: The object to use for encrypting.
78
+ # Defaults to Encryptor.
59
79
  #
60
- # :encrypt_method => The encrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'encrypt'.
80
+ # encrypt_method: The encrypt method name to call on the <tt>:encryptor</tt> object.
81
+ # Defaults to 'encrypt'.
61
82
  #
62
- # :decrypt_method => The decrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'decrypt'.
83
+ # decrypt_method: The decrypt method name to call on the <tt>:encryptor</tt> object.
84
+ # Defaults to 'decrypt'.
63
85
  #
64
- # :if => Attributes are only encrypted if this option evaluates to true. If you pass a symbol representing an instance
65
- # method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
66
- # Defaults to true.
86
+ # if: Attributes are only encrypted if this option evaluates
87
+ # to true. If you pass a symbol representing an instance
88
+ # method then the result of the method will be evaluated.
89
+ # Any objects that respond to <tt>:call</tt> are evaluated as well.
90
+ # Defaults to true.
67
91
  #
68
- # :unless => Attributes are only encrypted if this option evaluates to false. If you pass a symbol representing an instance
69
- # method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
70
- # Defaults to false.
92
+ # unless: Attributes are only encrypted if this option evaluates
93
+ # to false. If you pass a symbol representing an instance
94
+ # method then the result of the method will be evaluated.
95
+ # Any objects that respond to <tt>:call</tt> are evaluated as well.
96
+ # Defaults to false.
71
97
  #
72
- # :mode => Selects encryption mode for attribute: choose <tt>:single_iv_and_salt</tt> for compatibility
73
- # with the old attr_encrypted API: the default IV and salt of the underlying encryptor object
74
- # is used; <tt>:per_attribute_iv_and_salt</tt> uses a per-attribute IV and salt attribute and
75
- # is the recommended mode for new deployments.
76
- # Defaults to <tt>:single_iv_and_salt</tt>.
98
+ # mode: Selects encryption mode for attribute: choose <tt>:single_iv_and_salt</tt> for compatibility
99
+ # with the old attr_encrypted API: the IV is derived from the encryption key by the underlying Encryptor class; salt is not used.
100
+ # 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.
101
+ # A <tt>:per_attribute_iv</default> mode derives a unique IV per attribute; salt is not used.
102
+ # Defaults to <tt>:per_attribute_iv</tt>.
77
103
  #
78
104
  # You can specify your own default options
79
105
  #
80
106
  # class User
81
- # # now all attributes will be encoded and marshaled by default
82
- # attr_encrypted_options.merge!(:encode => true, :marshal => true, :some_other_option => true)
83
- # attr_encrypted :configuration, :key => 'my secret key'
107
+ # # Now all attributes will be encoded and marshaled by default
108
+ # attr_encrypted_options.merge!(encode: true, marshal: true, some_other_option: true)
109
+ # attr_encrypted :configuration, key: 'my secret key'
84
110
  # end
85
111
  #
86
112
  #
87
113
  # Example
88
114
  #
89
115
  # class User
90
- # attr_encrypted :email, :credit_card, :key => 'some secret key'
91
- # attr_encrypted :configuration, :key => 'some other secret key', :marshal => true
116
+ # attr_encrypted :email, key: 'some secret key'
117
+ # attr_encrypted :configuration, key: 'some other secret key', marshal: true
92
118
  # end
93
119
  #
94
120
  # @user = User.new
@@ -98,46 +124,32 @@ module AttrEncrypted
98
124
  # @user.email? # true
99
125
  # @user.encrypted_email # returns the encrypted version of 'test@example.com'
100
126
  #
101
- # @user.configuration = { :time_zone => 'UTC' }
127
+ # @user.configuration = { time_zone: 'UTC' }
102
128
  # @user.encrypted_configuration # returns the encrypted version of configuration
103
129
  #
104
130
  # See README for more examples
105
131
  def attr_encrypted(*attributes)
106
- options = {
107
- :prefix => 'encrypted_',
108
- :suffix => '',
109
- :if => true,
110
- :unless => false,
111
- :encode => false,
112
- :default_encoding => 'm',
113
- :marshal => false,
114
- :marshaler => Marshal,
115
- :dump_method => 'dump',
116
- :load_method => 'load',
117
- :encryptor => Encryptor,
118
- :encrypt_method => 'encrypt',
119
- :decrypt_method => 'decrypt',
120
- :mode => :single_iv_and_salt
121
- }.merge!(attr_encrypted_options).merge!(attributes.last.is_a?(Hash) ? attributes.pop : {})
132
+ options = attributes.last.is_a?(Hash) ? attributes.pop : {}
133
+ options = attr_encrypted_default_options.dup.merge!(attr_encrypted_options).merge!(options)
122
134
 
123
135
  options[:encode] = options[:default_encoding] if options[:encode] == true
136
+ options[:encode_iv] = options[:default_encoding] if options[:encode_iv] == true
137
+ options[:encode_salt] = options[:default_encoding] if options[:encode_salt] == true
124
138
 
125
139
  attributes.each do |attribute|
126
140
  encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
127
- iv_name = "#{encrypted_attribute_name}_iv".to_sym
128
- salt_name = "#{encrypted_attribute_name}_salt".to_sym
129
141
 
130
- instance_methods_as_symbols = instance_methods.collect { |method| method.to_sym }
142
+ instance_methods_as_symbols = attribute_instance_methods_as_symbols
131
143
  attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
132
144
  attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
133
145
 
134
- if options[:mode] == :per_attribute_iv_and_salt
135
- attr_reader iv_name unless instance_methods_as_symbols.include?(iv_name)
136
- attr_writer iv_name unless instance_methods_as_symbols.include?(:"#{iv_name}=")
146
+ iv_name = "#{encrypted_attribute_name}_iv".to_sym
147
+ attr_reader iv_name unless instance_methods_as_symbols.include?(iv_name)
148
+ attr_writer iv_name unless instance_methods_as_symbols.include?(:"#{iv_name}=")
137
149
 
138
- attr_reader salt_name unless instance_methods_as_symbols.include?(salt_name)
139
- attr_writer salt_name unless instance_methods_as_symbols.include?(:"#{salt_name}=")
140
- end
150
+ salt_name = "#{encrypted_attribute_name}_salt".to_sym
151
+ attr_reader salt_name unless instance_methods_as_symbols.include?(salt_name)
152
+ attr_writer salt_name unless instance_methods_as_symbols.include?(:"#{salt_name}=")
141
153
 
142
154
  define_method(attribute) do
143
155
  instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
@@ -153,9 +165,10 @@ module AttrEncrypted
153
165
  value.respond_to?(:empty?) ? !value.empty? : !!value
154
166
  end
155
167
 
156
- encrypted_attributes[attribute.to_sym] = options.merge(:attribute => encrypted_attribute_name)
168
+ encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
157
169
  end
158
170
  end
171
+
159
172
  alias_method :attr_encryptor, :attr_encrypted
160
173
 
161
174
  # Default options to use with calls to <tt>attr_encrypted</tt>
@@ -165,6 +178,30 @@ module AttrEncrypted
165
178
  @attr_encrypted_options ||= superclass.attr_encrypted_options.dup
166
179
  end
167
180
 
181
+ def attr_encrypted_default_options
182
+ {
183
+ prefix: 'encrypted_',
184
+ suffix: '',
185
+ if: true,
186
+ unless: false,
187
+ encode: false,
188
+ encode_iv: true,
189
+ encode_salt: true,
190
+ default_encoding: 'm',
191
+ marshal: false,
192
+ marshaler: Marshal,
193
+ dump_method: 'dump',
194
+ load_method: 'load',
195
+ encryptor: Encryptor,
196
+ encrypt_method: 'encrypt',
197
+ decrypt_method: 'decrypt',
198
+ mode: :per_attribute_iv,
199
+ algorithm: 'aes-256-gcm',
200
+ }
201
+ end
202
+
203
+ private :attr_encrypted_default_options
204
+
168
205
  # Checks if an attribute is configured with <tt>attr_encrypted</tt>
169
206
  #
170
207
  # Example
@@ -193,7 +230,7 @@ module AttrEncrypted
193
230
  options = encrypted_attributes[attribute.to_sym].merge(options)
194
231
  if options[:if] && !options[:unless] && !encrypted_value.nil? && !(encrypted_value.is_a?(String) && encrypted_value.empty?)
195
232
  encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
196
- value = options[:encryptor].send(options[:decrypt_method], options.merge!(:value => encrypted_value))
233
+ value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
197
234
  if options[:marshal]
198
235
  value = options[:marshaler].send(options[:load_method], value)
199
236
  elsif defined?(Encoding)
@@ -219,7 +256,7 @@ module AttrEncrypted
219
256
  options = encrypted_attributes[attribute.to_sym].merge(options)
220
257
  if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?)
221
258
  value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
222
- encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(:value => value))
259
+ encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value))
223
260
  encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
224
261
  encrypted_value
225
262
  else
@@ -233,10 +270,10 @@ module AttrEncrypted
233
270
  # Example
234
271
  #
235
272
  # class User
236
- # attr_encrypted :email, :key => 'my secret key'
273
+ # attr_encrypted :email, key: 'my secret key'
237
274
  # end
238
275
  #
239
- # User.encrypted_attributes # { :email => { :attribute => 'encrypted_email', :key => 'my secret key' } }
276
+ # User.encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
240
277
  def encrypted_attributes
241
278
  @encrypted_attributes ||= superclass.encrypted_attributes.dup
242
279
  end
@@ -247,7 +284,7 @@ module AttrEncrypted
247
284
  # Example
248
285
  #
249
286
  # class User
250
- # attr_encrypted :email, :key => 'my secret key'
287
+ # attr_encrypted :email, key: 'my secret key'
251
288
  # end
252
289
  #
253
290
  # User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
@@ -266,7 +303,7 @@ module AttrEncrypted
266
303
  #
267
304
  # class User
268
305
  # attr_accessor :secret_key
269
- # attr_encrypted :email, :key => :secret_key
306
+ # attr_encrypted :email, key: :secret_key
270
307
  #
271
308
  # def initialize(secret_key)
272
309
  # self.secret_key = secret_key
@@ -276,6 +313,7 @@ module AttrEncrypted
276
313
  # @user = User.new('some-secret-key')
277
314
  # @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
278
315
  def decrypt(attribute, encrypted_value)
316
+ encrypted_attributes[attribute.to_sym][:operation] = :decrypting
279
317
  self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
280
318
  end
281
319
 
@@ -285,7 +323,7 @@ module AttrEncrypted
285
323
  #
286
324
  # class User
287
325
  # attr_accessor :secret_key
288
- # attr_encrypted :email, :key => :secret_key
326
+ # attr_encrypted :email, key: :secret_key
289
327
  #
290
328
  # def initialize(secret_key)
291
329
  # self.secret_key = secret_key
@@ -295,19 +333,38 @@ module AttrEncrypted
295
333
  # @user = User.new('some-secret-key')
296
334
  # @user.encrypt(:email, 'test@example.com')
297
335
  def encrypt(attribute, value)
336
+ encrypted_attributes[attribute.to_sym][:operation] = :encrypting
298
337
  self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
299
338
  end
300
339
 
340
+ # Copies the class level hash of encrypted attributes with virtual attribute names as keys
341
+ # and their corresponding options as values to the instance
342
+ #
343
+ def encrypted_attributes
344
+ @encrypted_attributes ||= self.class.encrypted_attributes.dup
345
+ end
346
+
301
347
  protected
302
348
 
303
349
  # Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
304
350
  def evaluated_attr_encrypted_options_for(attribute)
305
- if self.class.encrypted_attributes[attribute.to_sym][:mode] == :per_attribute_iv_and_salt
306
- load_iv_for_attribute(attribute, self.class.encrypted_attributes[attribute.to_sym][:algorithm])
307
- load_salt_for_attribute(attribute)
351
+ evaluated_options = Hash.new
352
+ attribute_option_value = encrypted_attributes[attribute.to_sym][:attribute]
353
+ encrypted_attributes[attribute.to_sym].map do |option, value|
354
+ evaluated_options[option] = evaluate_attr_encrypted_option(value)
308
355
  end
309
356
 
310
- self.class.encrypted_attributes[attribute.to_sym].inject({}) { |hash, (option, value)| hash[option] = evaluate_attr_encrypted_option(value); hash }
357
+ evaluated_options[:attribute] = attribute_option_value
358
+
359
+ evaluated_options.tap do |options|
360
+ unless options[:mode] == :single_iv_and_salt
361
+ load_iv_for_attribute(attribute, options)
362
+ end
363
+
364
+ if options[:mode] == :per_attribute_iv_and_salt
365
+ load_salt_for_attribute(attribute, options)
366
+ end
367
+ end
311
368
  end
312
369
 
313
370
  # Evaluates symbol (method reference) or proc (responds to call) options
@@ -323,29 +380,63 @@ module AttrEncrypted
323
380
  end
324
381
  end
325
382
 
326
- def load_iv_for_attribute(attribute, algorithm)
327
- encrypted_attribute_name = self.class.encrypted_attributes[attribute.to_sym][:attribute]
328
- iv = send("#{encrypted_attribute_name}_iv")
329
- if(iv == nil)
383
+ def load_iv_for_attribute(attribute, options)
384
+ encrypted_attribute_name = options[:attribute]
385
+ encode_iv = options[:encode_iv]
386
+ iv = options[:iv] || send("#{encrypted_attribute_name}_iv")
387
+ if options[:operation] == :encrypting
330
388
  begin
331
- algorithm = algorithm || "aes-256-cbc"
332
- algo = OpenSSL::Cipher::Cipher.new(algorithm)
333
- iv = [algo.random_iv].pack("m")
389
+ iv = generate_iv(options[:algorithm])
390
+ iv = [iv].pack(encode_iv) if encode_iv
334
391
  send("#{encrypted_attribute_name}_iv=", iv)
335
392
  rescue RuntimeError
336
393
  end
337
394
  end
338
- self.class.encrypted_attributes[attribute.to_sym] = self.class.encrypted_attributes[attribute.to_sym].merge(:iv => iv.unpack("m").first) if (iv && !iv.empty?)
395
+ if iv && !iv.empty?
396
+ iv = iv.unpack(encode_iv).first if encode_iv
397
+ options[:iv] = iv
398
+ end
399
+ end
400
+
401
+ def generate_iv(algorithm)
402
+ algo = OpenSSL::Cipher.new(algorithm)
403
+ algo.encrypt
404
+ algo.random_iv
405
+ end
406
+
407
+ def load_salt_for_attribute(attribute, options)
408
+ encrypted_attribute_name = options[:attribute]
409
+ encode_salt = options[:encode_salt]
410
+ salt = options[:salt] || send("#{encrypted_attribute_name}_salt")
411
+ if (salt == nil)
412
+ salt = SecureRandom.random_bytes
413
+ salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt
414
+ send("#{encrypted_attribute_name}_salt=", salt)
415
+ end
416
+ if salt && !salt.empty?
417
+ salt = decode_salt_if_encoded(salt, encode_salt) if encode_salt
418
+ options[:salt] = salt
419
+ end
420
+ end
421
+
422
+ def prefix_and_encode_salt(salt, encoding)
423
+ prefix = '_'
424
+ prefix + [salt].pack(encoding)
339
425
  end
340
426
 
341
- def load_salt_for_attribute(attribute)
342
- encrypted_attribute_name = self.class.encrypted_attributes[attribute.to_sym][:attribute]
343
- salt = send("#{encrypted_attribute_name}_salt") || send("#{encrypted_attribute_name}_salt=", Digest::SHA256.hexdigest((Time.now.to_i * rand(1000)).to_s)[0..15])
344
- self.class.encrypted_attributes[attribute.to_sym] = self.class.encrypted_attributes[attribute.to_sym].merge(:salt => salt)
427
+ def decode_salt_if_encoded(salt, encoding)
428
+ prefix = '_'
429
+ salt.slice(0).eql?(prefix) ? salt.slice(1..-1).unpack(encoding).first : salt
345
430
  end
346
431
  end
432
+
433
+ protected
434
+
435
+ def attribute_instance_methods_as_symbols
436
+ instance_methods.collect { |method| method.to_sym }
437
+ end
438
+
347
439
  end
348
440
 
349
- Object.extend AttrEncrypted
350
441
 
351
442
  Dir[File.join(File.dirname(__FILE__), 'attr_encrypted', 'adapters', '*.rb')].each { |adapter| require adapter }