HomeLearnHow-toHow to Use MongoDB Client-Side Field Level Encryption (CSFLE) with C#

How to Use MongoDB Client-Side Field Level Encryption (CSFLE) with C#

Published: Feb 11, 2021

  • C#
  • Field Level Encryption
  • Security
  • ...

By Adrienne Tacke

Rate this article

Client-side field level encryption (CSFLE) provides an additional layer of security to your most sensitive data. Using a supported MongoDB driver, CSFLE encrypts certain fields that you specify, ensuring they are never transmitted unencrypted, nor seen unencrypted by the MongoDB server.

Encrypt button being pushed, with screen encrypting some data
This may be the only time I use a Transformers GIF. Encryption GIFs are hard to find!

This also means that it's nearly impossible to obtain sensitive information from the database server. Without access to a specific key, data cannot be decrypted and exposed, rendering the intercepting data from the client fruitless. Reading data directly from disk, even with DBA or root credentials, will also be impossible as the data is stored in an encrypted state.

Key applications that showcase the power of client-side field level encryption are those in the medical field. If you quickly think back to the last time you visited a clinic, you already have an effective use case for an application that requires a mix of encrypted and non-encrypted fields. When you check into a clinic, the person may need to search for you by name or insurance provider. These are common data fields that are usually left non-encrypted. Then, there are more obvious pieces of information that require encryption: things like a Social Security number, medical records, or your insurance policy number. For these data fields, encryption is necessary.

This tutorial will walk you through setting up a similar medical system that uses automatic client-side field level encryption in the MongoDB .NET Driver (for explicit, meaning manual, client-side field level encryption, check out these docs).

In it, you'll:

Prepare a .NET Core console application

Generate secure, random keys needed for CSFLE

Configure CSFLE on the MongoClient

See CSFLE in action

💡️ This can be an intimidating tutorial, so don't hesitate to take as many breaks as you need; in fact, complete the steps over a few days! I've tried my best to ensure each step completed acts as a natural save point for the duration of the entire tutorial. :)

Let's do this step by step!

#Prerequisites

💻 The code for this tutorial is available in this repo.

#Create a .NET Core Console Application

Let's start by scaffolding our console application. Open Visual Studio (I'm using Visual Studio 2019 Community Edition) and create a new project. When selecting a template, choose the "Console App (.NET Core)" option and follow the prompts to name your project.

Visual Studio 2019 create a new project prompt; Console App (.NET Core) option is highlighted.
Visual Studio 2019 create a new project prompt; Console App (.NET Core) option is highlighted.

#Install CSFLE Dependencies

Once the project template loads, we'll need to install one of our dependencies. In your Package Manager Console, use the following command to install the MongoDB Driver:

1Install-Package MongoDB.Driver -Version 2.12.0-beta1

💡️ If your Package Manager Console is not visible in your IDE, you can get to it via View > Other Windows > Package Manager Console in the File Menu.

The next dependency you'll need to install is mongocryptd, which is an application that is provided as part of MongoDB Enterprise and is needed for automatic field level encryption. Follow the instructions to install mongocryptd on your machine. In a production environment, it's recommended to run mongocryptd as a service at startup on your VM or container.

Now that our base project and dependencies are set, we can move onto creating and configuring our different encryption keys.

MongoDB client-side field level encryption uses an encryption strategy called envelope encryption. This strategy uses two different kinds of keys.

The first key is called a data encryption key, which is used to encrypt/decrypt the data you'll be storing in MongoDB. The other key is called a master key and is used to encrypt the data encryption key. This is the top-level plaintext key that will always be required and is the key we are going to generate in the next step.

🚨️ Before we proceed, it's important to note that this tutorial will demonstrate the generation of a master key file stored as plaintext in the root of our application. This is okay for development and educational purposes, such as this tutorial. However, this should NOT be done in a production environment!

Why? In this scenario, anyone that obtains a copy of the disk or a VM snapshot of the app server hosting our application would also have access to this key file, making it possible to access the application's data.

Instead, you should configure a master key in a Key Management System such as Azure Key Vault or AWS KMS for production.

Keep this in mind and watch for another post that shows how to implement CSFLE with Azure Key Vault!

#Create a Local Master Key

In this step, we generate a 96-byte, locally-managed master key. Then, we save it to a local file called master-key.txt. We'll be doing a few more things with keys, so create a separate class called KmsKeyHelper.cs. Then, add the following code to it:

1// KmsKeyHelper.cs
2
3using System;
4using System.IO;
5
6namespace EnvoyMedSys
7{
8 public class KmsKeyHelper
9 {
10 private readonly static string __localMasterKeyPath = "../../../master-key.txt";
11
12 public void GenerateLocalMasterKey()
13 {
14 using (var randomNumberGenerator = System.Security.Cryptography.RandomNumberGenerator.Create())
15 {
16 var bytes = new byte[96];
17 randomNumberGenerator.GetBytes(bytes);
18 var localMasterKeyBase64 = Convert.ToBase64String(bytes);
19 Console.WriteLine(localMasterKeyBase64);
20 File.WriteAllText(__localMasterKeyPath, localMasterKeyBase64);
21 }
22 }
23 }
24}

So, what's happening here? Let's break it down, line by line:

First, we declare and set a private variable called __localMasterKeyPath. This holds the path to where we save our master key.

Next, we create a GenerateLocalMasterKey() method. In this method, we use .NET's Cryptography services to create an instance of a RandomNumberGenerator. Using this RandomNumberGenerator, we generate a cryptographically strong, 96-byte key. After converting it to a Base64 representation, we save the key to the master-key.txt file.

Great! We now have a way to generate a local master key. Let's modify the main program to use it. In the Program.cs file, add the following code:

1// Program.cs
2
3using System;
4using System.IO;
5
6namespace EnvoyMedSys
7{
8 class Program
9 {
10 public static void Main()
11 {
12 var kmsKeyHelper = new KmsKeyHelper();
13
14 // Ensure GenerateLocalMasterKey() only runs once!
15 if (!File.Exists("../../../master-key.txt"))
16 {
17 kmsKeyHelper.GenerateLocalMasterKey();
18 }
19
20 Console.ReadKey();
21 }
22 }
23}

In the Main method, we create an instance of our KmsKeyHelper, then call our GenerateLocalMasterKey() method. Pretty straightforward!

Save all files, then run your program. If all is successful, you'll see a console pop up and the Base64 representation of your newly generated master key printed in the console. You'll also see a new master-key.txt file appear in your solution explorer.

Now that we have a master key, we can move onto creating a data encryption key.

#Create a Data Encryption Key

The next key we need to generate is a data encryption key. This is the key the MongoDB driver stores in a key vault collection, and it's used for automatic encryption and decryption.

Automatic encryption requires MongoDB Enterprise 4.2 or a MongoDB 4.2 Atlas cluster. However, automatic decryption is supported for all users. See how to configure automatic decryption without automatic encryption.

Let's add a few more lines of code to the Program.cs file. The highlighted lines are the new additions:

1using System;
2using System.IO;
3using MongoDB.Driver;
4
5namespace EnvoyMedSys
6{
7 class Program
8 {
9 public static void Main()
10 {
11 var connectionString = Environment.GetEnvironmentVariable("MDB_URI");
12 var keyVaultNamespace = CollectionNamespace.FromFullName("encryption.__keyVault");
13
14 var kmsKeyHelper = new KmsKeyHelper(
15 connectionString: connectionString,
16 keyVaultNamespace: keyVaultNamespace);
17
18 string kmsKeyIdBase64;
19
20 // Ensure GenerateLocalMasterKey() only runs once!
21 if (!File.Exists("../../../master-key.txt"))
22 {
23 kmsKeyHelper.GenerateLocalMasterKey();
24 }
25
26 kmsKeyIdBase64 = kmsKeyHelper.CreateKeyWithLocalKmsProvider();
27
28 Console.ReadKey();
29 }
30 }
31}

So, what's changed? First, we added an additional import (MongoDB.Driver). Next, we declared a connectionString and a keyVaultNamespace variable.

For the key vault namespace, MongoDB will automatically create the database encryption and collection __keyVault if it does not currently exist. Both the database and collection names were purely my preference. You can choose to name them something else if you'd like!

Next, we modified the KmsKeyHelper instantiation to accept two parameters: the connection string and key vault namespace we previously declared. Don't worry, we'll be changing our KmsKeyHelper.cs file to match this soon.

Finally, we declare a kmsKeyIdBase64 variable and set it to a new method we'll create soon: CreateKeyWithLocalKmsProvider();. This will hold our data encryption key.

#Securely Setting the MongoDB connection

In our code, we set our MongoDB URI by pulling from environment variables. This is far safer than pasting a connection string directly into our code and is scalable in a variety of automated deployment scenarios.

For our purposes, we'll create a launchSettings.json file.

💡️ Don't commit the launchSettings.json file to a public repo! In fact, add it to your .gitignore file now, if you have one or plan to share this application. Otherwise, you'll expose your MongoDB URI to the world!

Right-click on your project and select "Properties" in the context menu.

The project properties will open to the "Debug" section. In the "Environment variables:" area, add a variable called MDB_URI, followed by the connection URI:

Adding an environment variable to the project settings in Visual Studio 2019.
Adding an environment variable to the project settings in Visual Studio 2019.

What value do you set to your MDB_URI environment variable?

  • MongoDB Atlas: If using a MongoDB Atlas cluster, paste in your Atlas URI.
  • Local: If running a local MongoDB instance and haven't changed any default settings, you can use the default connection string: mongodb://localhost:27017.

Once your MDB_URI is added, save the project properties. You'll see that a launchSettings.json file will be automatically generated for you! Now, any Environment.GetEnvironmentVariable() calls will pull from this file.

With these changes, we now have to modify and add a few more methods to the KmsKeyHelper class. Let's do that now.

First, add these additional imports:

1// KmsKeyHelper.cs
2
3using System.Collections.Generic;
4using System.Threading;
5using MongoDB.Bson;
6using MongoDB.Driver;
7using MongoDB.Driver.Encryption;

Next, declare two private variables and create a constructor that accepts both a connection string and key vault namespace. We'll need this information to create our data encryption key; this also makes it easier to extend and integrate with a remote KMS later on.

1// KmsKeyhelper.cs
2
3private readonly string _mdbConnectionString;
4private readonly CollectionNamespace _keyVaultNamespace;
5
6public KmsKeyHelper( string connectionString, CollectionNamespace keyVaultNamespace)
7{
8 _mdbConnectionString = connectionString;
9 _keyVaultNamespace = keyVaultNamespace;
10}

After the GenerateLocalMasterKey() method, add the following new methods. Don't worry, we'll go over each one:

1// KmsKeyHelper.cs
2
3public string CreateKeyWithLocalKmsProvider()
4{
5 // Read Master Key from file & convert
6 string localMasterKeyBase64 = File.ReadAllText(__localMasterKeyPath);
7 var localMasterKeyBytes = Convert.FromBase64String(localMasterKeyBase64);
8
9 // Set KMS Provider Settings
10 // Client uses these settings to discover the master key
11 var kmsProviders = new Dictionary<string, IReadOnlyDictionary<string, object>>();
12 var localOptions = new Dictionary<string, object>
13 {
14 { "key", localMasterKeyBytes }
15 };
16 kmsProviders.Add("local", localOptions);
17
18 // Create Data Encryption Key
19 var clientEncryption = GetClientEncryption(kmsProviders);
20 var dataKeyid = clientEncryption.CreateDataKey("local", new DataKeyOptions(), CancellationToken.None);
21 clientEncryption.Dispose();
22 Console.WriteLine($"Local DataKeyId [UUID]: {dataKeyid}");
23
24 var dataKeyIdBase64 = Convert.ToBase64String(GuidConverter.ToBytes(dataKeyid, GuidRepresentation.Standard));
25 Console.WriteLine($"Local DataKeyId [base64]: {dataKeyIdBase64}");
26
27 // Optional validation; checks that key was created successfully
28 ValidateKey(dataKeyid);
29 return dataKeyIdBase64;
30}

This method is the one we call from the main program. It's here that we generate our data encryption key. Lines 6-7 read the local master key from our master-key.txt file and convert it to a byte array.

Lines 11-16 set the KMS provider settings the client needs in order to discover the master key. As you can see, we add the local provider and the matching local master key we've just retrieved.

With these KMS provider settings, we construct additional client encryption settings. We do this in a separate method called GetClientEncryption(). Once created, we finally generate an encrypted key.

As an extra measure, we call a third new method ValidateKey(), just to make sure the data encryption key was created. After these steps, and if successful, the CreateKeyWithLocalKmsProvider() method returns our data key id encoded in Base64 format.

After the CreateKeyWithLocalKmsProvider() method, add the following method:

1// KmsKeyHelper.cs
2
3private ClientEncryption GetClientEncryption( Dictionary<string, IReadOnlyDictionary<string, object>> kmsProviders)
4{
5 var keyVaultClient = new MongoClient(_mdbConnectionString);
6 var clientEncryptionOptions = new ClientEncryptionOptions(
7 keyVaultClient: keyVaultClient,
8 keyVaultNamespace: _keyVaultNamespace,
9 kmsProviders: kmsProviders);
10
11 return new ClientEncryption(clientEncryptionOptions);
12}

Within the CreateKeyWithLocalKmsProvider() method, we call GetClientEncryption() (the method we just added) to construct our client encryption settings. These include which key vault client, key vault namespace, and KMS providers to use.

In this method, we construct a MongoClient using the connection string, then set it as a key vault client. We also use the key vault namespace that was passed in and the local KMS providers we previously constructed. These client encryption options are then returned.

Last but not least, after GetClientEncryption(), add the final method:

1// KmsKeyHelper.cs
2
3private void ValidateKey(Guid dataKeyId)
4{
5 var client = new MongoClient(_mdbConnectionString);
6 var collection = client
7 .GetDatabase(_keyVaultNamespace.DatabaseNamespace.DatabaseName)
8 #pragma warning disable CS0618 // Type or member is obsolete
9 .GetCollection<BsonDocument>(_keyVaultNamespace.CollectionName, new MongoCollectionSettings { GuidRepresentation = GuidRepresentation.Standard });
10 #pragma warning restore CS0618 // Type or member is obsolete
11
12 var query = Builders<BsonDocument>.Filter.Eq("_id", new BsonBinaryData(dataKeyId, GuidRepresentation.Standard));
13 var keyDocument = collection
14 .Find(query)
15 .Single();
16
17 Console.WriteLine(keyDocument);
18}

Though optional, this method conveniently checks that the data encryption key was created correctly. It does this by constructing a MongoClient using the specified connection string, then queries the database for the data encryption key. If it was successfully created, the data encryption key would have been inserted as a document into your replica set and will be retrieved in the query.

With these changes, we're ready to generate our data encryption key. Make sure to save all files, then run your program. If all goes well, your console will print out two DataKeyIds (UUID and base64) as well as a document that resembles the following:

1{
2 "_id" : CSUUID("aae4f3b4-91b6-4cef-8867-3113a6dfb27b"),
3 "keyMaterial" : Binary(0, "rcfTQLRxF1mg98/Jr7iFwXWshvAVIQY6JCswrW+4bSqvLwa8bQrc65w7+3P3k+TqFS+1Ce6FW4Epf5o/eqDyT//I73IRc+yPUoZew7TB1pyIKmxL6ABPXJDkUhvGMiwwkRABzZcU9NNpFfH+HhIXjs324FuLzylIhAmJA/gvXcuz6QSD2vFpSVTRBpNu1sq0C9eZBSBaOxxotMZAcRuqMA=="),
4 "creationDate" : ISODate("2020-11-08T17:58:36.372Z"),
5 "updateDate" : ISODate("2020-11-08T17:58:36.372Z"),
6 "status" : 0,
7 "masterKey" : {
8 "provider" : "local"
9 }
10}

For reference, here's what my console output looks like:

Console output showing two data key ids and a data object; these are successful signs of a properly generated data encryption key.
Console output showing two data key ids and a data object; these are successful signs of a properly generated data encryption key.

If you want to be extra sure, you can also check your cluster to see that your data encryption key is stored as a document in the newly created encryption database and __keyVault collection. Since I'm connecting with my Atlas cluster, here's what it looks like there:

Screenshot of atlas ui showing saved data encryption key.
Saved data encryption key in MongoDB Atlas

Sweet! Now that we have generated a data encryption key, which has been encrypted itself with our local master key, the next step is to specify which fields in our application should be encrypted.

#Specify Encrypted Fields Using a JSON Schema

In order for automatic client-side encryption and decryption to work, a JSON schema needs to be defined that specifies which fields to encrypt, which encryption algorithms to use, and the BSON Type of each field.

Using our medical application as an example, let's plan on encrypting the following fields:

Fields to encrypt
Field nameEncryption algorithmsBSON Type
SSN (Social Security Number)DeterministicInt
Blood TypeRandomString
Medical RecordsRandomArray
Insurance: Policy NumberDeterministicInt (embedded inside insurance object)

To make this a bit easier, and to separate this functionality from the rest of the application, create another class named JsonSchemaCreator.cs. In it, add the following code:

1// JsonSchemaCreator.cs
2
3using MongoDB.Bson;
4using System;
5
6namespace EnvoyMedSys
7{
8 public static class JsonSchemaCreator
9 {
10 private static readonly string DETERMINISTIC_ENCRYPTION_TYPE = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
11 private static readonly string RANDOM_ENCRYPTION_TYPE = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
12
13 private static BsonDocument CreateEncryptMetadata(string keyIdBase64)
14 {
15 var keyId = new BsonBinaryData(Convert.FromBase64String(keyIdBase64), BsonBinarySubType.UuidStandard);
16 return new BsonDocument("keyId", new BsonArray(new[] { keyId }));
17 }
18
19 private static BsonDocument CreateEncryptedField(string bsonType, bool isDeterministic)
20 {
21 return new BsonDocument
22 {
23 {
24 "encrypt",
25 new BsonDocument
26 {
27 { "bsonType", bsonType },
28 { "algorithm", isDeterministic ? DETERMINISTIC_ENCRYPTION_TYPE : RANDOM_ENCRYPTION_TYPE}
29 }
30 }
31 };
32 }
33
34 public static BsonDocument CreateJsonSchema(string keyId)
35 {
36 return new BsonDocument
37 {
38 { "bsonType", "object" },
39 { "encryptMetadata", CreateEncryptMetadata(keyId) },
40 {
41 "properties",
42 new BsonDocument
43 {
44 { "ssn", CreateEncryptedField("int", true) },
45 { "bloodType", CreateEncryptedField("string", false) },
46 { "medicalRecords", CreateEncryptedField("array", false) },
47 {
48 "insurance",
49 new BsonDocument
50 {
51 { "bsonType", "object" },
52 {
53 "properties",
54 new BsonDocument
55 {
56 { "policyNumber", CreateEncryptedField("int", true) }
57 }
58 }
59 }
60 }
61 }
62 }
63 };
64 }
65 }
66}

As before, let's step through each line:

First, we create two static variables to hold our encryption types. We use Deterministic encryption for fields that are queryable and have high cardinality. We use Random encryption for fields we don't plan to query, have low cardinality, or are array fields.

Next, we create a CreateEncryptMetadata() helper method. This will return a BsonDocument that contains our converted data key. We'll use this key in the CreateJsonSchema() method.

Lines 19-32 make up another helper method called CreateEncryptedField(). This generates the proper BsonDocument needed to define our encrypted fields. It will output a BsonDocument that resembles the following:

1"ssn": {
2 "encrypt": {
3 "bsonType": "int",
4 "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
5 }
6}

Finally, the CreateJsonSchema() method. Here, we generate the full schema our application will use to know which fields to encrypt and decrypt. This method also returns a BsonDocument.

A few things to note about this schema:

Placing the encryptMetadata key at the root of our schema allows us to encrypt all fields with a single data key. It's here you see the call to our CreateEncryptMetadata() helper method.

Within the properties key go all the fields we wish to encrypt. So, for our ssn, bloodType, medicalRecords, and insurance.policyNumber fields, we generate the respective BsonDocument specifications they need using our CreateEncryptedField() helper method.

With our encrypted fields defined and the necessary encryption keys generated, we can now move onto enabling client-side field level encryption in our MongoDB client!

☕️ Don't forget to take a break! This is a lot of information to take in, so don't rush. Be sure to save all your files, then grab a coffee, stretch, and step away from the computer. This tutorial will be here waiting when you're ready. :)

#Create the CSFLE-Enabled MongoDB Client

A CSFLE-enabled MongoClient is not that much different from a standard client. To create an auto-encrypting client, we instantiate it with some additional auto-encryption options.

As before, let's create a separate class to hold this functionality. Create a file called AutoEncryptHelper.cs and add the following code (note that since this is a bit longer than the other code snippets, I've opted to add inline comments to explain what's happening rather than waiting until after the code block):

1// AutoEncryptHelper.cs
2
3using System;
4using System.Collections.Generic;
5using System.IO;
6using MongoDB.Bson;
7using MongoDB.Driver;
8using MongoDB.Driver.Encryption;
9
10namespace EnvoyMedSys
11{
12 public class AutoEncryptHelper
13 {
14 private static readonly string __localMasterKeyPath = "../../../master-key.txt";
15
16 // Most of what follows are sample fields and a sample medical record we'll be using soon.
17 private static readonly string __sampleNameValue = "Takeshi Kovacs";
18 private static readonly int __sampleSsnValue = 213238414;
19
20 private static readonly BsonDocument __sampleDocFields =
21 new BsonDocument
22 {
23 { "name", __sampleNameValue },
24 { "ssn", __sampleSsnValue },
25 { "bloodType", "AB-" },
26 {
27 "medicalRecords",
28 new BsonArray(new []
29 {
30 new BsonDocument("weight", 180),
31 new BsonDocument("bloodPressure", "120/80")
32 })
33 },
34 {
35 "insurance",
36 new BsonDocument
37 {
38 { "policyNumber", 211241 },
39 { "provider", "EnvoyHealth" }
40 }
41 }
42 };
43
44 // Scaffolding of some private variables we'll need.
45 private readonly string _connectionString;
46 private readonly CollectionNamespace _keyVaultNamespace;
47 private readonly CollectionNamespace _medicalRecordsNamespace;
48
49 // Constructor that will allow us to specify our auto-encrypting
50 // client settings. This also makes it a bit easier to extend and
51 // use with a remote KMS provider later on.
52 public AutoEncryptHelper(string connectionString, CollectionNamespace keyVaultNamespace)
53 {
54 _connectionString = connectionString;
55 _keyVaultNamespace = keyVaultNamespace;
56 _medicalRecordsNamespace = CollectionNamespace.FromFullName("medicalRecords.patients");
57 }
58
59 // The star of the show. Accepts a key location,
60 // a key vault namespace, and a schema; all needed
61 // to construct our CSFLE-enabled MongoClient.
62 private IMongoClient CreateAutoEncryptingClient( KmsKeyLocation kmsKeyLocation, CollectionNamespace keyVaultNamespace, BsonDocument schema)
63 {
64 var kmsProviders = new Dictionary<string, IReadOnlyDictionary<string, object>>();
65
66 // Specify the local master encryption key
67 if (kmsKeyLocation == KmsKeyLocation.Local)
68 {
69 var localMasterKeyBase64 = File.ReadAllText(__localMasterKeyPath);
70 var localMasterKeyBytes = Convert.FromBase64String(localMasterKeyBase64);
71 var localOptions = new Dictionary<string, object>
72 {
73 { "key", localMasterKeyBytes }
74 };
75 kmsProviders.Add("local", localOptions);
76 }
77
78 // Because we didn't explicitly specify the collection our
79 // JSON schema applies to, we assign it here. This will map it
80 // to a database called medicalRecords and a collection called
81 // patients.
82 var schemaMap = new Dictionary<string, BsonDocument>();
83 schemaMap.Add(_medicalRecordsNamespace.ToString(), schema);
84
85 // Specify location of mongocryptd binary, if necessary.
86 // Not required if path to the mongocryptd.exe executable
87 // has been added to your PATH variables
88 var extraOptions = new Dictionary<string, object>()
89 {
90 // Optionally uncomment the following line if you are running mongocryptd manually
91 // { "mongocryptdBypassSpawn", true }
92 };
93
94 // Create CSFLE-enabled MongoClient
95 // The addition of the automatic encryption settings are what
96 // transform this from a standard MongoClient to a CSFLE-enabled
97 // one
98 var clientSettings = MongoClientSettings.FromConnectionString(_connectionString);
99 var autoEncryptionOptions = new AutoEncryptionOptions(
100 keyVaultNamespace: keyVaultNamespace,
101 kmsProviders: kmsProviders,
102 schemaMap: schemaMap,
103 extraOptions: extraOptions);
104 clientSettings.AutoEncryptionOptions = autoEncryptionOptions;
105 return new MongoClient(clientSettings);
106 }
107 }
108}

Alright, we're almost done. Don't forget to save what you have so far! In our next (and final) step, we can finally try out client-side field level encryption with some queries!

🌟 Know what show this patient is from? Let me know your nerd cred (and let's be friends, fellow fan!) in a tweet!

#Perform Encrypted Read/Write Operations

Remember the sample data we've prepared? Let's put that to good use! To test out an encrypted write and read of this data, let's add another method to the AutoEncryptHelper class. Right after the constructor, add the following method:

1// AutoEncryptHelper.cs
2
3public async void EncryptedWriteAndReadAsync(string keyIdBase64, KmsKeyLocation kmsKeyLocation)
4{
5 // Construct a JSON Schema
6 var schema = JsonSchemaCreator.CreateJsonSchema(keyIdBase64);
7
8 // Construct an auto-encrypting client
9 var autoEncryptingClient = CreateAutoEncryptingClient(
10 kmsKeyLocation,
11 _keyVaultNamespace,
12 schema);
13
14 var collection = autoEncryptingClient
15 .GetDatabase(_medicalRecordsNamespace.DatabaseNamespace.DatabaseName)
16 .GetCollection<BsonDocument>(_medicalRecordsNamespace.CollectionName);
17
18 var ssnQuery = Builders<BsonDocument>.Filter.Eq("ssn", __sampleSsnValue);
19
20 // Upsert (update document if found, otherwise create it) a document into the collection
21 var medicalRecordUpdateResult = await collection
22 .UpdateOneAsync(ssnQuery, new BsonDocument("$set", __sampleDocFields), new UpdateOptions() { IsUpsert = true });
23
24 if (!medicalRecordUpdateResult.UpsertedId.IsBsonNull)
25 {
26 Console.WriteLine("Successfully upserted the sample document!");
27 }
28
29 // Query by SSN field with auto-encrypting client
30 var result = collection.Find(ssnQuery).Single();
31
32 Console.WriteLine($"Encrypted client query by the SSN (deterministically-encrypted) field:\n {result}\n");
33}

What's happening here? First, we use the JsonSchemaCreator class to construct our schema. Then, we create an auto-encrypting client using the CreateAutoEncryptingClient() method. Next, lines 14-16 set the working database and collection we'll be interacting with. Finally, we upsert a medical record using our sample data, then retrieve it with the auto-encrypting client.

Prior to inserting this new patient record, the CSFLE-enabled client automatically encrypts the appropriate fields as established in our JSON schema.

If you like diagrams, here's what's happening:

Diagram that shows the data flow for a write of field-level encrypted data
Flow of an encrypted write with field-level encrypted data

When retrieving the patient's data, it is decrypted by the client. The nicest part about enabling CSFLE in your application is that the queries don't change, meaning the driver methods you're already familiar with can still be used.

For the diagram people:

Diagram showing how MongoDB queries encrypted fields.
Flow of an encrypted read, decrypting field-level data

To see this in action, we just have to modify the main program slightly so that we can call the EncryptedWriteAndReadAsync() method.

Back in the Program.cs file, add the following code (new additions highlighted):

1// Program.cs
2
3using System;
4using System.IO;
5using MongoDB.Driver;
6
7namespace EnvoyMedSys
8{
9 public enum KmsKeyLocation
10 {
11 Local,
12 }
13
14 class Program
15 {
16 public static void Main()
17 {
18 var connectionString = "PASTE YOUR MONGODB CONNECTION STRING/ATLAS URI HERE";
19 var keyVaultNamespace = CollectionNamespace.FromFullName("encryption.__keyVault");
20
21 var kmsKeyHelper = new KmsKeyHelper(
22 connectionString: connectionString,
23 keyVaultNamespace: keyVaultNamespace);
24 var autoEncryptHelper = new AutoEncryptHelper(
25 connectionString: connectionString,
26 keyVaultNamespace: keyVaultNamespace);
27
28 string kmsKeyIdBase64;
29
30 // Ensure GenerateLocalMasterKey() only runs once!
31 if (!File.Exists("../../../master-key.txt"))
32 {
33 kmsKeyHelper.GenerateLocalMasterKey();
34 }
35
36 kmsKeyIdBase64 = kmsKeyHelper.CreateKeyWithLocalKmsProvider();
37 autoEncryptHelper.EncryptedWriteAndReadAsync(kmsKeyIdBase64, KmsKeyLocation.Local);
38
39 Console.ReadKey();
40 }
41 }
42}

Alright, this is it! Save your files and then run your program. After a short wait, you should see the following console output:

Console output showing a successfully upserted document, followed by a properly decrypted response.
Console output of an encrypted write and read

It works! The console output you see has been decrypted correctly by our CSFLE-enabled MongoClient. We can also verify that this patient record has been properly saved to our database. Logging into my Atlas cluster, I see Takeshi's patient record stored securely, with the specified fields encrypted:

Encrypted patient record stored in MongoDB Atlas.
Encrypted patient record stored in MongoDB Atlas

#Bonus: What's the Difference with a Non-Encrypted Client?

To see how these queries perform when using a non-encrypting client, let's add one more method to the AutoEncryptHelper class. Right after the EncryptedWriteAndReadAsync() method, add the following:

1// AutoEncryptHelper.cs
2
3public void QueryWithNonEncryptedClient()
4{
5 var nonAutoEncryptingClient = new MongoClient(_connectionString);
6 var collection = nonAutoEncryptingClient
7 .GetDatabase(_medicalRecordsNamespace.DatabaseNamespace.DatabaseName)
8 .GetCollection<BsonDocument>(_medicalRecordsNamespace.CollectionName);
9 var ssnQuery = Builders<BsonDocument>.Filter.Eq("ssn", __sampleSsnValue);
10
11 var result = collection.Find(ssnQuery).FirstOrDefault();
12 if (result != null)
13 {
14 throw new Exception("Expected no document to be found but one was found.");
15 }
16
17 // Query by name field with a normal non-auto-encrypting client
18 var nameQuery = Builders<BsonDocument>.Filter.Eq("name", __sampleNameValue);
19 result = collection.Find(nameQuery).FirstOrDefault();
20 if (result == null)
21 {
22 throw new Exception("Expected the document to be found but none was found.");
23 }
24
25 Console.WriteLine($"Query by name (non-encrypted field) using non-auto-encrypting client returned:\n {result}\n");
26}

Here, we instantiate a standard MongoClient with no auto-encryption settings. Notice that we query by the non-encrypted name field; this is because we can't query on encrypted fields using a MongoClient without CSFLE enabled.

Finally, add a call to this new method in the Program.cs file:

1// Program.cs
2
3// Comparison query on non-encrypting client
4autoEncryptHelper.QueryWithNonEncryptedClient();

Save all your files, then run your program again. You'll see your last query returns an encrypted patient record, as expected. Since we are using a non CSFLE-enabled MongoClient, no decryption happens, leaving only the non-encrypted fields legible to us:

Query output using a non CSFLE-enabled MongoClient. Since no decryption happens, the data is properly returned in an encrypted state.
Query output using a non CSFLE-enabled MongoClient. Since no decryption happens, the data is properly returned in an encrypted state.

#Let's Recap

Cheers! You've made it this far!

Poe happily pouring a drink, signaling excitement with eyebrows
Really, pat yourself on the back. This was a serious tutorial!

This tutorial walked you through:

  • Creating a .NET Core console application.
  • Installing dependencies needed to enable client-side field level encryption for your .NET core app.
  • Creating a local master key.
  • Creating a data encryption key.
  • Constructing a JSON Schema to establish which fields to encrypt.
  • Configuring a CSFLE-enabled MongoClient.
  • Performing an encrypted read and write of a sample patient record.
  • Performing a read using a non-CSFLE-enabled MongoClient to see the difference in the retrieved data.

With this knowledge of client-side field level encryption, you should be able to better secure applications and understand how it works!

I hope this tutorial made client-side field level encryption simpler to integrate into your .NET application! If you have any further questions or are stuck on something, head over to the MongoDB Community Forums and start a topic. A whole community of MongoDB engineers (including the DevRel team) and fellow developers are sure to help!

In case you want to learn a bit more, here are the resources that were crucial to helping me write this tutorial:

Rate this article
MongoDB Icon
  • Developer Hub
  • Documentation
  • University
  • Community Forums

© MongoDB, Inc.