lib/symmetric/encryption.rb in symmetric-encryption-0.2.0 vs lib/symmetric/encryption.rb in symmetric-encryption-0.3.0

- old
+ new

@@ -8,240 +8,255 @@ # Encrypt using 256 Bit AES CBC symmetric key and initialization vector # The symmetric key is protected using the private key below and must # be distributed separately from the application class Encryption - # 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 + # Defaults + @@cipher = nil + @@secondary_ciphers = [] + + # Set the Primary Symmetric Cipher to be used + def self.cipher=(cipher) + raise "Cipher must be similar to Symmetric::Ciphers" unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt) && cipher.respond_to?(:encrypted?) + @@cipher = cipher end - # The minimum length for an encrypted string - def self.min_encrypted_length - @@min_encrypted_length ||= encrypt('1').length + # Returns the Primary Symmetric Cipher being used + def self.cipher + @@cipher end - # Returns [true|false] a best effort determination as to whether the supplied - # string is encrypted or not, without incurring the penalty of actually - # decrypting the supplied data - # Parameters: - # encrypted_data: Encrypted string - def self.encrypted?(encrypted_data) - # Simple checks first - return false if (encrypted_data.length < min_encrypted_length) || (!encrypted_data.end_with?("\n")) - # For now have to decrypt it fully - begin - decrypt(encrypted_data) ? true : false - rescue - false + # Set the Secondary Symmetric Ciphers Array to be used + def self.secondary_ciphers=(secondary_ciphers) + raise "secondary_ciphers must be a collection" unless secondary_ciphers.respond_to? :each + secondary_ciphers.each do |cipher| + raise "secondary_ciphers can only consist of Symmetric::Ciphers" unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt) && cipher.respond_to?(:encrypted?) end + @@secondary_ciphers = secondary_ciphers end - # Set the Symmetric Cipher to be used - def self.cipher=(cipher) - @@cipher = cipher + # Returns the Primary Symmetric Cipher being used + def self.secondary_ciphers + @@secondary_ciphers end - # Returns the Symmetric Cipher being used - def self.cipher - @@cipher + # AES Symmetric Decryption of supplied string + # Returns decrypted string + # Returns nil if the supplied str is nil + # Returns "" if it is a string and it is empty + # + # Note: If secondary ciphers are supplied in the configuration file the + # first key will be used to decrypt 'str'. If it fails each cipher in the + # order supplied will be tried. + # It is slow to try each cipher in turn, so should be used during migrations + # only + # + # Raises: OpenSSL::Cipher::CipherError when 'str' was not encrypted using + # the supplied key and iv + # + def self.decrypt(str) + raise "Call Symmetric::Encryption.load! or Symmetric::Encryption.cipher= prior to encrypting or decrypting data" unless @@cipher + begin + @@cipher.decrypt(str) + rescue OpenSSL::Cipher::CipherError => exc + @@secondary_ciphers.each do |cipher| + begin + return cipher.decrypt(str) + rescue OpenSSL::Cipher::CipherError + end + end + raise exc + end end - # Set the Symmetric Key to use for encryption and decryption - def self.key=(key) - @@key = key + # 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 + def self.encrypt(str) + raise "Call Symmetric::Encryption.load! or Symmetric::Encryption.cipher= prior to encrypting or decrypting data" unless @@cipher + @@cipher.encrypt(str) end - # Set the Initialization Vector to use with Symmetric Key - def self.iv=(iv) - @@iv = iv + # Invokes decrypt + # Returns decrypted String + # Return nil if it fails to decrypt a String + # + # Useful for example when decoding passwords encrypted using a key from a + # different environment. I.e. We cannot decode production passwords + # in the test or development environments but still need to be able to load + # YAML config files that contain encrypted development and production passwords + def self.try_decrypt(str) + raise "Call Symmetric::Encryption.load! or Symmetric::Encryption.cipher= prior to encrypting or decrypting data" unless @@cipher + begin + decrypt(str) + rescue OpenSSL::Cipher::CipherError + nil + end end - # Defaults - @@key = nil - @@iv = nil + # Returns [true|false] a best effort determination as to whether the supplied + # string is encrypted or not, without incurring the penalty of actually + # decrypting the supplied data + # Parameters: + # encrypted_data: Encrypted string + def self.encrypted?(encrypted_data) + raise "Call Symmetric::Encryption.load! or Symmetric::Encryption.cipher= prior to encrypting or decrypting data" unless @@cipher + @@cipher.encrypted?(encrypted_data) + end # Load the Encryption Configuration from a YAML file - # filename: Name of file to read. + # filename: + # Name of file to read. # Mandatory for non-Rails apps - # Default: Rails.root/config/symmetry.yml + # 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 = YAML.load_file(filename || File.join(Rails.root, "config", "symmetric-encryption.yml"))[environment || Rails.env] - self.cipher = config['cipher'] || 'aes-256-cbc' - symmetric_key = config['symmetric_key'] - symmetric_iv = config['symmetric_iv'] + config = read_config(filename, environment) - # Hard coded symmetric_key? - if symmetric_key - self.key = symmetric_key - self.iv = symmetric_iv + # Check for hard coded key, iv and cipher + if config[:key] + @@cipher = Cipher.new(config) + @@secondary_ciphers = [] else - load_keys(config['symmetric_key_filename'], config['symmetric_iv_filename'], config['private_rsa_key']) + private_rsa_key = config[:private_rsa_key] + @@cipher, *@@secondary_ciphers = config[:ciphers].collect do |cipher_conf| + cipher_from_encrypted_files( + private_rsa_key, + cipher_conf[:cipher], + cipher_conf[:key_filename], + cipher_conf[:iv_filename]) + end end + true end - # Load the symmetric key to use for encrypting and decrypting data - # Call from environment.rb before calling encrypt or decrypt - # - # private_key: Key used to unlock file containing the actual symmetric key - def self.load_keys(key_filename, iv_filename, private_key) - # Load Encrypted Symmetric keys - encrypted_key = File.read(key_filename) - encrypted_iv = File.read(iv_filename) + # Future: Generate private key in config file generator + #new_key = OpenSSL::PKey::RSA.generate(2048) - # Decrypt Symmetric Key - rsa = OpenSSL::PKey::RSA.new(private_key) - @@key = rsa.private_decrypt(encrypted_key) - @@iv = rsa.private_decrypt(encrypted_iv) - nil - 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 initilization vector .iv # which is encrypted with the above Public key # - # Note: Existing files will be overwritten + # Warning: Existing files will be overwritten def self.generate_symmetric_key_files(filename=nil, environment=nil) - # Temporary: Generate private key manually for now. Will automate soon. - #new_key = OpenSSL::PKey::RSA.generate(2048) + config = read_config(filename, environment) + cipher_cfg = config[:ciphers].first + key_filename = cipher_cfg[:key_filename] + iv_filename = cipher_cfg[:iv_filename] + cipher = cipher_cfg[:cipher] - filename ||= File.join(Rails.root, "config", "symmetric-encryption.yml") - environment ||= (Rails.env || ENV['RAILS']) - config = YAML.load_file(filename)[environment] + raise "The configuration file must contain a 'private_rsa_key' parameter to generate symmetric keys" unless config[:private_rsa_key] + rsa_key = OpenSSL::PKey::RSA.new(config[:private_rsa_key]) - raise "Missing mandatory 'key_filename' for environment:#{environment} in #{filename}" unless key_filename = config['symmetric_key_filename'] - iv_filename = config['symmetric_iv_filename'] - raise "Missing mandatory 'private_key' for environment:#{environment} in #{filename}" unless private_key = config['private_rsa_key'] - rsa_key = OpenSSL::PKey::RSA.new(private_key) + # Generate a new Symmetric Key pair + key_pair = Symmetric::Cipher.random_key_pair(cipher || 'aes-256-cbc', !iv_filename.nil?) - # To ensure compatibility with C openssl code, remove RSA from pub file headers - #File.open(File.join(rsa_keys_path, 'private.key'), 'w') {|file| file.write(new_key.to_pem)} + # Save symmetric key after encrypting it with the private RSA key, backing up existing files if present + File.rename(key_filename, "#{key_filename}.#{Time.now.to_i}") if File.exist?(key_filename) + File.open(key_filename, 'wb') {|file| file.write( rsa_key.public_encrypt(key_pair[:key]) ) } - # Generate Symmetric Key - openssl_cipher = OpenSSL::Cipher::Cipher.new(config['cipher'] || 'aes-256-cbc') - openssl_cipher.encrypt - @@key = openssl_cipher.random_key - @@iv = openssl_cipher.random_iv if iv_filename - - # Save symmetric key after encrypting it with the private asymmetric key - File.open(key_filename, 'wb') {|file| file.write( rsa_key.public_encrypt(@@key) ) } - File.open(iv_filename, 'wb') {|file| file.write( rsa_key.public_encrypt(@@iv) ) } if iv_filename + if iv_filename + File.rename(iv_filename, "#{iv_filename}.#{Time.now.to_i}") if File.exist?(iv_filename) + File.open(iv_filename, 'wb') {|file| file.write( rsa_key.public_encrypt(key_pair[:iv]) ) } + end puts("Generated new Symmetric Key for encryption. Please copy #{key_filename} and #{iv_filename} to the other web servers in #{environment}.") end # Generate a 22 character random password def self.random_password - Base64.encode64(OpenSSL::Cipher::Cipher.new('aes-128-cbc').random_key)[0..-4] + Base64.encode64(OpenSSL::Cipher.new('aes-128-cbc').random_key)[0..-4] end - # AES Symmetric Decryption of supplied string - # Returns decrypted string - # Returns nil if the supplied str is nil - # Returns "" if it is a string and it is empty - def self.decrypt(str) - return str if str.nil? || (str.is_a?(String) && str.empty?) - self.crypt(:decrypt, Base64.decode64(str)) - end + protected - # 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 - def self.encrypt(str) - return str if str.nil? || (str.is_a?(String) && str.empty?) - Base64.encode64(self.crypt(:encrypt, str)) - end - - # Invokes decrypt - # Returns decrypted String - # Return nil if it fails to decrypt a String + # Returns the Encryption Configuration # - # Useful for example when decoding passwords encrypted using a key from a - # different environment. I.e. We cannot decode production passwords - # in the test or development environments but still need to be able to load - # YAML config files that contain encrypted development and production passwords - def self.try_decrypt(str) - self.decrypt(str) rescue nil - end + # Read the configuration from the YAML file and return in the latest format + # + # filename: + # 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. + def self.read_config(filename=nil, environment=nil) + config = YAML.load_file(filename || File.join(Rails.root, "config", "symmetric-encryption.yml"))[environment || Rails.env] - # AES Symmetric Encryption of supplied string - # Returns result as a binary encrypted string - # Returns nil if the supplied str is nil or empty - # Parameters - # compress => Whether to compress the supplied string using zip before - # encrypting - # true | false - # Default false - def self.encrypt_binary(str, compress=false) - return nil if str.nil? || (str.is_a?(String) && str.empty?) - # Bit Layout - # 15 => Compressed? - # 0..14 => Version number of encryption key/algorithm currently 0 - flags = 0 # Same as 0b0000_0000_0000_0000 - # If the data is to be compressed before being encrypted, set the flag and - # compress using zlib. Only compress if data is greater than 15 chars - str = str.to_s unless str.is_a?(String) - if compress && str.length > 15 - flags |= 0b1000_0000_0000_0000 - begin - ostream = StringIO.new - gz = Zlib::GzipWriter.new(ostream) - gz.write(str) - str = ostream.string - ensure - gz.close - end - end - return nil unless encrypted = self.crypt(:encrypt, str) - # Resulting buffer consists of: - # '@EnC' - # unsigned short (32 bits) in little endian format for flags above - # 'actual encrypted buffer data' - "#{MAGIC_HEADER}#{[flags].pack('v')}#{encrypted}" - end + # Default cipher + default_cipher = config['cipher'] || 'aes-256-cbc' + cfg = {} - # AES Symmetric Decryption of supplied Binary string - # Returns decrypted string - # Returns nil if the supplied str is nil - # Returns "" if it is a string and it is empty - def self.decrypt_binary(str) - return str if str.nil? || (str.is_a?(String) && str.empty?) - str = str.to_s unless str.is_a?(String) - encrypted = if str.starts_with? MAGIC_HEADER - # Remove header and extract flags - header, flags = str.unpack(@@unpack ||= "A#{MAGIC_HEADER_SIZE}v") - # Uncompress if data is compressed and remove header - if flags & 0b1000_0000_0000_0000 - begin - gz = Zlib::GzipReader.new(StringIO.new(str[MAGIC_HEADER_SIZE,-1])) - gz.read - ensure - gz.close - end - else - str[MAGIC_HEADER_SIZE,-1] + # Hard coded symmetric_key? - Dev / Testing use only! + if symmetric_key = (config['key'] || config['symmetric_key']) + raise "Symmetric::Encryption Cannot hard code Production encryption keys in #{filename}" if (environment || Rails.env) == 'production' + cfg[:key] = symmetric_key + cfg[:iv] = config['iv'] || config['symmetric_iv'] + cfg[:cipher] = default_cipher + + elsif ciphers = config['ciphers'] + raise "Missing mandatory config parameter 'private_rsa_key'" unless cfg[:private_rsa_key] = config['private_rsa_key'] + + cfg[:ciphers] = ciphers.collect do |cipher_cfg| + key_filename = cipher_cfg['key_filename'] || cipher_cfg['symmetric_key_filename'] + raise "Missing mandatory 'key_filename' for environment:#{environment} in #{filename}" unless key_filename + iv_filename = cipher_cfg['iv_filename'] || cipher_cfg['symmetric_iv_filename'] + { + :cipher => cipher_cfg['cipher'] || default_cipher, + :key_filename => key_filename, + :iv_filename => iv_filename, + } end + else - Base64.decode64(str) + # Migrate old format config + raise "Missing mandatory config parameter 'private_rsa_key'" unless cfg['private_rsa_key'] = config['private_rsa_key'] + cfg[:ciphers] = [ { + :cipher => cipher['cipher'] || default_cipher, + :key_filename => config['symmetric_key_filename'], + :iv_filename => config['symmetric_iv_filename'], + } ] end - self.crypt(:decrypt, encrypted) + + cfg end - protected + # Returns an instance of Symmetric::Cipher initialized from keys + # stored in files + # + # Raises an Exception on failure + # + # Parameters: + # cipher + # Encryption cipher for the symmetric encryption key + # private_key + # Key used to unlock file containing the actual symmetric key + # key_filename + # Name of file containing symmetric key encrypted using the public + # key matching the supplied private_key + # iv_filename + # Optional. Name of file containing symmetric key initialization vector + # encrypted using the public key matching the supplied private_key + def self.cipher_from_encrypted_files(private_rsa_key, cipher, key_filename, iv_filename = nil) + # Load Encrypted Symmetric keys + encrypted_key = File.read(key_filename) + encrypted_iv = File.read(iv_filename) if iv_filename - def self.crypt(cipher_method, string) #:nodoc: - openssl_cipher = OpenSSL::Cipher::Cipher.new(self.cipher) - openssl_cipher.send(cipher_method) - raise "Encryption.key must be set before calling Encryption encrypt or decrypt" unless @@key - openssl_cipher.key = @@key - openssl_cipher.iv = @@iv if @@iv - result = openssl_cipher.update(string.to_s) - result << openssl_cipher.final + # Decrypt Symmetric Keys + rsa = OpenSSL::PKey::RSA.new(private_rsa_key) + iv = rsa.private_decrypt(encrypted_iv) if iv_filename + Cipher.new( + :key => rsa.private_decrypt(encrypted_key), + :iv => iv, + :cipher => cipher + ) end end end \ No newline at end of file