lib/kms_encrypted/model.rb in kms_encrypted-0.3.0 vs lib/kms_encrypted/model.rb in kms_encrypted-1.0.0

- old
+ new

@@ -1,152 +1,100 @@ module KmsEncrypted module Model - def has_kms_key(legacy_key_id = nil, name: nil, key_id: nil) - key_id ||= legacy_key_id || ENV["KMS_KEY_ID"] + 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 class << self def kms_keys @kms_keys ||= {} end unless respond_to?(:kms_keys) end - kms_keys[key_method.to_sym] = {key_id: key_id} + kms_keys[key_method.to_sym] = { + key_id: key_id, + name: name, + version: version, + previous_versions: previous_versions, + upgrade_context: upgrade_context + } - # same pattern as attr_encrypted reload - if method_defined?(:reload) && kms_keys.size == 1 - alias_method :reload_without_kms_encrypted, :reload - def reload(*args, &block) - result = reload_without_kms_encrypted(*args, &block) - self.class.kms_keys.keys.each do |key_method| - instance_variable_set("@#{key_method}", nil) + 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 - result + 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 + + # same pattern as attr_encrypted reload + if method_defined?(:reload) + alias_method :reload_without_kms_encrypted, :reload + def reload(*args, &block) + result = reload_without_kms_encrypted(*args, &block) + self.class.kms_keys.keys.each do |key_method| + instance_variable_set("@#{key_method}", nil) + end + result + end + end end define_method(key_method) do - raise ArgumentError, "Missing key id" unless key_id - instance_var = "@#{key_method}" unless instance_variable_get(instance_var) - key_column = "encrypted_#{key_method}" - context_method = name ? "kms_encryption_context_#{name}" : "kms_encryption_context" - context = respond_to?(context_method, true) ? send(context_method) : {} - default_encoding = "m" + 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) - unless send(key_column) - plaintext_key = nil - encrypted_key = nil - - event = { - key_id: key_id, - context: context - } - ActiveSupport::Notifications.instrument("generate_data_key.kms_encrypted", event) do - if key_id == "insecure-test-key" - plaintext_key = "00000000000000000000000000000000" - encrypted_key = "insecure-data-key-#{rand(1_000_000_000_000)}" - elsif key_id.start_with?("projects/") - # generate random AES-256 key - plaintext_key = OpenSSL::Random.random_bytes(32) - - # encrypt it - # load client first to ensure namespace is loaded - client = KmsEncrypted.google_client - request = ::Google::Apis::CloudkmsV1::EncryptRequest.new( - plaintext: plaintext_key, - additional_authenticated_data: context.to_json - ) - response = client.encrypt_crypto_key(key_id, request) - key_version = response.name - - # shorten key to save space - short_key_id = Base64.encode64(key_version.split("/").select.with_index { |_, i| i.odd? }.join("/")) - - # build encrypted key - # we reference the key in the field for easy rotation - encrypted_key = "$gc$#{short_key_id}$#{[response.ciphertext].pack(default_encoding)}" - elsif key_id.start_with?("vault/") - # generate random AES-256 key - plaintext_key = OpenSSL::Random.random_bytes(32) - - # encrypt it - response = KmsEncrypted.vault_client.logical.write( - "transit/encrypt/#{key_id.sub("vault/", "")}", - plaintext: Base64.encode64(plaintext_key), - context: Base64.encode64(context.to_json) - ) - - encrypted_key = response.data[:ciphertext] - else - # generate data key from API - resp = KmsEncrypted.aws_client.generate_data_key( - key_id: key_id, - encryption_context: context, - key_spec: "AES_256" - ) - plaintext_key = resp.plaintext - encrypted_key = [resp.ciphertext_blob].pack(default_encoding) + 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 - end - instance_variable_set(instance_var, plaintext_key) - self.send("#{key_column}=", encrypted_key) - end - - unless instance_variable_get(instance_var) - encrypted_key = send(key_column) - plaintext_key = nil - - event = { - key_id: key_id, - context: context - } - ActiveSupport::Notifications.instrument("decrypt_data_key.kms_encrypted", event) do - if encrypted_key.start_with?("insecure-data-key-") - plaintext_key = "00000000000000000000000000000000".encode("BINARY") - elsif encrypted_key.start_with?("$gc$") - _, _, short_key_id, ciphertext = encrypted_key.split("$", 4) - - # restore key, except for cryptoKeyVersion - stored_key_id = Base64.decode64(short_key_id).split("/")[0..3] - stored_key_id.insert(0, "projects") - stored_key_id.insert(2, "locations") - stored_key_id.insert(4, "keyRings") - stored_key_id.insert(6, "cryptoKeys") - stored_key_id = stored_key_id.join("/") - - # load client first to ensure namespace is loaded - client = KmsEncrypted.google_client - request = ::Google::Apis::CloudkmsV1::DecryptRequest.new( - ciphertext: ciphertext.unpack(default_encoding).first, - additional_authenticated_data: context.to_json - ) - plaintext_key = client.decrypt_crypto_key(stored_key_id, request).plaintext - elsif encrypted_key.start_with?("vault:") - response = KmsEncrypted.vault_client.logical.write( - "transit/decrypt/#{key_id.sub("vault/", "")}", - ciphertext: encrypted_key, - context: Base64.encode64(context.to_json) - ) - - plaintext_key = Base64.decode64(response.data[:plaintext]) - else - plaintext_key = KmsEncrypted.aws_client.decrypt( - ciphertext_blob: encrypted_key.unpack(default_encoding).first, - encryption_context: context - ).plaintext + 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 - end - instance_variable_set(instance_var, plaintext_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 = {}