Welcome to mirror list, hosted at ThFree Co, Russian Federation.

attr_encrypted.rb « lib « attr_encrypted « gems « vendor - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 88e5f65e3c81841d2d488c9da3e31260a0afb4b2 (plain)
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 }