Module: KmsEncrypted::Model

Defined in:
lib/kms_encrypted/model.rb

Instance Method Summary collapse

Instance Method Details

#has_kms_key(name: nil, key_id: nil, eager_encrypt: false, version: 1, previous_versions: nil, upgrade_context: false) ⇒ 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
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/kms_encrypted/model.rb', line 3

def has_kms_key(name: nil, key_id: nil, eager_encrypt: false, version: 1, previous_versions: nil, upgrade_context: false)
  key_id ||= ENV["KMS_KEY_ID"]

  key_method = name ? "kms_key_#{name}" : "kms_key"
  key_column = "encrypted_#{key_method}"
  context_method = name ? "kms_encryption_context_#{name}" : "kms_encryption_context"

  class_eval do
    @kms_keys ||= {}

    unless respond_to?(:kms_keys)
      def self.kms_keys
        parent_keys =
          if superclass.respond_to?(:kms_keys)
            superclass.kms_keys
          else
            {}
          end

        parent_keys.merge(@kms_keys || {})
      end
    end

    @kms_keys[key_method.to_sym] = {
      key_id: key_id,
      name: name,
      version: version,
      previous_versions: previous_versions,
      upgrade_context: upgrade_context
    }

    if @kms_keys.size == 1
      after_save :encrypt_kms_keys

      # fetch all keys together so only need to update database once
      def encrypt_kms_keys
        updates = {}
        self.class.kms_keys.each do |key_method, key|
          instance_var = "@#{key_method}"
          key_column = "encrypted_#{key_method}"
          plaintext_key = instance_variable_get(instance_var)

          if !send(key_column) && plaintext_key
            updates[key_column] = KmsEncrypted::Database.new(self, key_method).encrypt(plaintext_key)
          end
        end
        if updates.any?
          current_time = current_time_from_proper_timezone
          timestamp_attributes_for_update_in_model.each do |attr|
            updates[attr] = current_time
          end
          update_columns(updates)
        end
      end

      if method_defined?(:reload)
        m = Module.new do
          define_method(:reload) do |*args, &block|
            result = super(*args, &block)
            self.class.kms_keys.keys.each do |key_method|
              instance_variable_set("@#{key_method}", nil)
            end
            result
          end
        end
        prepend m
      end
    end

    define_method(key_method) do
      instance_var = "@#{key_method}"

      unless instance_variable_get(instance_var)
        encrypted_key = send(key_column)
        plaintext_key =
          if encrypted_key
            KmsEncrypted::Database.new(self, key_method).decrypt(encrypted_key)
          else
            key = SecureRandom.random_bytes(32)

            if eager_encrypt == :fetch_id
              raise ArgumentError, ":fetch_id only works with Postgres" unless self.class.connection.adapter_name =~ /postg/i
              self.id ||= self.class.connection.execute("select nextval('#{self.class.sequence_name}')").first["nextval"]
            end

            if eager_encrypt == true || ([:try, :fetch_id].include?(eager_encrypt) && id)
              encrypted_key = KmsEncrypted::Database.new(self, key_method).encrypt(key)
              send("#{key_column}=", encrypted_key)
            end

            key
          end
        instance_variable_set(instance_var, plaintext_key)
      end

      instance_variable_get(instance_var)
    end

    define_method(context_method) do
      raise KmsEncrypted::Error, "id needed for encryption context" unless id

      {
        model_name: model_name.to_s,
        model_id: id
      }
    end

    define_method("rotate_#{key_method}!") do
      # decrypt
      plaintext_attributes = {}

      # attr_encrypted
      if self.class.respond_to?(:encrypted_attributes)
        self.class.encrypted_attributes.select { |_, v| v[:key] == key_method.to_sym }.keys.each do |key|
          plaintext_attributes[key] = send(key)
        end
      end

      # lockbox attributes
      if self.class.respond_to?(:lockbox_attributes)
        self.class.lockbox_attributes.select { |_, v| v[:key] == key_method.to_sym }.keys.each do |key|
          plaintext_attributes[key] = send(key)
        end
      end

      # TODO lockbox attachments
      # if self.class.respond_to?(:lockbox_attachments)
      # end

      # reset key
      instance_variable_set("@#{key_method}", nil)
      send("encrypted_#{key_method}=", nil)

      # encrypt again
      plaintext_attributes.each do |attr, value|
        send("#{attr}=", value)
      end

      # update atomically
      save!
    end
  end
end