# ideally encrypt and decrypt would happen at the blob/service level # however, there isn't really a great place to define encryption settings there # instead, we encrypt and decrypt at the attachment level, # and we define encryption settings at the model level module Lockbox module ActiveStorageExtensions module Attached protected def encrypted? # could use record_type directly # but record should already be loaded most of the time Utils.encrypted?(record, name) end def encrypt_attachable(attachable) Utils.encrypt_attachable(record, name, attachable) end def rebuild_attachable(attachment) { io: StringIO.new(attachment.download), filename: attachment.filename, content_type: attachment.content_type } end end module AttachedOne if ActiveStorage::VERSION::MAJOR < 6 def attach(attachable) attachable = encrypt_attachable(attachable) if encrypted? super(attachable) end end def rotate_encryption! raise "Not encrypted" unless encrypted? attach(rebuild_attachable(self)) if attached? true end end module AttachedMany if ActiveStorage::VERSION::MAJOR < 6 def attach(*attachables) if encrypted? attachables = attachables.flatten.collect do |attachable| encrypt_attachable(attachable) end end super(attachables) end end def rotate_encryption! raise "Not encrypted" unless encrypted? # must call to_a - do not change previous_attachments = attachments.to_a attachables = previous_attachments.map do |attachment| rebuild_attachable(attachment) end ActiveStorage::Attachment.transaction do attach(attachables) previous_attachments.each(&:purge) end attachments.reload true end end module CreateOne def initialize(name, record, attachable) # this won't encrypt existing blobs attachable = Lockbox::Utils.encrypt_attachable(record, name, attachable) if Lockbox::Utils.encrypted?(record, name) && !attachable.is_a?(ActiveStorage::Blob) super(name, record, attachable) end end module Attachment extend ActiveSupport::Concern def download result = super options = Utils.encrypted_options(record, name) if options result = Utils.build_box(record, options, record.class.table_name, name).decrypt(result) end result end def mark_analyzed if Utils.encrypted_options(record, name) blob.update!(metadata: blob.metadata.merge(analyzed: true)) end end included do after_save :mark_analyzed end end end end