1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
|
module Lockbox
class Utils
def self.build_box(context, options, table, attribute)
# dup options (with except) since keys are sometimes changed or deleted
options = options.except(:attribute, :encrypted_attribute, :migrating, :attached, :type)
options[:encode] = false unless options.key?(:encode)
options.each do |k, v|
if v.respond_to?(:call)
# context not present for pluck
# still possible to use if not dependent on context
options[k] = context ? context.instance_exec(&v) : v.call
elsif v.is_a?(Symbol)
# context not present for pluck
raise Error, "Not available since :#{k} depends on record" unless context
options[k] = context.send(v)
end
end
unless options[:key] || options[:encryption_key] || options[:decryption_key]
options[:key] =
Lockbox.attribute_key(
table: options.delete(:key_table) || table,
attribute: options.delete(:key_attribute) || attribute,
master_key: options.delete(:master_key),
encode: false
)
end
unless options.key?(:previous_versions)
options[:previous_versions] = Lockbox.default_options[:previous_versions]
end
if options[:previous_versions].is_a?(Array)
# dup previous versions array (with map) since elements are updated
# dup each version (with dup) since keys are sometimes deleted
options[:previous_versions] = options[:previous_versions].map(&:dup)
options[:previous_versions].each_with_index do |version, i|
if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && (version[:master_key] || version[:key_table] || version[:key_attribute])
# could also use key_table and key_attribute from options
# when specified, but keep simple for now
# also, this change isn't backward compatible
key =
Lockbox.attribute_key(
table: version.delete(:key_table) || table,
attribute: version.delete(:key_attribute) || attribute,
master_key: version.delete(:master_key),
encode: false
)
options[:previous_versions][i] = version.merge(key: key)
end
end
end
Lockbox.new(**options)
end
def self.encrypted_options(record, name)
record.class.respond_to?(:lockbox_attachments) ? record.class.lockbox_attachments[name.to_sym] : nil
end
def self.decode_key(key, size: 32, name: "Key")
if key.encoding != Encoding::BINARY && key =~ /\A[0-9a-f]{#{size * 2}}\z/i
key = [key].pack("H*")
end
raise Lockbox::Error, "#{name} must be #{size} bytes (#{size * 2} hex digits)" if key.bytesize != size
raise Lockbox::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
key
end
def self.encrypted?(record, name)
!encrypted_options(record, name).nil?
end
def self.encrypt_attachable(record, name, attachable)
io = nil
ActiveSupport::Notifications.instrument("encrypt_file.lockbox", {name: name}) do
options = encrypted_options(record, name)
box = build_box(record, options, record.class.table_name, name)
case attachable
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
io = attachable
attachable = {
io: box.encrypt_io(io),
filename: attachable.original_filename,
content_type: attachable.content_type
}
when Hash
io = attachable[:io]
attachable = attachable.dup
attachable[:io] = box.encrypt_io(io)
else
raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
end
# don't analyze encrypted data
metadata = {"analyzed" => true, "encrypted" => true}
attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
end
# set content type based on unencrypted data
# keep synced with ActiveStorage::Blob#extract_content_type
attachable[:io].extracted_content_type = Marcel::MimeType.for(io, name: attachable[:filename].to_s, declared_type: attachable[:content_type])
attachable
end
def self.decrypt_result(record, name, options, result)
ActiveSupport::Notifications.instrument("decrypt_file.lockbox", {name: name}) do
Utils.build_box(record, options, record.class.table_name, name).decrypt(result)
end
end
def self.rebuild_attachable(attachment)
{
io: StringIO.new(attachment.download),
filename: attachment.filename,
content_type: attachment.content_type
}
end
end
end
|