require 'openssl' module Eligible # A simple wrapper for the standard OpenSSL library module Encryptor extend self # The default options to use when calling the encrypt and decrypt methods # # Defaults to { algorithm: 'aes-256-gcm', # auth_data: '', # insecure_mode: false, # hmac_iterations: 2000, # v2_gcm_iv: false } # # Run 'openssl list-cipher-commands' in your terminal to view a list all cipher algorithms that are supported on your platform def default_options @default_options ||= { algorithm: 'aes-256-cbc', auth_data: '', insecure_mode: false, hmac_iterations: 2000, v2_gcm_iv: false } end # Encrypts a :value with a specified :key and :iv. # # Optionally accepts :salt, :auth_data, :algorithm, :hmac_iterations, and :insecure_mode options. # # Example # # encrypted_value = Encryptor.encrypt(value: 'some string to encrypt', key: 'some secret key', iv: 'some unique value', salt: 'another unique value') # # or # encrypted_value = Encryptor.encrypt('some string to encrypt', key: 'some secret key', iv: 'some unique value', salt: 'another unique value') def encrypt(*args, &block) crypt :encrypt, *args, &block end # Decrypts a :value with a specified :key and :iv. # # Optionally accepts :salt, :auth_data, :algorithm, :hmac_iterations, and :insecure_mode options. # # Example # # decrypted_value = Encryptor.decrypt(value: 'some encrypted string', key: 'some secret key', iv: 'some unique value', salt: 'another unique value') # # or # decrypted_value = Encryptor.decrypt('some encrypted string', key: 'some secret key', iv: 'some unique value', salt: 'another unique value') def decrypt(*args, &block) crypt :decrypt, *args, &block end protected def crypt(cipher_method, *args) #:nodoc: options = default_options.merge(value: args.first).merge(args.last.is_a?(Hash) ? args.last : {}) raise ArgumentError.new('must specify a key') if options[:key].to_s.empty? cipher = OpenSSL::Cipher.new(options[:algorithm]) cipher.send(cipher_method) unless options[:insecure_mode] raise ArgumentError.new("key must be #{cipher.key_len} bytes or longer") if options[:key].bytesize < cipher.key_len raise ArgumentError.new('must specify an iv') if options[:iv].to_s.empty? raise ArgumentError.new("iv must be #{cipher.iv_len} bytes or longer") if options[:iv].bytesize < cipher.iv_len end if options[:iv] # This is here for backwards compatibility for Encryptor v2.0.0. cipher.iv = options[:iv] if options[:v2_gcm_iv] if options[:salt].nil? # Use a non-salted cipher. # This behaviour is retained for backwards compatibility. This mode # is not secure and new deployments should use the :salt options # wherever possible. cipher.key = options[:key] else # Use an explicit salt (which can be persisted into a database on a # per-column basis, for example). This is the preferred (and more # secure) mode of operation. cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(options[:key], options[:salt], options[:hmac_iterations], cipher.key_len) end cipher.iv = options[:iv] unless options[:v2_gcm_iv] else # This is deprecated and needs to be changed. cipher.pkcs5_keyivgen(options[:key]) end yield cipher, **options if block_given? value = options[:value] if cipher.authenticated? if encryption?(cipher_method) cipher.auth_data = options[:auth_data] else value = extract_cipher_text(options[:value]) cipher.auth_tag = extract_auth_tag(options[:value]) # auth_data must be set after auth_tag has been set when decrypting # See http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html#method-i-auth_data-3D cipher.auth_data = options[:auth_data] end end result = cipher.update(value) result << cipher.final result << cipher.auth_tag if cipher.authenticated? && encryption?(cipher_method) result end def encryption?(cipher_method) cipher_method == :encrypt end def extract_cipher_text(value) value[0..-17] end def extract_auth_tag(value) value[-16..-1] end end end