1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
|
# frozen_string_literal: true
require 'encryptor'
# Adds attr_accessors that encrypt and decrypt an object's attributes
module AttrEncrypted
autoload :Version, 'attr_encrypted/version'
def self.extended(base) # :nodoc:
base.class_eval do
include InstanceMethods
attr_writer :attr_encrypted_options
@attr_encrypted_options, @encrypted_attributes = {}, {}
end
end
# Generates attr_accessors that encrypt and decrypt attributes transparently
#
# Options (any other options you specify are passed to the Encryptor's encrypt and decrypt methods)
#
# attribute: The name of the referenced encrypted attribute. For example
# <tt>attr_accessor :email, attribute: :ee</tt> would generate an
# attribute named 'ee' to store the encrypted email. This is useful when defining
# one attribute to encrypt at a time or when the :prefix and :suffix options
# aren't enough.
# Defaults to nil.
#
# prefix: A prefix used to generate the name of the referenced encrypted attributes.
# For example <tt>attr_accessor :email, prefix: 'crypted_'</tt> would
# generate attributes named 'crypted_email' to store the encrypted
# email and password.
# Defaults to 'encrypted_'.
#
# suffix: A suffix used to generate the name of the referenced encrypted attributes.
# For example <tt>attr_accessor :email, prefix: '', suffix: '_encrypted'</tt>
# would generate attributes named 'email_encrypted' to store the
# encrypted email.
# Defaults to ''.
#
# key: The encryption key. This option may not be required if
# you're using a custom encryptor. If you pass a symbol
# representing an instance method then the :key option
# will be replaced with the result of the method before
# being passed to the encryptor. Objects that respond
# to :call are evaluated as well (including procs).
# Any other key types will be passed directly to the encryptor.
# Defaults to nil.
#
# encode: If set to true, attributes will be encoded as well as
# encrypted. This is useful if you're planning on storing
# the encrypted attributes in a database. The default
# encoding is 'm' (base64), however this can be overwritten
# by setting the :encode option to some other encoding
# string instead of just 'true'. See
# http://www.ruby-doc.org/core/classes/Array.html#M002245
# for more encoding directives.
# Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
#
# encode_iv: Defaults to true.
# encode_salt: Defaults to true.
#
# default_encoding: Defaults to 'm' (base64).
#
# marshal: If set to true, attributes will be marshaled as well
# as encrypted. This is useful if you're planning on
# encrypting something other than a string.
# Defaults to false.
#
# marshaler: The object to use for marshaling.
# Defaults to Marshal.
#
# dump_method: The dump method name to call on the <tt>:marshaler</tt> object to.
# Defaults to 'dump'.
#
# load_method: The load method name to call on the <tt>:marshaler</tt> object.
# Defaults to 'load'.
#
# encryptor: The object to use for encrypting.
# Defaults to Encryptor.
#
# encrypt_method: The encrypt method name to call on the <tt>:encryptor</tt> object.
# Defaults to 'encrypt'.
#
# decrypt_method: The decrypt method name to call on the <tt>:encryptor</tt> object.
# Defaults to 'decrypt'.
#
# if: Attributes are only encrypted if this option evaluates
# to true. If you pass a symbol representing an instance
# method then the result of the method will be evaluated.
# Any objects that respond to <tt>:call</tt> are evaluated as well.
# Defaults to true.
#
# unless: Attributes are only encrypted if this option evaluates
# to false. If you pass a symbol representing an instance
# method then the result of the method will be evaluated.
# Any objects that respond to <tt>:call</tt> are evaluated as well.
# Defaults to false.
#
# mode: Selects encryption mode for attribute: choose <tt>:single_iv_and_salt</tt> for compatibility
# with the old attr_encrypted API: the IV is derived from the encryption key by the underlying Encryptor class; salt is not used.
# 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.
# A <tt>:per_attribute_iv</default> mode derives a unique IV per attribute; salt is not used.
# Defaults to <tt>:per_attribute_iv</tt>.
#
# allow_empty_value: Attributes which have nil or empty string values will not be encrypted unless this option
# has a truthy value.
#
# You can specify your own default options
#
# class User
# # Now all attributes will be encoded and marshaled by default
# attr_encrypted_options.merge!(encode: true, marshal: true, some_other_option: true)
# attr_encrypted :configuration, key: 'my secret key'
# end
#
#
# Example
#
# class User
# attr_encrypted :email, key: 'some secret key'
# attr_encrypted :configuration, key: 'some other secret key', marshal: true
# end
#
# @user = User.new
# @user.encrypted_email # nil
# @user.email? # false
# @user.email = 'test@example.com'
# @user.email? # true
# @user.encrypted_email # returns the encrypted version of 'test@example.com'
#
# @user.configuration = { time_zone: 'UTC' }
# @user.encrypted_configuration # returns the encrypted version of configuration
#
# See README for more examples
def attr_encrypted(*attributes)
options = attributes.last.is_a?(Hash) ? attributes.pop : {}
options = attr_encrypted_default_options.dup.merge!(attr_encrypted_options).merge!(options)
options[:encode] = options[:default_encoding] if options[:encode] == true
options[:encode_iv] = options[:default_encoding] if options[:encode_iv] == true
options[:encode_salt] = options[:default_encoding] if options[:encode_salt] == true
attributes.each do |attribute|
encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
instance_methods_as_symbols = attribute_instance_methods_as_symbols
if attribute_instance_methods_as_symbols_available?
attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
iv_name = "#{encrypted_attribute_name}_iv".to_sym
attr_reader iv_name unless instance_methods_as_symbols.include?(iv_name)
attr_writer iv_name unless instance_methods_as_symbols.include?(:"#{iv_name}=")
salt_name = "#{encrypted_attribute_name}_salt".to_sym
attr_reader salt_name unless instance_methods_as_symbols.include?(salt_name)
attr_writer salt_name unless instance_methods_as_symbols.include?(:"#{salt_name}=")
end
define_method(attribute) do
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
end
define_method("#{attribute}=") do |value|
send("#{encrypted_attribute_name}=", encrypt(attribute, value))
instance_variable_set("@#{attribute}", value)
end
define_method("#{attribute}?") do
value = send(attribute)
value.respond_to?(:empty?) ? !value.empty? : !!value
end
encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
end
end
alias_method :attr_encryptor, :attr_encrypted
# Default options to use with calls to <tt>attr_encrypted</tt>
#
# It will inherit existing options from its superclass
def attr_encrypted_options
@attr_encrypted_options ||= superclass.attr_encrypted_options.dup
end
def attr_encrypted_default_options
{
prefix: 'encrypted_',
suffix: '',
if: true,
unless: false,
encode: false,
encode_iv: true,
encode_salt: true,
default_encoding: 'm',
marshal: false,
marshaler: Marshal,
dump_method: 'dump',
load_method: 'load',
encryptor: Encryptor,
encrypt_method: 'encrypt',
decrypt_method: 'decrypt',
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
allow_empty_value: false,
}
end
private :attr_encrypted_default_options
# Checks if an attribute is configured with <tt>attr_encrypted</tt>
#
# Example
#
# class User
# attr_accessor :name
# attr_encrypted :email
# end
#
# User.attr_encrypted?(:name) # false
# User.attr_encrypted?(:email) # true
def attr_encrypted?(attribute)
encrypted_attributes.has_key?(attribute.to_sym)
end
# Decrypts a value for the attribute specified
#
# Example
#
# class User
# attr_encrypted :email
# end
#
# email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
def decrypt(attribute, encrypted_value, options = {})
options = encrypted_attributes[attribute.to_sym].merge(options)
if options[:if] && !options[:unless] && not_empty?(encrypted_value)
encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
if options[:marshal]
value = options[:marshaler].send(options[:load_method], value)
elsif defined?(Encoding)
encoding = Encoding.default_internal || Encoding.default_external
value = value.force_encoding(encoding.name)
end
value
else
encrypted_value
end
end
# Encrypts a value for the attribute specified
#
# Example
#
# class User
# attr_encrypted :email
# end
#
# encrypted_email = User.encrypt(:email, 'test@example.com')
def encrypt(attribute, value, options = {})
options = encrypted_attributes[attribute.to_sym].merge(options)
if options[:if] && !options[:unless] && (options[:allow_empty_value] || not_empty?(value))
value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value))
encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
encrypted_value
else
value
end
end
def not_empty?(value)
!value.nil? && !(value.is_a?(String) && value.empty?)
end
# Contains a hash of encrypted attributes with virtual attribute names as keys
# and their corresponding options as values
#
# Example
#
# class User
# attr_encrypted :email, key: 'my secret key'
# end
#
# User.encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
def encrypted_attributes
@encrypted_attributes ||= superclass.encrypted_attributes.dup
end
# Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
# if attribute was configured with attr_encrypted
#
# Example
#
# class User
# attr_encrypted :email, key: 'my secret key'
# end
#
# User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
def method_missing(method, *arguments, &block)
if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
send($1, $3, *arguments)
else
super
end
end
module InstanceMethods
# Decrypts a value for the attribute specified using options evaluated in the current object's scope
#
# Example
#
# class User
# attr_accessor :secret_key
# attr_encrypted :email, key: :secret_key
#
# def initialize(secret_key)
# self.secret_key = secret_key
# end
# end
#
# @user = User.new('some-secret-key')
# @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
def decrypt(attribute, encrypted_value)
encrypted_attributes[attribute.to_sym][:operation] = :decrypting
encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
end
# Encrypts a value for the attribute specified using options evaluated in the current object's scope
#
# Example
#
# class User
# attr_accessor :secret_key
# attr_encrypted :email, key: :secret_key
#
# def initialize(secret_key)
# self.secret_key = secret_key
# end
# end
#
# @user = User.new('some-secret-key')
# @user.encrypt(:email, 'test@example.com')
def encrypt(attribute, value)
encrypted_attributes[attribute.to_sym][:operation] = :encrypting
encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value)
self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
end
# Copies the class level hash of encrypted attributes with virtual attribute names as keys
# and their corresponding options as values to the instance
#
def encrypted_attributes
@encrypted_attributes ||= begin
duplicated= {}
self.class.encrypted_attributes.map { |key, value| duplicated[key] = value.dup }
duplicated
end
end
protected
# Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
def evaluated_attr_encrypted_options_for(attribute)
evaluated_options = Hash.new
attributes = encrypted_attributes[attribute.to_sym]
attribute_option_value = attributes[:attribute]
[:if, :unless, :value_present, :allow_empty_value].each do |option|
evaluated_options[option] = evaluate_attr_encrypted_option(attributes[option])
end
evaluated_options[:attribute] = attribute_option_value
evaluated_options.tap do |options|
if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value]
(attributes.keys - evaluated_options.keys).each do |option|
options[option] = evaluate_attr_encrypted_option(attributes[option])
end
unless options[:mode] == :single_iv_and_salt
load_iv_for_attribute(attribute, options)
end
if options[:mode] == :per_attribute_iv_and_salt
load_salt_for_attribute(attribute, options)
end
end
end
end
# Evaluates symbol (method reference) or proc (responds to call) options
#
# If the option is not a symbol or proc then the original option is returned
def evaluate_attr_encrypted_option(option)
if option.is_a?(Symbol) && respond_to?(option, true)
send(option)
elsif option.respond_to?(:call)
option.call(self)
else
option
end
end
def load_iv_for_attribute(attribute, options)
encrypted_attribute_name = options[:attribute]
encode_iv = options[:encode_iv]
iv = options[:iv] || send("#{encrypted_attribute_name}_iv")
if options[:operation] == :encrypting
begin
iv = generate_iv(options[:algorithm])
iv = [iv].pack(encode_iv) if encode_iv
send("#{encrypted_attribute_name}_iv=", iv)
rescue RuntimeError
end
end
if iv && !iv.empty?
iv = iv.unpack(encode_iv).first if encode_iv
options[:iv] = iv
end
end
def generate_iv(algorithm)
algo = OpenSSL::Cipher.new(algorithm)
algo.encrypt
algo.random_iv
end
def load_salt_for_attribute(attribute, options)
encrypted_attribute_name = options[:attribute]
encode_salt = options[:encode_salt]
salt = options[:salt] || send("#{encrypted_attribute_name}_salt")
if options[:operation] == :encrypting
salt = SecureRandom.random_bytes
salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt
send("#{encrypted_attribute_name}_salt=", salt)
end
if salt && !salt.empty?
salt = decode_salt_if_encoded(salt, encode_salt) if encode_salt
options[:salt] = salt
end
end
def prefix_and_encode_salt(salt, encoding)
prefix = '_'
prefix + [salt].pack(encoding)
end
def decode_salt_if_encoded(salt, encoding)
prefix = '_'
salt.slice(0).eql?(prefix) ? salt.slice(1..-1).unpack(encoding).first : salt
end
end
protected
def attribute_instance_methods_as_symbols
instance_methods.collect { |method| method.to_sym }
end
def attribute_instance_methods_as_symbols_available?
true
end
end
Dir[File.join(File.dirname(__FILE__), 'attr_encrypted', 'adapters', '*.rb')].each { |adapter| require adapter }
|