lib/symmetric_encryption/symmetric_encryption.rb in symmetric-encryption-3.9.1 vs lib/symmetric_encryption/symmetric_encryption.rb in symmetric-encryption-4.0.0.beta3

- old
+ new

@@ -24,11 +24,11 @@ # :datetime => DateTime # :time => Time # :date => Date # :json => Uses JSON serialization, useful for hashes and arrays # :yaml => Uses YAML serialization, useful for hashes and arrays - COERCION_TYPES = [:string, :integer, :float, :decimal, :datetime, :time, :date, :boolean, :json, :yaml] + COERCION_TYPES = [:string, :integer, :float, :decimal, :datetime, :time, :date, :boolean, :json, :yaml] # Set the Primary Symmetric Cipher to be used # # Example: For testing purposes the following test cipher can be used: # @@ -37,19 +37,21 @@ # iv: '1234567890ABCDEF', # cipher: 'aes-128-cbc' # ) def self.cipher=(cipher) raise(ArgumentError, 'Cipher must respond to :encrypt and :decrypt') unless cipher.nil? || (cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt)) + @@cipher = cipher end # Returns the Primary Symmetric Cipher being used # If a version is supplied # Returns the primary cipher if no match was found and version == 0 # Returns nil if no match was found and version != 0 def self.cipher(version = nil) - raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher + raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless cipher? + return @@cipher if version.nil? || (@@cipher.version == version) secondary_ciphers.find { |c| c.version == version } || (@@cipher if version == 0) end # Returns whether a primary cipher has been set @@ -58,10 +60,11 @@ end # Set the Secondary Symmetric Ciphers Array to be used def self.secondary_ciphers=(secondary_ciphers) raise(ArgumentError, 'secondary_ciphers must be a collection') unless secondary_ciphers.respond_to? :each + secondary_ciphers.each do |cipher| raise(ArgumentError, 'secondary_ciphers can only consist of SymmetricEncryption::Ciphers') unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt) end @@secondary_ciphers = secondary_ciphers end @@ -69,21 +72,22 @@ # Returns the Primary Symmetric Cipher being used def self.secondary_ciphers @@secondary_ciphers end - # AES Symmetric Decryption of supplied string - # Returns decrypted value - # Returns nil if the supplied value is nil - # Returns "" if it is a string and it is empty + # Decrypt supplied string. # + # Returns [String] the decrypted string. + # Returns [nil] if the supplied value is nil. + # Returns [''] if it is a string and it is empty. + # # Parameters - # str - # Encrypted string to decrypt - # version + # string [String] + # Encrypted string to decrypt. + # version [Integer] # Specify which cipher version to use if no header is present on the - # encrypted string + # encrypted string. # type [:string|:integer|:float|:decimal|:datetime|:time|:date|:boolean] # If value is set to something other than :string, then the coercible gem # will be use to coerce the unencrypted string value into the specified # type. This assumes that the value was stored using the same type. # Note: If type is set to something other than :string, it's expected @@ -103,37 +107,59 @@ # NOTE: #decrypt will _not_ attempt to use a secondary cipher if it fails # to decrypt the current string. This is because in a very small # yet significant number of cases it is possible to decrypt data using # the incorrect key. Clearly the data returned is garbage, but it still # successfully returns a string of data - def self.decrypt(encrypted_and_encoded_string, version=nil, type=:string) - raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher + def self.decrypt(encrypted_and_encoded_string, version: nil, type: :string) return encrypted_and_encoded_string if encrypted_and_encoded_string.nil? || (encrypted_and_encoded_string == '') - str = encrypted_and_encoded_string.to_s + str = encrypted_and_encoded_string.to_s # Decode before decrypting supplied string - decoded = @@cipher.decode(str) + decoded = cipher.decode(str) return unless decoded return decoded if decoded.empty? + header = Header.new decrypted = - if header = Cipher.parse_header!(decoded) - header.decryption_cipher.binary_decrypt(decoded, header) + if header.parse!(decoded) + header.cipher.binary_decrypt(decoded, header: header) else - # Use cipher_selector if present to decide which cipher to use - c = @@select_cipher.nil? ? cipher(version) : @@select_cipher.call(str, decoded) + c = + if version + # Supplied version takes preference + cipher(version) + elsif @@select_cipher + # Use cipher_selector if present to decide which cipher to use + @@select_cipher.call(str, decoded) + else + # Global cipher + cipher + end c.binary_decrypt(decoded) end # Try to force result to UTF-8 encoding, but if it is not valid, force it back to Binary unless decrypted.force_encoding(SymmetricEncryption::UTF8_ENCODING).valid_encoding? decrypted.force_encoding(SymmetricEncryption::BINARY_ENCODING) end Coerce.coerce_from_string(decrypted, type) end + # Returns the header for the encrypted string + # Returns [nil] if no header is present + def self.header(encrypted_and_encoded_string) + return if encrypted_and_encoded_string.nil? || (encrypted_and_encoded_string == '') + + # Decode before decrypting supplied string + decoded = cipher.encoder.decode(encrypted_and_encoded_string.to_s) + return if decoded.nil? || decoded.empty? + + h = Header.new + h.parse(decoded) == 0 ? nil : h + end + # AES Symmetric Encryption of supplied string # Returns result as a Base64 encoded string # Returns nil if the supplied str is nil # Returns "" if it is a string and it is empty # @@ -172,15 +198,15 @@ # When type is set to :string (the default), uses #to_s to convert # non-string values to string values. # Note: If type is set to something other than :string, it's expected that # the coercible gem is available in the path. # Default: :string - def self.encrypt(str, random_iv=false, compress=false, type=:string) - raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher + def self.encrypt(str, random_iv: false, compress: false, type: :string, header: cipher.always_add_header) + return str if str.nil? || (str == '') # Encrypt and then encode the supplied string - @@cipher.encrypt(Coerce.coerce_to_string(str, type), random_iv, compress) + cipher.encrypt(Coerce.coerce_to_string(str, type), random_iv: random_iv, compress: compress, header: header) end # Invokes decrypt # Returns decrypted String # Return nil if it fails to decrypt a String @@ -191,31 +217,25 @@ # YAML config files that contain encrypted development and production passwords # # WARNING: It is possible to decrypt data using the wrong key, so the value # returned should not be relied upon def self.try_decrypt(str) - raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher - begin - decrypt(str) - rescue OpenSSL::Cipher::CipherError, SymmetricEncryption::CipherError - nil - end + decrypt(str) + rescue OpenSSL::Cipher::CipherError, SymmetricEncryption::CipherError + nil end - # Returns [true|false] as to whether the data could be decrypted - # Parameters: - # encrypted_data: Encrypted string + # Returns [true|false] whether the string is encrypted. # - # WARNING: This method can only be relied upon if the encrypted data includes the - # symmetric encryption header. In some cases data decrypted using the - # wrong key will decrypt and return garbage + # Notes: + # * This method only works reliably when the encrypted data includes the symmetric encryption header. + # * nil and '' are considered "encrypted" so that validations do not blow up on empty values. def self.encrypted?(encrypted_data) - raise(SymmetricEncryption::ConfigError, 'Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data') unless @@cipher + return false if encrypted_data.nil? || (encrypted_data == '') - # For now have to decrypt it fully - result = try_decrypt(encrypted_data) - !(result.nil? || result == '') + @header ||= SymmetricEncryption.cipher.encoded_magic_header + encrypted_data.to_s.start_with?(@header) end # When no header is present in the encrypted data, this custom Block/Proc is # used to determine which cipher to use to decrypt the data. # @@ -244,82 +264,24 @@ def self.select_cipher(&block) @@select_cipher = block ? block : nil end # Load the Encryption Configuration from a YAML file - # filename: + # file_name: # Name of file to read. # Mandatory for non-Rails apps # Default: Rails.root/config/symmetric-encryption.yml # environment: # Which environments config to load. Usually: production, development, etc. # Default: Rails.env - def self.load!(filename = nil, environment = nil) - Config.load!(filename, environment) + def self.load!(file_name = nil, env = nil) + Config.load!(file_name: file_name, env: env) end - # Generate new random symmetric keys for use with this Encryption library - # - # Note: Only the current Encryption key settings are used - # - # Creates Symmetric Key .key and initialization vector .iv - # which is encrypted with the key encryption key. - # - # Existing key files will be renamed if present - def self.generate_symmetric_key_files(filename = nil, environment = nil) - config = Config.read_config(filename, environment) - - # Only regenerating the first configured cipher - cipher_config = config[:ciphers].first - - # Delete unused config keys to generate new random keys - [:version, :always_add_header].each do |key| - cipher_config.delete(key) - end - - key_config = {private_rsa_key: config[:private_rsa_key]} - cipher_cfg = Cipher.generate_random_keys(key_config.merge(cipher_config)) - - puts - if encoded_encrypted_key = cipher_cfg[:encrypted_key] - puts 'If running in Heroku, add the environment specific key:' - puts "heroku config:add #{environment.upcase}_KEY1=#{encoded_encrypted_key}\n" - end - - if encoded_encrypted_iv = cipher_cfg[:encrypted_iv] - puts 'If running in Heroku, add the environment specific key:' - puts "heroku config:add #{environment.upcase}_IV1=#{encoded_encrypted_iv}" - end - - if key = cipher_cfg[:key] - puts "Please add the key: #{key} to your config file" - end - - if iv = cipher_cfg[:iv] - puts "Please add the iv: #{iv} to your config file" - end - - if file_name = cipher_cfg[:key_filename] - puts("Please copy #{file_name} to the other servers in #{environment}.") - end - - if file_name = cipher_cfg[:iv_filename] - puts("Please copy #{file_name} to the other servers in #{environment}.") - end - cipher_cfg - end - - # Generate a 22 character random password - def self.random_password - Base64.encode64(OpenSSL::Cipher.new('aes-128-cbc').random_key)[0..-4].strip - end - - # Binary encrypted data includes this magic header so that we can quickly - # identify binary data versus base64 encoded data that does not have this header - unless defined? MAGIC_HEADER - MAGIC_HEADER = '@EnC' - MAGIC_HEADER_SIZE = MAGIC_HEADER.size - MAGIC_HEADER_UNPACK = "a#{MAGIC_HEADER_SIZE}v" + # Generate a Random password + def self.random_password(size = 22) + require 'securerandom' unless defined?(SecureRandom) + SecureRandom.urlsafe_base64(size) end BINARY_ENCODING = Encoding.find('binary') UTF8_ENCODING = Encoding.find('UTF-8') end