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 = {}