Encrypting your files with Rails – Part II
This is the second post in a two part series (part 1 is here) on adding encryption to attachment_fu for Rails applications.
What about making the file available for download? AVOIDING THE ISSUE OF SCALABILITY FOR A MOMENT (since sendfile is not the right way to serve files from Rails), we want to use a variant of sendfile to do the decryption and send the file. Here’s a modified version of send_file that uses an extra hash parameter (acme) to decrypt if provided:
module ActionController
module Streaming
def send_file_x(path, options = {}) #:doc:
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
options[:length] ||= File.size(path)
options[:filename] ||= File.basename(path)
send_file_headers! options
@performed_render = false
logger.warn("Sending file #{path}")
if options[:stream]
render :status => options[:status], :text => Proc.new { |response, output|
logger.info "Streaming file #{path}" unless logger.nil?
len = options[:buffer_size] || 4096
if options[:acme]
c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
c.decrypt
c.key = key = options[:acme]
c.iv = iv = Digest::SHA1.hexdigest("OneFishTwoFish")
end
File.open(path, 'rb') do |file|
while buf = file.read(len)
if options[:acme]
output.write(c.update(buf))
else
output.write(buf)
end
end
end
}
else
logger.info "Sending file #{path}" unless logger.nil?
File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
end
end
end
end
The code could be made more efficient by not performing the options[:acme] test each time a buffer is written. Our controller action that downloads a file would call it like so:
send_file_x(@file_item.stored_filename,
:filename => @file_item.filename,
:type => @file_item.content_type,
:disposition => 'attachment',
:stream => 'true',
:buffer_size => 4096,
:acme => @file_item.acme)
In a production environment, send_file consumes to many server resources – the rails application, and method used to service it (FastCGI, Mongrel, etc.) are tied up serving the file.
It’s more likely the case that the rails application will be behind a reverse proxy like nginx; in that case, a directive is sent to the server to provide the file (usually through an HTTP header). For nginx, serving a non-encrypted, static file would be done by sending a header with the location of a file:
if defined?(NGINX_FOR_DOWNLOAD) && NGINX_FOR_DOWNLOAD
# code omitted – set up file name and path
response.headers['X-Accel-Redirect'] = NGINX_PATH_FOR_ _DOWNLOAD + path
response.headers['Content-Type'] = file_item.content_type
render :nothing=>true;
else
send_file_x(File.join(RAILS_ROOT, FILE_STORAGE_PATH, path_parts, file_item.filename),
:type => file_item.content_type,
:disposition => 'attachment',
:stream => 'true',
:buffer_size => 4096,
:acme => nil,
:encoding => 'utf8',
:filename => URI.encode(file_item.filename))
End
For more information on nginx and rails, learn more about NginxXSendfile.
To perform a similar feat of decrypting and sending a file for nginx, a new module would need to be written for nginx that takes an additional header variable ‘X-Accel-Redirect-Key’ and uses that as the key to send the file, decrypting as it goes.