Class: Lockbox::Utils

Inherits:
Object
  • Object
show all
Defined in:
lib/lockbox/utils.rb

Class Method Summary collapse

Class Method Details

.build_box(context, options, table, attribute) ⇒ Object



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
# File 'lib/lockbox/utils.rb', line 3

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

.decode_key(key, size: 32, name: "Key") ⇒ Object

Raises:



61
62
63
64
65
66
67
68
69
70
# File 'lib/lockbox/utils.rb', line 61

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

.decrypt_result(record, name, options, result) ⇒ Object



111
112
113
114
115
# File 'lib/lockbox/utils.rb', line 111

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

.encrypt_attachable(record, name, attachable) ⇒ Object



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
# File 'lib/lockbox/utils.rb', line 76

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
     = {"analyzed" => true, "encrypted" => true}
    attachable[:metadata] = (attachable[:metadata] || {}).merge()
  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

.encrypted?(record, name) ⇒ Boolean

Returns:

  • (Boolean)


72
73
74
# File 'lib/lockbox/utils.rb', line 72

def self.encrypted?(record, name)
  !encrypted_options(record, name).nil?
end

.encrypted_options(record, name) ⇒ Object



57
58
59
# File 'lib/lockbox/utils.rb', line 57

def self.encrypted_options(record, name)
  record.class.respond_to?(:lockbox_attachments) ? record.class.lockbox_attachments[name.to_sym] : nil
end

.rebuild_attachable(attachment) ⇒ Object



117
118
119
120
121
122
123
# File 'lib/lockbox/utils.rb', line 117

def self.rebuild_attachable(attachment)
  {
    io: StringIO.new(attachment.download),
    filename: attachment.filename,
    content_type: attachment.content_type
  }
end