diff options
Diffstat (limited to 'vendor/gems/attr_encrypted/lib/attr_encrypted.rb')
-rw-r--r-- | vendor/gems/attr_encrypted/lib/attr_encrypted.rb | 473 |
1 files changed, 473 insertions, 0 deletions
diff --git a/vendor/gems/attr_encrypted/lib/attr_encrypted.rb b/vendor/gems/attr_encrypted/lib/attr_encrypted.rb new file mode 100644 index 00000000000..88e5f65e3c8 --- /dev/null +++ b/vendor/gems/attr_encrypted/lib/attr_encrypted.rb @@ -0,0 +1,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 } |