Skip to content

Commit f159956

Browse files
DmitryLukyanovBorisDog
authored andcommitted
CSHARP-4255: Automatically create Queryable Encryption keys. (mongodb#961)
1 parent 586b2cc commit f159956

File tree

4 files changed

+184
-3
lines changed

4 files changed

+184
-3
lines changed

src/MongoDB.Driver.Core/Core/Encryption/EncryptedCollectionHelper.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,30 @@ public static BsonDocument GetEffectiveEncryptedFields(CollectionNamespace colle
7676
}
7777
}
7878

79+
public static IEnumerable<BsonDocument> IterateEmptyKeyIds(CollectionNamespace collectionNamespace, BsonDocument encryptedFields)
80+
{
81+
if (!EncryptedCollectionHelper.TryGetEffectiveEncryptedFields(collectionNamespace, encryptedFields, encryptedFieldsMap: null, out var storedEncryptedFields))
82+
{
83+
throw new InvalidOperationException("There are no encrypted fields defined for the collection.");
84+
}
85+
86+
if (storedEncryptedFields.TryGetValue("fields", out var fields) && fields is BsonArray fieldsArray)
87+
{
88+
foreach (var field in fieldsArray.OfType<BsonDocument>()) // If `F` is not a document element, skip it.
89+
{
90+
if (field.TryGetElement("keyId", out var keyId) && keyId.Value == BsonNull.Value)
91+
{
92+
yield return field;
93+
}
94+
}
95+
}
96+
}
97+
98+
public static void ModifyEncryptedFields(BsonDocument fieldDocument, Guid dataKey)
99+
{
100+
fieldDocument["keyId"] = new BsonBinaryData(dataKey, GuidRepresentation.Standard);
101+
}
102+
79103
public enum HelperCollectionForEncryption
80104
{
81105
Esc,

src/MongoDB.Driver/Encryption/ClientEncryption.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using MongoDB.Bson;
2121
using MongoDB.Driver.Core.Clusters;
2222
using MongoDB.Driver.Core.Configuration;
23+
using MongoDB.Driver.Core.Misc;
2324
using MongoDB.Libmongocrypt;
2425

2526
namespace MongoDB.Driver.Encryption
@@ -78,6 +79,64 @@ public BsonDocument AddAlternateKeyName(Guid id, string alternateKeyName, Cancel
7879
public Task<BsonDocument> AddAlternateKeyNameAsync(Guid id, string alternateKeyName, CancellationToken cancellationToken = default) =>
7980
_libMongoCryptController.AddAlternateKeyNameAsync(id, alternateKeyName, cancellationToken);
8081

82+
/// <summary>
83+
/// Create encrypted collection.
84+
/// </summary>
85+
/// <param name="collectionNamespace">The collection namespace.</param>
86+
/// <param name="createCollectionOptions">The create collection options.</param>
87+
/// <param name="kmsProvider">The kms provider.</param>
88+
/// <param name="dataKeyOptions">The datakey options.</param>
89+
/// <param name="cancellationToken">The cancellation token.</param>
90+
/// <remarks>
91+
/// if EncryptionFields contains a keyId with a null value, a data key will be automatically generated and assigned to keyId value.
92+
/// </remarks>
93+
public void CreateEncryptedCollection<TCollection>(CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default)
94+
{
95+
Ensure.IsNotNull(collectionNamespace, nameof(collectionNamespace));
96+
Ensure.IsNotNull(createCollectionOptions, nameof(createCollectionOptions));
97+
Ensure.IsNotNull(dataKeyOptions, nameof(dataKeyOptions));
98+
Ensure.IsNotNull(kmsProvider, nameof(kmsProvider));
99+
100+
foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(collectionNamespace, createCollectionOptions.EncryptedFields))
101+
{
102+
var dataKey = CreateDataKey(kmsProvider, dataKeyOptions, cancellationToken);
103+
EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey);
104+
}
105+
106+
var database = _libMongoCryptController.KeyVaultClient.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName);
107+
108+
database.CreateCollection(collectionNamespace.CollectionName, createCollectionOptions, cancellationToken);
109+
}
110+
111+
/// <summary>
112+
/// Create encrypted collection.
113+
/// </summary>
114+
/// <param name="collectionNamespace">The collection namespace.</param>
115+
/// <param name="createCollectionOptions">The create collection options.</param>
116+
/// <param name="kmsProvider">The kms provider.</param>
117+
/// <param name="dataKeyOptions">The datakey options.</param>
118+
/// <param name="cancellationToken">The cancellation token.</param>
119+
/// <remarks>
120+
/// if EncryptionFields contains a keyId with a null value, a data key will be automatically generated and assigned to keyId value.
121+
/// </remarks>
122+
public async Task CreateEncryptedCollectionAsync<TCollection>(CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default)
123+
{
124+
Ensure.IsNotNull(collectionNamespace, nameof(collectionNamespace));
125+
Ensure.IsNotNull(createCollectionOptions, nameof(createCollectionOptions));
126+
Ensure.IsNotNull(dataKeyOptions, nameof(dataKeyOptions));
127+
Ensure.IsNotNull(kmsProvider, nameof(kmsProvider));
128+
129+
foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(collectionNamespace, createCollectionOptions.EncryptedFields))
130+
{
131+
var dataKey = await CreateDataKeyAsync(kmsProvider, dataKeyOptions, cancellationToken).ConfigureAwait(false);
132+
EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey);
133+
}
134+
135+
var database = _libMongoCryptController.KeyVaultClient.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName);
136+
137+
await database.CreateCollectionAsync(collectionNamespace.CollectionName, createCollectionOptions, cancellationToken).ConfigureAwait(false);
138+
}
139+
81140
/// <summary>
82141
/// An alias function equivalent to createKey.
83142
/// </summary>

src/MongoDB.Driver/Encryption/LibMongoCryptControllerBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ protected LibMongoCryptControllerBase(
6161
_tlsOptions = Ensure.IsNotNull(encryptionOptions.TlsOptions, nameof(encryptionOptions.TlsOptions));
6262
}
6363

64+
// public properties
65+
public IMongoClient KeyVaultClient => _keyVaultClient;
66+
6467
// protected methods
6568
protected void FeedResult(CryptContext context, BsonDocument document)
6669
{

tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,76 @@ public ClientEncryptionProseTests(ITestOutputHelper testOutputHelper)
8787
// public methods
8888
[Theory]
8989
[ParameterAttributeData]
90+
public void AutomaticDataEncryptionKeysTest(
91+
[Range(1, 4)] int testCase,
92+
[Values(false, true)] bool async)
93+
{
94+
RequireServer.Check().Supports(Feature.Csfle2).ClusterTypes(ClusterType.ReplicaSet, ClusterType.Sharded, ClusterType.LoadBalanced);
95+
96+
var kmsProvider = "local";
97+
using (var client = ConfigureClient())
98+
using (var clientEncryption = ConfigureClientEncryption(client, kmsProviderFilter: kmsProvider))
99+
{
100+
var encryptedFields = BsonDocument.Parse($@"
101+
{{
102+
fields:
103+
[
104+
{{
105+
path: ""ssn"",
106+
bsonType: ""string"",
107+
keyId: null
108+
}}
109+
]
110+
}}");
111+
112+
DropCollection(__collCollectionNamespace, encryptedFields);
113+
114+
RunTestCase(testCase);
115+
116+
void RunTestCase(int testCase)
117+
{
118+
switch (testCase)
119+
{
120+
case 1: // Case 1: Simple Creation and Validation
121+
{
122+
var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields, kmsProvider, async);
123+
124+
var exception = Record.Exception(() => Insert(collection, async, new BsonDocument("ssn", "123-45-6789")));
125+
exception.Should().BeOfType<MongoBulkWriteException<BsonDocument>>().Which.Message.Should().Contain("Document failed validation");
126+
}
127+
break;
128+
case 2: // Case 2: Missing ``encryptedFields``
129+
{
130+
var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields: null, kmsProvider, async));
131+
132+
exception.Should().BeOfType<InvalidOperationException>().Which.Message.Should().Contain("There are no encrypted fields defined for the collection.") ;
133+
}
134+
break;
135+
case 3: // Case 3: Invalid ``keyId``
136+
{
137+
var effectiveEncryptedFields = encryptedFields.DeepClone();
138+
effectiveEncryptedFields["fields"].AsBsonArray[0].AsBsonDocument["keyId"] = false;
139+
var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, effectiveEncryptedFields.AsBsonDocument, kmsProvider, async));
140+
exception.Should().BeOfType<MongoCommandException>().Which.Message.Should().Contain("BSON field 'create.encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData'");
141+
}
142+
break;
143+
case 4: // Case 4: Insert encrypted value
144+
{
145+
var createCollectionOptions = new CreateCollectionOptions { EncryptedFields = encryptedFields };
146+
var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, createCollectionOptions, kmsProvider, async);
147+
var dataKey = createCollectionOptions.EncryptedFields["fields"].AsBsonArray[0].AsBsonDocument["keyId"].AsGuid; // get generated datakey
148+
var encryptedValue = ExplicitEncrypt(clientEncryption, new EncryptOptions(algorithm: EncryptionAlgorithm.Unindexed, keyId: dataKey), "123-45-6789", async); // use explicit encryption to encrypt data before inserting
149+
Insert(collection, async, new BsonDocument("ssn", encryptedValue));
150+
}
151+
break;
152+
default: throw new Exception($"Unexpected test case {testCase}.");
153+
}
154+
}
155+
}
156+
}
157+
158+
[SkippableTheory]
159+
[ParameterAttributeData]
90160
public void BsonSizeLimitAndBatchSizeSplittingTest(
91161
[Values(false, true)] bool async)
92162
{
@@ -1123,6 +1193,7 @@ void RunTestCase(IMongoCollection<BsonDocument> decryptionEventsCollection, int
11231193
reply["cursor"]["firstBatch"].AsBsonArray.Single()["encrypted"].AsBsonBinaryData.SubType.Should().Be(BsonBinarySubType.Encrypted);
11241194
}
11251195
break;
1196+
default: throw new Exception($"Unexpected test case {testCase}.");
11261197
}
11271198
}
11281199

@@ -1875,6 +1946,8 @@ HttpClientWrapperWithModifiedRequest CreateHttpClientWrapperWithModifiedRequest(
18751946
}
18761947
}
18771948

1949+
[SkippableTheory]
1950+
[ParameterAttributeData]
18781951
public void RewrapTest(
18791952
[Values("local", "aws", "azure", "gcp", "kmip")] string srcProvider,
18801953
[Values("local", "aws", "azure", "gcp", "kmip")] string dstProvider,
@@ -1929,7 +2002,7 @@ public void ViewAreProhibitedTest([Values(false, true)] bool async)
19292002
using (var client = ConfigureClient(false))
19302003
using (var clientEncrypted = ConfigureClientEncrypted(kmsProviderFilter: "local"))
19312004
{
1932-
DropView(viewName);
2005+
DropCollection(viewName);
19332006
client
19342007
.GetDatabase(viewName.DatabaseNamespace.DatabaseName)
19352008
.CreateView(
@@ -2257,6 +2330,28 @@ private void CreateCollection(IMongoClient client, CollectionNamespace collectio
22572330
});
22582331
}
22592332

2333+
private IMongoCollection<BsonDocument> CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, BsonDocument encryptedFields, string kmsProvider, bool async)
2334+
{
2335+
var createCollectionOptions = new CreateCollectionOptions { EncryptedFields = encryptedFields };
2336+
return CreateEncryptedCollection(client, clientEncryption, collectionNamespace, createCollectionOptions, kmsProvider, async);
2337+
}
2338+
2339+
private IMongoCollection<BsonDocument> CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, bool async)
2340+
{
2341+
var datakeyOptions = CreateDataKeyOptions(kmsProvider);
2342+
2343+
if (async)
2344+
{
2345+
clientEncryption.CreateEncryptedCollectionAsync<BsonDocument>(collectionNamespace, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult();
2346+
}
2347+
else
2348+
{
2349+
clientEncryption.CreateEncryptedCollection<BsonDocument>(collectionNamespace, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default);
2350+
}
2351+
2352+
return client.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName).GetCollection<BsonDocument>(collectionNamespace.CollectionName);
2353+
}
2354+
22602355
private Guid CreateDataKey(
22612356
ClientEncryption clientEncryption,
22622357
string kmsProvider,
@@ -2407,9 +2502,9 @@ private MongoClientSettings CreateMongoClientSettings(
24072502
return mongoClientSettings;
24082503
}
24092504

2410-
private void DropView(CollectionNamespace viewNamespace)
2505+
private void DropCollection(CollectionNamespace collectionNamespace, BsonDocument encryptedFields = null)
24112506
{
2412-
var operation = new DropCollectionOperation(viewNamespace, CoreTestConfiguration.MessageEncoderSettings);
2507+
var operation = DropCollectionOperation.CreateEncryptedDropCollectionOperationIfConfigured(collectionNamespace, encryptedFields, CoreTestConfiguration.MessageEncoderSettings, configureDropCollectionConfigurator: null);
24132508
using (var session = CoreTestConfiguration.StartSession(_cluster))
24142509
using (var binding = new WritableServerBinding(_cluster, session.Fork()))
24152510
using (var bindingHandle = new ReadWriteBindingHandle(binding))

0 commit comments

Comments
 (0)