Search code examples
ruby-on-railsamazon-s3storageamazon-kmsshrine

How to decrypt file (encrypted with aws KMS) when downloading from aws S3 via Shrine?


I use Shrine in my Rails app to upload files to Amazon S3. Some of the user's files connected to GDPR compliance and I need to implement client-side encryption ([Shrine documentation] (https://github.com/shrinerb/shrine/blob/v2.16.0/doc/storage/s3.md#encryption)). In Shrine docs you can see information for file encryption via AWS KMS, but no info about decryption.

How can I decrypt the file, when I download it from aws s3?

Here is my code:

config/initializers/shrine.rb - In this place I specify configs for Shrine.

require 'shrine'
require 'shrine/storage/s3'
require 'shrine/storage/file_system'

class Shrine
  plugin :activerecord

  class << self
    Aws::S3::Encryption::Client.extend Forwardable
    Aws::S3::Encryption::Client.delegate(
        [:head_object, :get_object, :config, :build_request] => :client
    )

    def s3_options_encrypted
      {
          prefix:            'encrypted',
          region:            ENV['AWS_REGION'],
          bucket:            ENV['AWS_BUCKET_NAME'] ,
          endpoint:          ENV['AWS_ENDPOINT'],
          access_key_id:     ENV['AWS_ACCESS_KEY_ID'],
          secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
          upload_options:    { acl: 'private' },
          client:            Aws::S3::Encryption::Client.new(kms_key_id: ENV['KMS_KEY_ID'])
      }
    end

    def storages
      {
        cache: Shrine::Storage::FileSystem.new('public', prefix: 'cache_files'),
        encrypted: Shrine::Storage::S3.new(**s3_options_encrypted)
      }
    end
  end
end

models/document.rb - Model

class Document < ApplicationRecord
  include DocumentsUploader::Attachment.new(:file, {store: :encrypted})
end

controllers/downloads_controller.rb - in this place I am downloading file and need to decrypt it.

class DownloadsController < ApplicationController
  def documents
    document = Document.find(id) if Document.exists?(id: params[:id])

    if document.nil? || !document&.file
      redirect_to root_path and return
    end

    temp_file = document.file.download
    name      = "#{document.name}.pdf"
    type      = 'application/pdf'

    send_file temp_file, filename: name, type: type, disposition: 'attachment'
  end
end

Solution

  • The developer of the Shrink framework helped me to solve the problem in this chat

    It was a mistake in the delegation. I delegated [:head_object, :get_object, :config, :build_request] to :client, but I should delegate [:head_object, :delete_object, :config, :build_request]. The correct delegation look like this:

    Aws::S3::Encryption::Client.delegate(
            [:head_object, :delete_object, :config, :build_request] => :client
        )
    

    Aws::S3::Encryption::Client#get_object is already implemented and it's what does the decryption, so adding that #get_object delegate would use the regular Aws::S3::Client#get_object which doesn't do any decryption.

    With this, downloads are automatically decrypted.