Skip to content

Commit 0aaf865

Browse files
MONGOID-5615 Allow use key_alt_name (#5665)
* MONGOID-5615 Allow use key_alt_name * Update specs * Support key_alt_name in the rake task * Cleanup * Update docs * Fix code review remarks
1 parent bf44047 commit 0aaf865

File tree

10 files changed

+150
-51
lines changed

10 files changed

+150
-51
lines changed

docs/tutorials/automatic-encryption.txt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,15 @@ You can create multiple DEKs, if necessary.
181181
% rake db:mongoid:encryption:create_data_key
182182
Created data key with id: 'Vxr5m+5cQISjDOruzZgE0w==' for kms provider: 'local' in key vault: 'encryption.__keyVault'.
183183

184+
You can also provide an alternate name for the DEK. This allows you to reference
185+
the DEK by name when configuring encryption for your fields. It also allows you
186+
to dynamically assign a DEK to a field at runtime.
187+
188+
.. code-block:: sh
189+
190+
% rake db:mongoid:encryption:create_data_key -- --key-alt-name=my_data_key
191+
Created data key with id: 'yjF8hKmKQsqGeFGXlB9Sow==' with key alt name: 'my_data_key' for kms provider: 'local' in key vault: 'encryption.__keyVault'.
192+
184193

185194
Configure Encryption Schema
186195
===========================
@@ -226,9 +235,11 @@ Now we can tell Mongoid what should be encrypted:
226235
field :insurer, type: String
227236

228237
# This field is encrypted using AEAD_AES_256_CBC_HMAC_SHA_512-Random
229-
# algorithm using the same data key as Patient class attributes.
238+
# algorithm using the key which alternate name is stored in the
239+
# policy_number_key field.
230240
field :policy_number, type: Integer, encrypt: {
231-
deterministic: false
241+
deterministic: false,
242+
key_field_name: :policy_number_key
232243
}
233244

234245
embedded_in :patient
@@ -254,7 +265,7 @@ according to the configuration:
254265
passport_id: '123456',
255266
blood_type: 'AB+',
256267
ssn: 98765,
257-
insurance: Insurance.new(insurer: 'TK', policy_number: 123456)
268+
insurance: Insurance.new(insurer: 'TK', policy_number: 123456, policy_number_key: 'my_data_key')
258269
)
259270

260271
# Fields are encrypted in the database
@@ -265,7 +276,7 @@ according to the configuration:
265276
# "passport_id"=><BSON::Binary:0x404080 type=ciphertext data=0x012889b2cb0b1341...>,
266277
# "blood_type"=><BSON::Binary:0x404560 type=ciphertext data=0x022889b2cb0b1341...>,
267278
# "ssn"=><BSON::Binary:0x405040 type=ciphertext data=0x012889b2cb0b1341...>,
268-
# "insurance"=>{"_id"=>BSON::ObjectId('6446a1d046ebfd701f9f4293'), "insurer"=>"TK", "policy_number"=><BSON::Binary:0x405920 type=ciphertext data=0x012889b2cb0b1341...>}}
279+
# "insurance"=>{"_id"=>BSON::ObjectId('6446a1d046ebfd701f9f4293'), "insurer"=>"TK", "policy_number"=><BSON::Binary:0x405920 type=ciphertext data=0x012889b2cb0b1341...>}, "policy_number_key"=>"my_data_key"}
269280

270281
Fields encrypted using a deterministic algorithm can be queried. Only exact match
271282
queries are supported. For more details please consult `the server documentation

lib/mongoid/config/encryption.rb

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def encryption_schema_map(default_database, models = ::Mongoid.models)
7373
# @return [ Hash ] The encryptMetadata object.
7474
def metadata_for(model)
7575
metadata = {}.tap do |metadata|
76-
if (key_id = key_id_for(model.encrypt_metadata[:key_id]))
76+
if (key_id = key_id_for(model.encrypt_metadata[:key_id], model.encrypt_metadata[:key_name_field]))
7777
metadata['keyId'] = key_id
7878
end
7979
if model.encrypt_metadata.key?(:deterministic)
@@ -129,7 +129,7 @@ def properties_for_fields(model)
129129
if (algorithm = algorithm_for(field))
130130
props[name]['encrypt']['algorithm'] = algorithm
131131
end
132-
if (key_id = key_id_for(field.key_id))
132+
if (key_id = key_id_for(field.key_id, field.key_name_field))
133133
props[name]['encrypt']['keyId'] = key_id
134134
end
135135
end
@@ -192,13 +192,22 @@ def algorithm_for(field)
192192
# key id.
193193
#
194194
# @param [ String | nil ] key_id_base64 The base64 encoded key id.
195-
#
196-
# @return [ Array<BSON::Binary> | nil ] The keyId encryption schema field,
197-
# or nil if the key id is nil.
198-
def key_id_for(key_id_base64)
199-
return nil if key_id_base64.nil?
195+
# @param [ String | nil ] key_name_field The name of the key name field.
196+
#
197+
# @return [ Array<BSON::Binary> | String | nil ] The keyId encryption schema field,
198+
# JSON pointer to the field that contains keyAltName,
199+
# or nil if both key_id_base64 and key_name_field are nil.
200+
def key_id_for(key_id_base64, key_name_field)
201+
return nil if key_id_base64.nil? && key_name_field.nil?
202+
if !key_id_base64.nil? && !key_name_field.nil?
203+
raise ArgumentError, 'Specifying both key_id and key_name_field is not allowed'
204+
end
200205

201-
[ BSON::Binary.new(Base64.decode64(key_id_base64), :uuid) ]
206+
if key_id_base64.nil?
207+
"/#{key_name_field}"
208+
else
209+
[ BSON::Binary.new(Base64.decode64(key_id_base64), :uuid) ]
210+
end
202211
end
203212
end
204213
end

lib/mongoid/fields/encrypted.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def key_id
2626
@encryption_options[:key_id]
2727
end
2828

29+
# @return [ String | nil ] The name of the field that contains the
30+
# key alt name to use for encryption; if not specified, nil is returned.
31+
def key_name_field
32+
@encryption_options[:key_name_field]
33+
end
34+
2935
# Override the key_id for the field.
3036
#
3137
# This method is solely for testing purposes and should not be used in

lib/mongoid/tasks/encryption.rake

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
11
# frozen_string_literal: true
2-
# rubocop:todo all
32

3+
require 'optparse'
4+
5+
# rubocop:disable Metrics/BlockLength
46
namespace :db do
57
namespace :mongoid do
68
namespace :encryption do
7-
8-
desc "Create encryption key"
9-
task :create_data_key, [:client, :provider] => [:environment] do |_t, args|
10-
result = ::Mongoid::Tasks::Encryption.create_data_key(
11-
client_name: args[:client],
12-
kms_provider_name: args[:provider]
9+
desc 'Create encryption key'
10+
task create_data_key: [ :environment ] do
11+
options = {}
12+
parser = OptionParser.new do |opts|
13+
opts.on('-c', '--client CLIENT', 'Name of the client to use') do |v|
14+
options[:client_name] = v
15+
end
16+
opts.on('-p', '--provider PROVIDER', 'KMS provider to use') do |v|
17+
options[:kms_provider_name] = v
18+
end
19+
opts.on('-n', '--key-alt-name KEY_ALT_NAME', 'Alternate name for the key') do |v|
20+
options[:key_alt_name] = v
21+
end
22+
end
23+
# rubocop:disable Lint/EmptyBlock
24+
parser.parse!(parser.order!(ARGV) {})
25+
# rubocop:enable Lint/EmptyBlock
26+
result = Mongoid::Tasks::Encryption.create_data_key(
27+
client_name: options[:client_name],
28+
kms_provider_name: options[:kms_provider_name],
29+
key_alt_name: options[:key_alt_name]
1330
)
14-
puts "Created data key with id: '#{result[:key_id]}' " +
15-
"for kms provider: '#{result[:kms_provider]}' " +
16-
"in key vault: '#{result[:key_vault_namespace]}'."
31+
output = [].tap do |lines|
32+
lines << "Created data key with id: '#{result[:key_id]}'"
33+
lines << "with key alt name: '#{result[:key_alt_name]}'" if result[:key_alt_name]
34+
lines << "for kms provider: '#{result[:kms_provider]}'"
35+
lines << "in key vault: '#{result[:key_vault_namespace]}'."
36+
end
37+
puts output.join(' ')
1738
end
1839
end
1940
end
2041
end
42+
# rubocop:enable Metrics/BlockLength

lib/mongoid/tasks/encryption.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ module Encryption
1616
# @param [ String | nil ] client_name The name of the client to take
1717
# auto_encryption_options from. If not provided, the default client
1818
# will be used.
19+
# @param [ String | nil ] key_alt_name The alternate name of the key.
1920
#
2021
# @return [ Hash ] A hash containing the key id as :key_id,
2122
# kms provider name as :kms_provider, and key vault namespace as
2223
# :key_vault_namespace.
23-
def create_data_key(kms_provider_name: nil, client_name: nil)
24+
def create_data_key(client_name: nil, kms_provider_name: nil, key_alt_name: nil)
2425
kms_provider_name, kms_providers, key_vault_namespace = prepare_arguments(
2526
kms_provider_name,
2627
client_name
@@ -31,12 +32,16 @@ def create_data_key(kms_provider_name: nil, client_name: nil)
3132
key_vault_namespace: key_vault_namespace,
3233
kms_providers: kms_providers
3334
)
34-
data_key_id = client_encryption.create_data_key(kms_provider_name)
35+
client_encryption_opts = {}.tap do |opts|
36+
opts[:key_alt_names] = [key_alt_name] if key_alt_name
37+
end
38+
data_key_id = client_encryption.create_data_key(kms_provider_name, client_encryption_opts)
3539
{
3640
key_id: Base64.strict_encode64(data_key_id.data),
3741
kms_provider: kms_provider_name,
38-
key_vault_namespace: key_vault_namespace
39-
}
42+
key_vault_namespace: key_vault_namespace,
43+
key_alt_name: key_alt_name
44+
}.compact
4045
end
4146

4247
private

spec/integration/encryption_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
code: '12345',
5555
medical_records: ['one', 'two', 'three'],
5656
blood_type: 'A+',
57+
blood_type_key_name: key_alt_name,
5758
ssn: 123456789,
5859
insurance: Crypt::Insurance.new(policy_number: 123456789)
5960
)
@@ -71,6 +72,7 @@
7172
code: '12345',
7273
medical_records: ['one', 'two', 'three'],
7374
blood_type: 'A+',
75+
blood_type_key_name: key_alt_name,
7476
ssn: 123456789,
7577
insurance: Crypt::Insurance.new(policy_number: 123456789)
7678
)

spec/mongoid/config/encryption_spec.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
},
3131
"blood_type" => {
3232
"encrypt" => {
33+
"keyId" => "/blood_type_key_name",
3334
"bsonType" => "string",
3435
"algorithm" => "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
3536
}
@@ -57,7 +58,7 @@
5758
end
5859

5960
let(:models) do
60-
[ Crypt::Patient ]
61+
[Crypt::Patient]
6162
end
6263

6364
it "returns a map of encryption schemas" do
@@ -66,7 +67,7 @@
6667

6768
context "when models are related" do
6869
let(:models) do
69-
[ Crypt::Patient, Crypt::Insurance ]
70+
[Crypt::Patient, Crypt::Insurance]
7071
end
7172

7273
it "returns a map of encryption schemas" do
@@ -76,7 +77,7 @@
7677

7778
context 'and fields do not have encryption options' do
7879
let(:models) do
79-
[ Crypt::Car ]
80+
[Crypt::Car]
8081
end
8182

8283
let(:expected_schema_map) do
@@ -129,7 +130,7 @@
129130
end
130131

131132
let(:models) do
132-
[ Crypt::User ]
133+
[Crypt::User]
133134
end
134135

135136
it "returns a map of encryption schemas" do
@@ -140,7 +141,7 @@
140141

141142
context 'when a model does not have encrypted fields' do
142143
let(:models) do
143-
[ Person ]
144+
[Person]
144145
end
145146

146147
it 'returns an empty map' do

spec/mongoid/tasks/encryption_spec.rb

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,43 +31,82 @@
3131
BSON::Binary.new('data_key_id', :uuid)
3232
end
3333

34+
let(:key_alt_name) do
35+
'mongoid_test_alt_name'
36+
end
37+
3438
before do
3539
key_vault_client[key_vault_collection].drop
3640
Mongoid::Config.send(:clients=, config)
3741
end
3842

3943
context 'when all parameters are correct' do
40-
before do
41-
expect_any_instance_of(Mongo::ClientEncryption)
42-
.to receive(:create_data_key)
43-
.with('local')
44-
.and_return(data_key_id)
45-
end
46-
4744
context 'when all parameters are provided' do
45+
before do
46+
expect_any_instance_of(Mongo::ClientEncryption)
47+
.to receive(:create_data_key)
48+
.with('local', { key_alt_names: [key_alt_name] })
49+
.and_return(data_key_id)
50+
end
4851
it 'creates a data key' do
49-
result = Mongoid::Tasks::Encryption.create_data_key(kms_provider_name: 'local', client_name: :encrypted)
52+
result = Mongoid::Tasks::Encryption.create_data_key(
53+
kms_provider_name: 'local',
54+
client_name: :encrypted,
55+
key_alt_name: key_alt_name
56+
)
5057
expect(result).to eq(
5158
{
5259
key_id: Base64.strict_encode64(data_key_id.data),
5360
key_vault_namespace: key_vault_namespace,
54-
kms_provider: 'local'
61+
kms_provider: 'local',
62+
key_alt_name: key_alt_name
5563
}
5664
)
5765
end
5866
end
5967

6068
context 'when kms_provider_name is not provided' do
6169
context 'and there is only one kms provider' do
62-
it 'creates a data key' do
63-
result = Mongoid::Tasks::Encryption.create_data_key(client_name: :encrypted)
64-
expect(result).to eq(
65-
{
66-
key_id: Base64.strict_encode64(data_key_id.data),
67-
key_vault_namespace: key_vault_namespace,
68-
kms_provider: 'local'
69-
}
70-
)
70+
context 'without key_alt_name' do
71+
before do
72+
expect_any_instance_of(Mongo::ClientEncryption)
73+
.to receive(:create_data_key)
74+
.with('local', {})
75+
.and_return(data_key_id)
76+
end
77+
it 'creates a data key' do
78+
result = Mongoid::Tasks::Encryption.create_data_key(client_name: :encrypted)
79+
expect(result).to eq(
80+
{
81+
key_id: Base64.strict_encode64(data_key_id.data),
82+
key_vault_namespace: key_vault_namespace,
83+
kms_provider: 'local'
84+
}
85+
)
86+
end
87+
end
88+
89+
context 'with key_alt_name' do
90+
before do
91+
expect_any_instance_of(Mongo::ClientEncryption)
92+
.to receive(:create_data_key)
93+
.with('local', {key_alt_names: [key_alt_name]})
94+
.and_return(data_key_id)
95+
end
96+
it 'creates a data key' do
97+
result = Mongoid::Tasks::Encryption.create_data_key(
98+
client_name: :encrypted,
99+
key_alt_name: key_alt_name
100+
)
101+
expect(result).to eq(
102+
{
103+
key_id: Base64.strict_encode64(data_key_id.data),
104+
key_vault_namespace: key_vault_namespace,
105+
kms_provider: 'local',
106+
key_alt_name: key_alt_name
107+
}
108+
)
109+
end
71110
end
72111
end
73112
end

spec/support/crypt.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ module Crypt
7171
if (data_key = client_encryption.get_key_by_alt_name(key_alt_name))
7272
Base64.encode64(data_key['_id'].data)
7373
else
74-
key_id = client_encryption.create_data_key('local', key_alt_name: key_alt_name)
74+
key_id = client_encryption.create_data_key('local', key_alt_names: [key_alt_name])
7575
Base64.encode64(key_id.data).strip
7676
end
7777
end

spec/support/crypt/models.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ class Patient
77

88
field :code, type: String
99
field :medical_records, type: Array, encrypt: { deterministic: false}
10-
field :blood_type, type: String, encrypt: { deterministic: false }
10+
field :blood_type, type: String, encrypt: {
11+
deterministic: false,
12+
key_name_field: :blood_type_key_name
13+
}
1114
field :ssn, type: Integer, encrypt: { deterministic: true }
15+
field :blood_type_key_name, type: String
1216

1317
embeds_one :insurance, class_name: "Crypt::Insurance"
1418
end

0 commit comments

Comments
 (0)