Encrypting your files with Rails – Part I

This is the first post in a two part series on adding encryption to attachment_fu for Rails applications.

Let’s say that you’re building an application that will be used by a number of different people, and it involves storing information in files, and providing that information to the right person(s) at the right time.

There are choices about where to store the ‘files’ for users – in a database, in a file system (on disk), or in a specialized store (e.g. Amazon S3). For this example, we’re going to choose storing files ‘on disk’ in a file system – locally, across the network, NAS, SAN – wherever, as long as we can ‘see’ the information as a file.

One traditional way to enforce permissions on files is to have the file system itself enforce whether someone can or can’t have access to a file. In the old days, on *nix systems, that meant juggling user and group databases, and working within the ‘user-group-other’ paradigm. More modern attributed file systems make this easier, however that might not work with your web application, because you might not want to synchronize file system attribute information with your web identity information.

You might ultimately decide to control access yourself using some sort of User information DB (homegrown, LDAP, etc.), and explicitly control access by protecting the URLs which download specific files.

As a further measure, you might want to consider encrypting information on a file level. When a file is uploaded, you would generate an encryption key, encrypt the file on disk with that key, store the key separately, and use that key when a file is accessed (as close to the point that the file is downloaded as possible).

For prototype or low-volume applications, let’s look at what it would take to modify attachment_fu and a download controller action to accomplish this, and then extrapolate to what it might take in a higher-performance environment.

Attachment_fu is a nice plug-in to quickly enable file uploads. Plenty of examples are available on how to use it – we’re going to modify it to generate a an encryption password when a file is uploaded, then encrypt that file as it’s stored into the file system.
Here’s the original code for saveto_storage in attachmentfu/backends/filesystembackend.rb.

      #Saves the file to the file system
      def save_to_storage
        if save_attachment?
          # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
          FileUtils.mkdir_p(File.dirname(full_filename))
          File.cp(temp_path, full_filename)
          File.chmod(attachment_options[:chmod] || 0644, full_filename)
        end
        @old_filename = nil
        true
      end

Borrowing some example code from OpenSSL for doing aes encryption (you could use a different method if you like), we’ve modified save_to_storage to accept a key acme, which is used to encrypt the file as it is copied from temporary storage.

      #Saves the file to the file system
      def save_to_storage(acme=nil)
        if save_attachment?
          # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
          FileUtils.mkdir_p(File.dirname(full_filename))
          if acme.nil?
            File.cp(temp_path, full_filename)
          else
            c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
            c.encrypt
            #
            c.key = key = acme
            c.iv = iv = Digest::SHA1.hexdigest("OneFishTwoFish")
            output = File.open(full_filename,'wb')
            File.open(temp_path, 'rb') do |file|
              while buf = file.read(4096)
                output.write(c.update(buf))
              end
              file.close
            end
            output.write(c.final)
            output.close
          end
          File.chmod(attachment_options[:chmod] || 0644, full_filename)
        end
        @old_filename = nil
        true
      end

Where do we get ‘acme’? filesystembackend is called from attachmentfu.rb – afterprocessattachment needs to generate a key, pass it to savetostorage, and also make the key available in the model attachmentfu is mixed-in with.

 # Cleans up after processing.  Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared.
    def after_process_attachment
      if @saved_attachment
        if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil?
          temp_file = temp_path || create_temp_file
          attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) }
        end

        myacme = nil
        if attachment_options[:encrypted_storage]
          myacme = Digest::SHA1.hexdigest(Clock.now.to_i.to_s+rand.to_s)
        end

        save_to_storage(myacme)

        @temp_paths.clear
        @saved_attachment = nil
        write_attribute :acme, myacme
        callback :after_attachment_saved
      end
    end

Note that we’re using Clock.now.toi.tos+rand.to_s as the key… for a real-world example, you would likely additionally seed this with a phrase only known to you.

Your model code should have an attribute named acme, of type string. When your model object is saved, acme will also be updated.

UPDATE: Read Part II

2 thoughts on “Encrypting your files with Rails – Part I

  1. Attachment_fu writes the file to a temp location before it does its final save, which exposes this method to insecurity. Do you have any ideas about encrypting before it's ever written to the disk at all?

  2. Since this post was written, a number of new techniques for handling file uploads have been developed. One that is a favorite of ours is the upload module for NGINX – http://www.grid.net.ru/nginx/upload.en.html The upload module has a number of great features that make it easier to handle files 'by reference' – automatic crc, sha, and md5 generation, etc.

    Since source is available, it is feasible to add a feature to the module to generate a pseudo-random values at each file upload start which is used to (symmetrically) encrypt the file on disk; when the file is completely uploaded, the upstream would be provided with this key value as one of the form fields (see the nginx upload module documentation).

Comments are closed.