lib/symmetric_encryption/cipher.rb in symmetric-encryption-3.7.2 vs lib/symmetric_encryption/cipher.rb in symmetric-encryption-3.8.0

- old
+ new

@@ -16,23 +16,33 @@ # Backward compatibility alias_method :cipher, :cipher_name # Defines the Header Structure returned when parsing the header HeaderStruct = Struct.new( - :compressed, # [true|false] Whether the data is compressed, if supplied in the header - :iv, # [String] IV used to encrypt the data, if supplied in the header - :key, # [String] Key used to encrypt the data, if supplied in the header - :cipher_name, # [String] Name of the cipher used, if supplied in the header - :version, # [Integer] Version of the cipher used, if supplied in the header - :decryption_cipher # [SymmetricEncryption::Cipher] Cipher matching the header, or SymmetricEncryption.cipher(default_version) + # [true|false] Whether the data is compressed, if supplied in the header + :compressed, + # [String] IV used to encrypt the data, if supplied in the header + :iv, + # [String] Key used to encrypt the data, if supplied in the header + :key, + # [String] Name of the cipher used, if supplied in the header + :cipher_name, + # [Integer] Version of the cipher used, if supplied in the header + :version, + # [SymmetricEncryption::Cipher] Cipher matching the header, or SymmetricEncryption.cipher(default_version) + :decryption_cipher ) # Generate a new Symmetric Key pair # # Returns a hash containing a new random symmetric_key pair # consisting of a :key and :iv. # The cipher_name is also included for compatibility with the Cipher initializer + # + # Notes: + # * The key _must_ be properly secured + # * The iv can be stored in the clear and it is not necessary to encrypt it def self.random_key_pair(cipher_name = 'aes-256-cbc') openssl_cipher = ::OpenSSL::Cipher.new(cipher_name) openssl_cipher.encrypt { @@ -40,19 +50,81 @@ 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 = config[: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] + + 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 = config.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 + 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 = config.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 + end + end + # Create a Symmetric::Key for encryption and decryption purposes # # Parameters: # :key [String] # The Symmetric Key to use for encryption and decryption + # Or, + # :key_filename + # Name of file containing 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 # # :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 + # Or, + # :encrypted_iv + # Initialization vector encrypted using the public key from the private_rsa_key + # and then Base64 encoded # # :cipher_name [String] # Optional. Encryption Cipher to use # Default: aes-256-cbc # @@ -82,20 +154,41 @@ # Increases the length of the encrypted data by a few bytes, but makes # 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 def initialize(params={}) params = params.dup - @key = params.delete(:key) - @iv = params.delete(:iv) @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 - raise(ArgumentError, "Missing mandatory parameter :key") unless @key + # To decrypt encrypted key or iv files + 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) + 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 + + raise(ArgumentError, 'Missing mandatory parameter :key, :key_filename, or :encrypted_key') unless @key raise(ArgumentError, "Invalid Encoding: #{@encoding}") unless ENCODINGS.include?(@encoding) 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 @@ -187,20 +280,17 @@ # Now encode data based on encoding setting case encoding when :base64 encoded_string = ::Base64.encode64(binary_string) - # Support Ruby 1.9 encoding - defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string + encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) when :base64strict encoded_string = ::Base64.encode64(binary_string).gsub(/\n/, '') - # Support Ruby 1.9 encoding - defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string + encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) when :base16 encoded_string = binary_string.to_s.unpack('H*').first - # Support Ruby 1.9 encoding - defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string + encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) else binary_string end end @@ -212,16 +302,14 @@ return encoded_string if encoded_string.nil? || (encoded_string == '') case encoding when :base64, :base64strict decoded_string = ::Base64.decode64(encoded_string) - # Support Ruby 1.9 encoding - defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string + decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) when :base16 decoded_string = [encoded_string].pack('H*') - # Support Ruby 1.9 encoding - defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string + decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) else encoded_string end end @@ -275,32 +363,32 @@ # Key in binary form # 2 Byte Cipher Name Length if included # Cipher name it UTF8 text # Remove header and extract flags - _, flags = buffer.slice!(0..MAGIC_HEADER_SIZE+1).unpack(MAGIC_HEADER_UNPACK) - compressed = (flags & 0b1000_0000_0000_0000) != 0 - include_iv = (flags & 0b0100_0000_0000_0000) != 0 - include_key = (flags & 0b0010_0000_0000_0000) != 0 - include_cipher= (flags & 0b0001_0000_0000_0000) != 0 + _, flags = buffer.slice!(0..MAGIC_HEADER_SIZE+1).unpack(MAGIC_HEADER_UNPACK) + compressed = (flags & 0b1000_0000_0000_0000) != 0 + include_iv = (flags & 0b0100_0000_0000_0000) != 0 + include_key = (flags & 0b0010_0000_0000_0000) != 0 + include_cipher = (flags & 0b0001_0000_0000_0000) != 0 # Version of the key to use to decrypt the key if present, # otherwise to decrypt the data following the header - version = flags & 0b0000_0000_1111_1111 + version = flags & 0b0000_0000_1111_1111 decryption_cipher = SymmetricEncryption.cipher(version) raise(SymmetricEncryption::CipherError, "Cipher with version:#{version.inspect} not found in any of the configured SymmetricEncryption ciphers") unless decryption_cipher - iv, key, cipher_name = nil + iv, key, cipher_name = nil if include_iv len = buffer.slice!(0..1).unpack('v').first iv = buffer.slice!(0..len-1) end if include_key len = buffer.slice!(0..1).unpack('v').first key = decryption_cipher.binary_decrypt(buffer.slice!(0..len-1), header=false) end if include_cipher - len = buffer.slice!(0..1).unpack('v').first + len = buffer.slice!(0..1).unpack('v').first cipher_name = buffer.slice!(0..len-1) end HeaderStruct.new(compressed, iv, key, cipher_name, version, decryption_cipher) end @@ -328,18 +416,18 @@ # Default: nil : Exclude cipher_name name from header def self.build_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil) # Ruby V2 named parameters would be perfect here # Version number of supplied encryption key, or use the global cipher version if none was supplied - flags = iv || key ? (SymmetricEncryption.cipher.version || 0) : (version || 0) # Same as 0b0000_0000_0000_0000 + flags = iv || key ? (SymmetricEncryption.cipher.version || 0) : (version || 0) # Same as 0b0000_0000_0000_0000 # If the data is to be compressed before being encrypted, set the # compressed bit in the flags word - flags |= 0b1000_0000_0000_0000 if compressed - flags |= 0b0100_0000_0000_0000 if iv - flags |= 0b0010_0000_0000_0000 if key - flags |= 0b0001_0000_0000_0000 if cipher_name + flags |= 0b1000_0000_0000_0000 if compressed + flags |= 0b0100_0000_0000_0000 if iv + flags |= 0b0010_0000_0000_0000 if key + flags |= 0b0001_0000_0000_0000 if cipher_name header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING) if iv header << [iv.length].pack('v') header << iv end @@ -376,22 +464,23 @@ # Creates a new OpenSSL::Cipher with every call so that this call # is thread-safe openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name) openssl_cipher.encrypt openssl_cipher.key = @key - add_header = always_add_header || random_iv || compress if add_header.nil? - result = if add_header - # Random iv and compress both add the magic header - iv = random_iv ? openssl_cipher.random_iv : @iv - openssl_cipher.iv = iv if iv - # Set the binary indicator on the header if string is Binary Encoded - self.class.build_header(version, compress, random_iv ? iv : nil, nil, nil) + - openssl_cipher.update(compress ? Zlib::Deflate.deflate(string) : string) - else - openssl_cipher.iv = @iv if @iv - openssl_cipher.update(string) - end + add_header = always_add_header || random_iv || compress if add_header.nil? + result = + if add_header + # Random iv and compress both add the magic header + iv = random_iv ? openssl_cipher.random_iv : @iv + openssl_cipher.iv = iv if iv + # Set the binary indicator on the header if string is Binary Encoded + self.class.build_header(version, compress, random_iv ? iv : nil, nil, nil) + + openssl_cipher.update(compress ? Zlib::Deflate.deflate(string) : string) + else + openssl_cipher.iv = @iv if @iv + openssl_cipher.update(string) + end result << openssl_cipher.final end # Advanced use only # See #decrypt to decrypt encoded strings @@ -427,27 +516,27 @@ str = encrypted_string.to_s str.force_encoding(SymmetricEncryption::BINARY_ENCODING) if str.respond_to?(:force_encoding) return str if str.empty? if header || self.class.has_header?(str) - str = str.dup + str = str.dup header ||= self.class.parse_header!(str) openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || self.cipher_name) openssl_cipher.decrypt openssl_cipher.key = header.key || @key - iv = header.iv || @iv - openssl_cipher.iv = iv if iv - result = openssl_cipher.update(str) + iv = header.iv || @iv + openssl_cipher.iv = iv if iv + result = openssl_cipher.update(str) result << openssl_cipher.final header.compressed ? Zlib::Inflate.inflate(result) : result else openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name) openssl_cipher.decrypt openssl_cipher.key = @key - openssl_cipher.iv = @iv if @iv - result = openssl_cipher.update(str) + openssl_cipher.iv = @iv if @iv + result = openssl_cipher.update(str) result << openssl_cipher.final end end # Returns [String] object represented as a string, filtering out the key @@ -456,7 +545,48 @@ end 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: rails generate symmetric_encryption:new_keys\n\n" + end + end + + # Save symmetric key after encrypting it with the private RSA key + # Backing up existing files if present + def write_to_file(file_name, key, rsa) + raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when filename key is used') unless rsa + 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)) + end + end end