diff --git a/docs/tutorials/automatic-encryption.txt b/docs/tutorials/automatic-encryption.txt index 6b3af35f2e..c7d5f91500 100644 --- a/docs/tutorials/automatic-encryption.txt +++ b/docs/tutorials/automatic-encryption.txt @@ -181,6 +181,15 @@ You can create multiple DEKs, if necessary. % rake db:mongoid:encryption:create_data_key Created data key with id: 'Vxr5m+5cQISjDOruzZgE0w==' for kms provider: 'local' in key vault: 'encryption.__keyVault'. +You can also provide an alternate name for the DEK. This allows you to reference +the DEK by name when configuring encryption for your fields. It also allows you +to dynamically assign a DEK to a field at runtime. + +.. code-block:: sh + + % rake db:mongoid:encryption:create_data_key -- --key-alt-name=my_data_key + Created data key with id: 'yjF8hKmKQsqGeFGXlB9Sow==' with key alt name: 'my_data_key' for kms provider: 'local' in key vault: 'encryption.__keyVault'. + Configure Encryption Schema =========================== @@ -226,9 +235,11 @@ Now we can tell Mongoid what should be encrypted: field :insurer, type: String # This field is encrypted using AEAD_AES_256_CBC_HMAC_SHA_512-Random - # algorithm using the same data key as Patient class attributes. + # algorithm using the key which alternate name is stored in the + # policy_number_key field. field :policy_number, type: Integer, encrypt: { - deterministic: false + deterministic: false, + key_field_name: :policy_number_key } embedded_in :patient @@ -254,7 +265,7 @@ according to the configuration: passport_id: '123456', blood_type: 'AB+', ssn: 98765, - insurance: Insurance.new(insurer: 'TK', policy_number: 123456) + insurance: Insurance.new(insurer: 'TK', policy_number: 123456, policy_number_key: 'my_data_key') ) # Fields are encrypted in the database @@ -265,7 +276,7 @@ according to the configuration: # "passport_id"=>, # "blood_type"=>, # "ssn"=>, - # "insurance"=>{"_id"=>BSON::ObjectId('6446a1d046ebfd701f9f4293'), "insurer"=>"TK", "policy_number"=>}} + # "insurance"=>{"_id"=>BSON::ObjectId('6446a1d046ebfd701f9f4293'), "insurer"=>"TK", "policy_number"=>}, "policy_number_key"=>"my_data_key"} Fields encrypted using a deterministic algorithm can be queried. Only exact match queries are supported. For more details please consult `the server documentation diff --git a/lib/mongoid/config/encryption.rb b/lib/mongoid/config/encryption.rb index 314e2f5622..6a77ba49b8 100644 --- a/lib/mongoid/config/encryption.rb +++ b/lib/mongoid/config/encryption.rb @@ -73,7 +73,7 @@ def encryption_schema_map(default_database, models = ::Mongoid.models) # @return [ Hash ] The encryptMetadata object. def metadata_for(model) metadata = {}.tap do |metadata| - if (key_id = key_id_for(model.encrypt_metadata[:key_id])) + if (key_id = key_id_for(model.encrypt_metadata[:key_id], model.encrypt_metadata[:key_name_field])) metadata['keyId'] = key_id end if model.encrypt_metadata.key?(:deterministic) @@ -129,7 +129,7 @@ def properties_for_fields(model) if (algorithm = algorithm_for(field)) props[name]['encrypt']['algorithm'] = algorithm end - if (key_id = key_id_for(field.key_id)) + if (key_id = key_id_for(field.key_id, field.key_name_field)) props[name]['encrypt']['keyId'] = key_id end end @@ -192,13 +192,22 @@ def algorithm_for(field) # key id. # # @param [ String | nil ] key_id_base64 The base64 encoded key id. - # - # @return [ Array | nil ] The keyId encryption schema field, - # or nil if the key id is nil. - def key_id_for(key_id_base64) - return nil if key_id_base64.nil? + # @param [ String | nil ] key_name_field The name of the key name field. + # + # @return [ Array | String | nil ] The keyId encryption schema field, + # JSON pointer to the field that contains keyAltName, + # or nil if both key_id_base64 and key_name_field are nil. + def key_id_for(key_id_base64, key_name_field) + return nil if key_id_base64.nil? && key_name_field.nil? + if !key_id_base64.nil? && !key_name_field.nil? + raise ArgumentError, 'Specifying both key_id and key_name_field is not allowed' + end - [ BSON::Binary.new(Base64.decode64(key_id_base64), :uuid) ] + if key_id_base64.nil? + "/#{key_name_field}" + else + [ BSON::Binary.new(Base64.decode64(key_id_base64), :uuid) ] + end end end end diff --git a/lib/mongoid/fields/encrypted.rb b/lib/mongoid/fields/encrypted.rb index 29d54273e1..d33c388938 100644 --- a/lib/mongoid/fields/encrypted.rb +++ b/lib/mongoid/fields/encrypted.rb @@ -26,6 +26,12 @@ def key_id @encryption_options[:key_id] end + # @return [ String | nil ] The name of the field that contains the + # key alt name to use for encryption; if not specified, nil is returned. + def key_name_field + @encryption_options[:key_name_field] + end + # Override the key_id for the field. # # This method is solely for testing purposes and should not be used in diff --git a/lib/mongoid/tasks/encryption.rake b/lib/mongoid/tasks/encryption.rake index 5cff3894af..2f9cf8cdfd 100644 --- a/lib/mongoid/tasks/encryption.rake +++ b/lib/mongoid/tasks/encryption.rake @@ -1,20 +1,42 @@ # frozen_string_literal: true -# rubocop:todo all +require 'optparse' + +# rubocop:disable Metrics/BlockLength namespace :db do namespace :mongoid do namespace :encryption do - - desc "Create encryption key" - task :create_data_key, [:client, :provider] => [:environment] do |_t, args| - result = ::Mongoid::Tasks::Encryption.create_data_key( - client_name: args[:client], - kms_provider_name: args[:provider] + desc 'Create encryption key' + task create_data_key: [ :environment ] do + options = {} + parser = OptionParser.new do |opts| + opts.on('-c', '--client CLIENT', 'Name of the client to use') do |v| + options[:client_name] = v + end + opts.on('-p', '--provider PROVIDER', 'KMS provider to use') do |v| + options[:kms_provider_name] = v + end + opts.on('-n', '--key-alt-name KEY_ALT_NAME', 'Alternate name for the key') do |v| + options[:key_alt_name] = v + end + end + # rubocop:disable Lint/EmptyBlock + parser.parse!(parser.order!(ARGV) {}) + # rubocop:enable Lint/EmptyBlock + result = Mongoid::Tasks::Encryption.create_data_key( + client_name: options[:client_name], + kms_provider_name: options[:kms_provider_name], + key_alt_name: options[:key_alt_name] ) - puts "Created data key with id: '#{result[:key_id]}' " + - "for kms provider: '#{result[:kms_provider]}' " + - "in key vault: '#{result[:key_vault_namespace]}'." + output = [].tap do |lines| + lines << "Created data key with id: '#{result[:key_id]}'" + lines << "with key alt name: '#{result[:key_alt_name]}'" if result[:key_alt_name] + lines << "for kms provider: '#{result[:kms_provider]}'" + lines << "in key vault: '#{result[:key_vault_namespace]}'." + end + puts output.join(' ') end end end end +# rubocop:enable Metrics/BlockLength diff --git a/lib/mongoid/tasks/encryption.rb b/lib/mongoid/tasks/encryption.rb index 613e1699d3..52c177f469 100644 --- a/lib/mongoid/tasks/encryption.rb +++ b/lib/mongoid/tasks/encryption.rb @@ -16,11 +16,12 @@ module Encryption # @param [ String | nil ] client_name The name of the client to take # auto_encryption_options from. If not provided, the default client # will be used. + # @param [ String | nil ] key_alt_name The alternate name of the key. # # @return [ Hash ] A hash containing the key id as :key_id, # kms provider name as :kms_provider, and key vault namespace as # :key_vault_namespace. - def create_data_key(kms_provider_name: nil, client_name: nil) + def create_data_key(client_name: nil, kms_provider_name: nil, key_alt_name: nil) kms_provider_name, kms_providers, key_vault_namespace = prepare_arguments( kms_provider_name, client_name @@ -31,12 +32,16 @@ def create_data_key(kms_provider_name: nil, client_name: nil) key_vault_namespace: key_vault_namespace, kms_providers: kms_providers ) - data_key_id = client_encryption.create_data_key(kms_provider_name) + client_encryption_opts = {}.tap do |opts| + opts[:key_alt_names] = [key_alt_name] if key_alt_name + end + data_key_id = client_encryption.create_data_key(kms_provider_name, client_encryption_opts) { key_id: Base64.strict_encode64(data_key_id.data), kms_provider: kms_provider_name, - key_vault_namespace: key_vault_namespace - } + key_vault_namespace: key_vault_namespace, + key_alt_name: key_alt_name + }.compact end private diff --git a/spec/integration/encryption_spec.rb b/spec/integration/encryption_spec.rb index d77e37fd10..ee4b956a1e 100644 --- a/spec/integration/encryption_spec.rb +++ b/spec/integration/encryption_spec.rb @@ -54,6 +54,7 @@ code: '12345', medical_records: ['one', 'two', 'three'], blood_type: 'A+', + blood_type_key_name: key_alt_name, ssn: 123456789, insurance: Crypt::Insurance.new(policy_number: 123456789) ) @@ -71,6 +72,7 @@ code: '12345', medical_records: ['one', 'two', 'three'], blood_type: 'A+', + blood_type_key_name: key_alt_name, ssn: 123456789, insurance: Crypt::Insurance.new(policy_number: 123456789) ) diff --git a/spec/mongoid/config/encryption_spec.rb b/spec/mongoid/config/encryption_spec.rb index b8e0f00fa5..66bf608300 100644 --- a/spec/mongoid/config/encryption_spec.rb +++ b/spec/mongoid/config/encryption_spec.rb @@ -30,6 +30,7 @@ }, "blood_type" => { "encrypt" => { + "keyId" => "/blood_type_key_name", "bsonType" => "string", "algorithm" => "AEAD_AES_256_CBC_HMAC_SHA_512-Random" } @@ -57,7 +58,7 @@ end let(:models) do - [ Crypt::Patient ] + [Crypt::Patient] end it "returns a map of encryption schemas" do @@ -66,7 +67,7 @@ context "when models are related" do let(:models) do - [ Crypt::Patient, Crypt::Insurance ] + [Crypt::Patient, Crypt::Insurance] end it "returns a map of encryption schemas" do @@ -76,7 +77,7 @@ context 'and fields do not have encryption options' do let(:models) do - [ Crypt::Car ] + [Crypt::Car] end let(:expected_schema_map) do @@ -129,7 +130,7 @@ end let(:models) do - [ Crypt::User ] + [Crypt::User] end it "returns a map of encryption schemas" do @@ -140,7 +141,7 @@ context 'when a model does not have encrypted fields' do let(:models) do - [ Person ] + [Person] end it 'returns an empty map' do diff --git a/spec/mongoid/tasks/encryption_spec.rb b/spec/mongoid/tasks/encryption_spec.rb index fbbecb2176..8084547a82 100644 --- a/spec/mongoid/tasks/encryption_spec.rb +++ b/spec/mongoid/tasks/encryption_spec.rb @@ -31,27 +31,35 @@ BSON::Binary.new('data_key_id', :uuid) end + let(:key_alt_name) do + 'mongoid_test_alt_name' + end + before do key_vault_client[key_vault_collection].drop Mongoid::Config.send(:clients=, config) end context 'when all parameters are correct' do - before do - expect_any_instance_of(Mongo::ClientEncryption) - .to receive(:create_data_key) - .with('local') - .and_return(data_key_id) - end - context 'when all parameters are provided' do + before do + expect_any_instance_of(Mongo::ClientEncryption) + .to receive(:create_data_key) + .with('local', { key_alt_names: [key_alt_name] }) + .and_return(data_key_id) + end it 'creates a data key' do - result = Mongoid::Tasks::Encryption.create_data_key(kms_provider_name: 'local', client_name: :encrypted) + result = Mongoid::Tasks::Encryption.create_data_key( + kms_provider_name: 'local', + client_name: :encrypted, + key_alt_name: key_alt_name + ) expect(result).to eq( { key_id: Base64.strict_encode64(data_key_id.data), key_vault_namespace: key_vault_namespace, - kms_provider: 'local' + kms_provider: 'local', + key_alt_name: key_alt_name } ) end @@ -59,15 +67,46 @@ context 'when kms_provider_name is not provided' do context 'and there is only one kms provider' do - it 'creates a data key' do - result = Mongoid::Tasks::Encryption.create_data_key(client_name: :encrypted) - expect(result).to eq( - { - key_id: Base64.strict_encode64(data_key_id.data), - key_vault_namespace: key_vault_namespace, - kms_provider: 'local' - } - ) + context 'without key_alt_name' do + before do + expect_any_instance_of(Mongo::ClientEncryption) + .to receive(:create_data_key) + .with('local', {}) + .and_return(data_key_id) + end + it 'creates a data key' do + result = Mongoid::Tasks::Encryption.create_data_key(client_name: :encrypted) + expect(result).to eq( + { + key_id: Base64.strict_encode64(data_key_id.data), + key_vault_namespace: key_vault_namespace, + kms_provider: 'local' + } + ) + end + end + + context 'with key_alt_name' do + before do + expect_any_instance_of(Mongo::ClientEncryption) + .to receive(:create_data_key) + .with('local', {key_alt_names: [key_alt_name]}) + .and_return(data_key_id) + end + it 'creates a data key' do + result = Mongoid::Tasks::Encryption.create_data_key( + client_name: :encrypted, + key_alt_name: key_alt_name + ) + expect(result).to eq( + { + key_id: Base64.strict_encode64(data_key_id.data), + key_vault_namespace: key_vault_namespace, + kms_provider: 'local', + key_alt_name: key_alt_name + } + ) + end end end end diff --git a/spec/support/crypt.rb b/spec/support/crypt.rb index 8023833e95..86a78225d1 100644 --- a/spec/support/crypt.rb +++ b/spec/support/crypt.rb @@ -71,7 +71,7 @@ module Crypt if (data_key = client_encryption.get_key_by_alt_name(key_alt_name)) Base64.encode64(data_key['_id'].data) else - key_id = client_encryption.create_data_key('local', key_alt_name: key_alt_name) + key_id = client_encryption.create_data_key('local', key_alt_names: [key_alt_name]) Base64.encode64(key_id.data).strip end end diff --git a/spec/support/crypt/models.rb b/spec/support/crypt/models.rb index 7812d8b43c..b3e7c67cdc 100644 --- a/spec/support/crypt/models.rb +++ b/spec/support/crypt/models.rb @@ -7,8 +7,12 @@ class Patient field :code, type: String field :medical_records, type: Array, encrypt: { deterministic: false} - field :blood_type, type: String, encrypt: { deterministic: false } + field :blood_type, type: String, encrypt: { + deterministic: false, + key_name_field: :blood_type_key_name + } field :ssn, type: Integer, encrypt: { deterministic: true } + field :blood_type_key_name, type: String embeds_one :insurance, class_name: "Crypt::Insurance" end