Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MONGOID-5615 Allow use key_alt_name #5665

Merged
merged 6 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions docs/tutorials/automatic-encryption.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========================
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -265,7 +276,7 @@ according to the configuration:
# "passport_id"=><BSON::Binary:0x404080 type=ciphertext data=0x012889b2cb0b1341...>,
# "blood_type"=><BSON::Binary:0x404560 type=ciphertext data=0x022889b2cb0b1341...>,
# "ssn"=><BSON::Binary:0x405040 type=ciphertext data=0x012889b2cb0b1341...>,
# "insurance"=>{"_id"=>BSON::ObjectId('6446a1d046ebfd701f9f4293'), "insurer"=>"TK", "policy_number"=><BSON::Binary:0x405920 type=ciphertext data=0x012889b2cb0b1341...>}}
# "insurance"=>{"_id"=>BSON::ObjectId('6446a1d046ebfd701f9f4293'), "insurer"=>"TK", "policy_number"=><BSON::Binary:0x405920 type=ciphertext data=0x012889b2cb0b1341...>}, "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
Expand Down
21 changes: 14 additions & 7 deletions lib/mongoid/config/encryption.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -192,13 +192,20 @@ def algorithm_for(field)
# key id.
#
# @param [ String | nil ] key_id_base64 The base64 encoded key id.
# @param [ String | nil ] key_name_field The name of the key name field.
#
# @return [ Array<BSON::Binary> | 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?
# @return [ Array<BSON::Binary> | 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?
raise ArgumentError, 'FIXME' if !key_id_base64.nil? && !key_name_field.nil?
comandeo marked this conversation as resolved.
Show resolved Hide resolved

[ BSON::Binary.new(Base64.decode64(key_id_base64), :uuid) ]
if !key_id_base64.nil?
[ BSON::Binary.new(Base64.decode64(key_id_base64), :uuid) ]
else
"/#{key_name_field}"
end
end
end
end
Expand Down
6 changes: 6 additions & 0 deletions lib/mongoid/fields/encrypted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 32 additions & 10 deletions lib/mongoid/tasks/encryption.rake
Original file line number Diff line number Diff line change
@@ -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
13 changes: 9 additions & 4 deletions lib/mongoid/tasks/encryption.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions spec/integration/encryption_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand All @@ -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)
)
Expand Down
11 changes: 6 additions & 5 deletions spec/mongoid/config/encryption_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
},
"blood_type" => {
"encrypt" => {
"keyId" => "/blood_type_key_name",
"bsonType" => "string",
"algorithm" => "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
Expand Down Expand Up @@ -57,7 +58,7 @@
end

let(:models) do
[ Crypt::Patient ]
[Crypt::Patient]
end

it "returns a map of encryption schemas" do
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -129,7 +130,7 @@
end

let(:models) do
[ Crypt::User ]
[Crypt::User]
end

it "returns a map of encryption schemas" do
Expand All @@ -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
Expand Down
75 changes: 57 additions & 18 deletions spec/mongoid/tasks/encryption_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,43 +31,82 @@
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
end

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
Expand Down
2 changes: 1 addition & 1 deletion spec/support/crypt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion spec/support/crypt/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading