lib/symmetric_encryption/cipher.rb in symmetric-encryption-3.8.3 vs lib/symmetric_encryption/cipher.rb in symmetric-encryption-3.9.0

- old
+ new

@@ -1,20 +1,18 @@ +require 'openssl' module SymmetricEncryption - # Hold all information related to encryption keys # as well as encrypt and decrypt data using those keys # # Cipher is thread safe so that the same instance can be called by multiple # threads at the same time without needing an instance of Cipher per thread class Cipher # Cipher to use for encryption and decryption - attr_reader :cipher_name, :version, :iv - attr_accessor :encoding, :always_add_header + attr_accessor :cipher_name, :version, :iv, :always_add_header + attr_reader :encoder, :encoding + attr_writer :key - # Available encodings - ENCODINGS = [:none, :base64, :base64strict, :base16] - # Backward compatibility alias_method :cipher, :cipher_name # Defines the Header Structure returned when parsing the header HeaderStruct = Struct.new( @@ -50,52 +48,108 @@ iv: openssl_cipher.random_iv, cipher_name: cipher_name } end - # Generate new randomized keys and generate key and iv files if supplied - # Overwrites key files for the current environment - # See: #initialize for parameters - def self.generate_random_keys(params) - environment = params[:environment] - private_rsa_key = params[:private_rsa_key] - rsa = OpenSSL::PKey::RSA.new(private_rsa_key) if private_rsa_key - key_pair = SymmetricEncryption::Cipher.random_key_pair(params[:cipher_name] || 'aes-256-cbc') - key = key_pair[:key] - iv = key_pair[:iv] + # Generate new randomized keys and generate key and iv files if supplied. + # Overwrites key files for the current environment. + # + # Parameters + # :key_filename + # Name of file that will contain the symmetric key encrypted using the public + # key from the private_rsa_key. + # Or, + # :encrypted_key + # Symmetric key encrypted using the public key from the private_rsa_key + # and then Base64 encoded + # + # Note: + # If :key_filename and :encrypted_key are not supplied then a new :key will be returned. + # :key is the Symmetric Key to use for encryption and decryption. + # + # + # :iv_filename + # Name of file containing symmetric key initialization vector + # encrypted using the public key from the private_rsa_key + # Deprecated: It is _not_ necessary to encrypt the initialization vector (IV) + # Or, + # :encrypted_iv + # Initialization vector encrypted using the public key from the private_rsa_key + # and then Base64 encoded + # Deprecated: It is _not_ necessary to encrypt the initialization vector (IV) + # + # Note: + # If :iv_filename and :encrypted_iv are not supplied then a new :iv will be returned. + # :key is the Initialization Vector to use with Symmetric Key. + # + # + # private_rsa_key [String] + # Key encryption key. + # To generate a new one: SymmetricEncryption::KeyEncryptionKey.generate + # Required if :key_filename, :encrypted_key, :iv_filename, or :encrypted_iv is supplied + # + # :cipher_name [String] + # Encryption Cipher to use. + # Default: aes-256-cbc + # + # :encoding [Symbol] + # :base64strict + # Return as a base64 encoded string that does not include additional newlines + # This is the recommended format since newlines in the values to + # SQL queries are cumbersome. Also the newline reformatting is unnecessary + # It is not the default for backward compatibility + # :base64 + # Return as a base64 encoded string + # :base16 + # Return as a Hex encoded string + # :none + # Return as raw binary data string. Note: String can contain embedded nulls + # Default: :base64strict + def self.generate_random_keys(params = {}) + params = params.dup + private_rsa_key = params.delete(:private_rsa_key) + cipher_name = params.delete(:cipher_name) || 'aes-256-cbc' + encoding = params.delete(:encoding) || :base64strict + unless private_rsa_key + [:key_filename, :encrypted_key, :iv_filename, :encrypted_iv].each do |key| + raise(SymmetricEncryption::ConfigError, "When :#{key} is supplied, :private_rsa_key is required.") if params.include?(key) + end + end - puts 'Generated new Symmetric Key for encryption' - if params.has_key?(:key) - puts 'Put this value in your configuration file for :key' - p key - elsif file_name = params.delete(:key_filename) - write_to_file(file_name, key, rsa) - puts("Please copy #{file_name} to the other servers in #{environment}.") - elsif params.has_key?(:encrypted_key) - encrypted_key = encrypt_key(key, rsa) - puts 'If running in Heroku, add the environment specific key:' - puts "heroku config:add #{environment.upcase}_KEY1=#{encrypted_key}" - puts - puts 'Otherwise, set the :encrypted_key value to:' - puts encrypted_key + key_encryption_key = KeyEncryptionKey.new(private_rsa_key) if private_rsa_key + cipher_conf = {cipher_name: cipher_name, encoding: encoding} + + key_pair = SymmetricEncryption::Cipher.random_key_pair(cipher_name) + key = key_pair[:key] + iv = key_pair[:iv] + + if file_name = params.delete(:key_filename) + cipher_conf[:key_filename] = file_name + encrypted_key = key_encryption_key.encrypt(key) + write_to_file(file_name, encrypted_key) + elsif params.delete(:encrypted_key) + encrypted_key = key_encryption_key.encrypt(key) + cipher_conf[:encrypted_key] = SymmetricEncryption::Encoder[encoding].encode(encrypted_key) + else + params.delete(:key) + cipher_conf[:key] = SymmetricEncryption::Encoder[encoding].encode(key.to_s) end - puts 'Generated new Initialization Vector for encryption' - if params.has_key?(:iv) - puts 'Put this value in your configuration file for :iv' - p iv - elsif file_name = params.delete(:iv_filename) - write_to_file(file_name, iv, rsa) - puts("Please copy #{file_name} to the other servers in #{environment}.") - elsif params.has_key?(:encrypted_iv) - encrypted_iv = encrypt_key(iv, rsa) - puts 'If running in Heroku, add the environment specific key:' - puts "heroku config:add #{environment.upcase}_KEY1=#{encrypted_iv}" - puts - puts 'Otherwise, set the :encrypted_iv value to:' - puts encrypted_iv + if file_name = params.delete(:iv_filename) + cipher_conf[:iv_filename] = file_name + encrypted_iv = key_encryption_key.encrypt(iv) + write_to_file(file_name, encrypted_iv) + elsif params.delete(:encrypted_iv) + encrypted_iv = key_encryption_key.encrypt(iv) + cipher_conf[:encrypted_iv] = SymmetricEncryption::Encoder[encoding].encode(encrypted_iv) + else + params.delete(:iv) + cipher_conf[:iv] = SymmetricEncryption::Encoder[encoding].encode(iv.to_s) end + + raise(ArgumentError, "SymmetricEncryption::Cipher Invalid options #{params.inspect}") if params.size > 0 + cipher_conf end # Create a Symmetric::Key for encryption and decryption purposes # # Parameters: @@ -112,19 +166,19 @@ # # :iv [String] # Optional. The Initialization Vector to use with Symmetric Key # Highly Recommended as it is the input into the CBC algorithm # Or, - # Note: The following 2 options are deprecated since it is _not_ necessary - # to encrypt the initialization vector (IV) # :iv_filename # Name of file containing symmetric key initialization vector # encrypted using the public key from the private_rsa_key + # Deprecated: It is _not_ necessary to encrypt the initialization vector (IV) # Or, # :encrypted_iv # Initialization vector encrypted using the public key from the private_rsa_key # and then Base64 encoded + # Deprecated: It is _not_ necessary to encrypt the initialization vector (IV) # # :cipher_name [String] # Optional. Encryption Cipher to use # Default: aes-256-cbc # @@ -155,45 +209,61 @@ # migration to a new key trivial # Default: false # Recommended: true # # private_rsa_key [String] - # RSA Key used to decrypt key and iv as applicable - # Mandatory if :key_filename, :encrypted_key, :iv_filename, or :encrypted_iv is supplied + # Key encryption key. + # To generate a new one: SymmetricEncryption::KeyEncryptionKey.generate + # Required if :key_filename, :encrypted_key, :iv_filename, or :encrypted_iv is supplied def initialize(params={}) params = params.dup @cipher_name = params.delete(:cipher_name) || params.delete(:cipher) || 'aes-256-cbc' @version = params.delete(:version) @always_add_header = params.delete(:always_add_header) || false - @encoding = (params.delete(:encoding) || :base64).to_sym - - # To decrypt encrypted key or iv files + self.encoding = (params.delete(:encoding) || :base64).to_sym private_rsa_key = params.delete(:private_rsa_key) - rsa = OpenSSL::PKey::RSA.new(private_rsa_key) if private_rsa_key - - if key = params.delete(:key) - @key = key - elsif file_name = params.delete(:key_filename) - @key = read_from_file(file_name, rsa) - elsif encrypted_key = params.delete(:encrypted_key) - @key = decrypt_key(encrypted_key, rsa) + unless private_rsa_key + [:key_filename, :encrypted_key, :iv_filename, :encrypted_iv].each do |key| + raise(SymmetricEncryption::ConfigError, "When :#{key} is supplied, :private_rsa_key is required.") if params.include?(key) + end end - if iv = params.delete(:iv) - @iv = iv - elsif file_name = params.delete(:iv_filename) - @iv = read_from_file(file_name, rsa) - elsif encrypted_iv = params.delete(:encrypted_iv) - @iv = decrypt_key(encrypted_iv, rsa) - end + key_encryption_key = KeyEncryptionKey.new(private_rsa_key) if private_rsa_key + @key = + if key = params.delete(:key) + key + elsif file_name = params.delete(:key_filename) + encrypted_key = self.class.read_from_file(file_name) + key_encryption_key.decrypt(encrypted_key) + elsif encrypted_key = params.delete(:encrypted_key) + binary = self.encoder.decode(encrypted_key) + key_encryption_key.decrypt(binary) + else + raise(ArgumentError, 'Missing mandatory parameter :key, :key_filename, or :encrypted_key') + end - raise(ArgumentError, 'Missing mandatory parameter :key, :key_filename, or :encrypted_key') unless @key - raise(ArgumentError, "Invalid Encoding: #{@encoding}") unless ENCODINGS.include?(@encoding) + @iv = + if iv = params.delete(:iv) + iv + elsif file_name = params.delete(:iv_filename) + encrypted_iv = self.class.read_from_file(file_name) + key_encryption_key.decrypt(encrypted_iv) + elsif encrypted_iv = params.delete(:encrypted_iv) + binary = self.encoder.decode(encrypted_iv) + key_encryption_key.decrypt(binary) + end + raise(ArgumentError, "Cipher version has a valid range of 0 to 255. #{@version} is too high, or negative") if (@version.to_i > 255) || (@version.to_i < 0) raise(ArgumentError, "SymmetricEncryption::Cipher Invalid options #{params.inspect}") if params.size > 0 end + # Change the encoding + def encoding=(encoding) + @encoder = SymmetricEncryption::Encoder[encoding] + @encoding = encoding + end + # Encrypt and then encode a string # # Returns data encrypted and then encoded according to the encoding setting # of this cipher # Returns nil if str is nil @@ -275,44 +345,20 @@ # Note: No encryption or decryption is performed # # Returned string is UTF8 encoded except for encoding :none def encode(binary_string) return binary_string if binary_string.nil? || (binary_string == '') - - # Now encode data based on encoding setting - case encoding - when :base64 - encoded_string = ::Base64.encode64(binary_string) - encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) - when :base64strict - encoded_string = ::Base64.encode64(binary_string).gsub(/\n/, '') - encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) - when :base16 - encoded_string = binary_string.to_s.unpack('H*').first - encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) - else - binary_string - end + encoder.encode(binary_string) end # Decode the supplied string using the encoding in this cipher instance # Note: No encryption or decryption is performed # # Returned string is Binary encoded def decode(encoded_string) return encoded_string if encoded_string.nil? || (encoded_string == '') - - case encoding - when :base64, :base64strict - decoded_string = ::Base64.decode64(encoded_string) - decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) - when :base16 - decoded_string = [encoded_string].pack('H*') - decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) - else - encoded_string - end + encoder.decode(encoded_string) end # Return a new random key using the configured cipher_name # Useful for generating new symmetric keys def random_key @@ -546,47 +592,22 @@ private attr_reader :key - # Read the encrypted key from file - def read_from_file(file_name, rsa) - raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when filename key is used') unless rsa - begin - encrypted_key = File.open(file_name, 'rb') { |f| f.read } - rsa.private_decrypt(encrypted_key) - rescue Errno::ENOENT - puts "\nSymmetric Encryption key file: '#{file_name}' not found or readable." - puts "To generate the keys for the first time run: bin/rails generate symmetric_encryption:new_keys production\n\n" - end + # Read from the file, raising an exception if it is not found + def self.read_from_file(file_name) + File.open(file_name, 'rb') { |f| f.read } + rescue Errno::ENOENT => exc + puts "\nSymmetric Encryption key file: '#{file_name}' not found or readable." + puts "To generate the keys for the first time run: bin/rails generate symmetric_encryption:new_keys production\n\n" + raise(exc) end - # Save symmetric key after encrypting it with the private RSA key - # Backing up existing files if present - def self.write_to_file(file_name, key, rsa) - raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when filename key is used') unless rsa + # Write to the supplied filename, backing up the existing file if present + def self.write_to_file(file_name, data) File.rename(file_name, "#{file_name}.#{Time.now.to_i}") if File.exist?(file_name) - File.open(file_name, 'wb') { |file| file.write(rsa.public_encrypt(key)) } - end - - # Read the encrypted key from file - def decrypt_key(encrypted_key, rsa) - raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when encrypted key is supplied') unless rsa - - # Decode value first using encoding specified - encrypted_key = ::Base64.decode64(encrypted_key) - if !encrypted_key || encrypted_key.empty? - puts "\nSymmetric Encryption encrypted_key not found." - puts "To generate the keys for the first time run: rails generate symmetric_encryption:new_keys\n\n" - else - rsa.private_decrypt(encrypted_key) - end - end - - # Returns [String] encrypted form of supplied key - def encrypt_key(key, rsa) - raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when encrypted key is supplied') unless rsa - ::Base64.encode64(rsa.public_encrypt(key)) + File.open(file_name, 'wb') { |file| file.write(data) } end end end