I couldn’t find a useful .NET library for easy and robust JSON property-level encryption/decryption, so I made one.
The GitHub page covers more details, but this is the gist:
Installation:
Install-Package JsonCryption.Newtonsoft
// There's also a version for System.Text.Json, but the implementation
// for Newtonsoft.Json is better, owing to the greater feature surface
// and customizability of the latter at this time.
Configuration:
// pseudo code (assuming using Newtonsoft.Json for serialization)
container.Register<JsonSerializer>(() => new JsonSerializer()
{
ContractResolver = new JsonCryptionContractResolver(container.Resolve<IDataProtectionProvider>())
});
Usage:
var myFoo = new Foo("some important value", "something very public");
class Foo
{
[Encrypt]
public string EncryptedString { get; }
public string UnencryptedString { get; }
public Foo(string encryptedString, string unencryptedString)
{
...
}
}
var serializer = // resolve JsonSerializer
using var textWriter = ...
serializer.Serialize(textWriter, myFoo);
// pseudo output: '{ "encryptedString": "akjdfkldjagkldhtlfkjk...", "UnencryptedString": "something very public" }'
Why I need JsonCryption
My main project (not fully operational) is a .NET Core app that handles contact information for users. Being on the OCD spectrum, I wanted this data to have stronger protection than just disk-level and/or database-level encryption.
Property/field-level encryption – in addition to disk-level and database-level encryption – sounded pretty nice. But I needed to be able to easily control which fields/properties were encrypted from each object.
This project is also using Marten, which uses PostgreSQL as a document DB. Marten stores documents (C# objects, essentially) in tables with explicit lookup columns, and one column for the JSON blob. From what I could tell, the best hook offered by Marten’s API to encrypt/decrypt documents automatically is at the point of serialization/deserialization by providing an alternative ISerializer. If I encrypted the entire blob, I wouldn’t be able to query anything very well. So I needed a way to leave certain columns unencrypted when serializing – the ones that would serve as lookups in queries.
Discovery path
First Stop: Newtonsoft.Json.Encryption
This library provided a lot of inspiration. It intends to be very easy to use by requiring a single EncryptAttribute
to decorate what is to be encrypted, and it plugs into Newtonsoft.Json via the ContractResolver
approach (similar to JsonCryption above).
However, I felt that it had a few fatal flaws that would make using it a more difficult than initially meets the eye.
That it doesn’t store the Init Vector with the generated ciphertext was a non-starter for me. This requires consumers of the library to figure out how and where to store it themselves. I’m not a cryptographic expert (use JsonCryption at your own risk!), but it seems pretty standard practice to include the IV with the ciphertext to enable later decryption with just the symmetric key. In any case, this would be a bigger issue after later discoveries.
Overriding JsonConverter
Next, I came across this blog post by Thomas Freudenberg that used a slightly different approach. Rather than provide a custom ContractResolver
, he decorated each property needing encryption with a custom JsonConverter
. His approach also offered a normal way to handle the Init Vectors.
public class Settings {
[JsonConverter(typeof(EncryptingJsonConverter), "#my*S3cr3t")]
public string Password { get; set; }
}
This was interesting, but would be annoying to have to type all of that for each property needing encryption. Also, I would obviously need a way to inject the secret into the converter, rather than hard-code it here.
Nevertheless, it gave me an idea for an approach to use with .NET Core’s new System.Text.Json library…
Initial Attempt for System.Text.Json
Microsoft recently released System.Text.Json with .NET Core 3.0 as an open-source alternative to the also-open-source Newtonsoft.Json, which had been the default JSON serialization library for .NET Core up to now. Wanting to be cutting edge, and not knowing much about this new library, I started writing my solution around this.
The library has decent documentation, is open-source (as already mentioned), and enables powerful serialization customization via an unsealed public JsonConverterAttribute
. By overriding this with my own implementation, I could essentially implement Freudenberg’s approach with much less code:
public sealed class EncryptAttribute : JsonConverterAttribute
{
public EncryptAttribute() : base(typeof(EncryptedJsonConverterFactory))
{
}
}
Then I just needed to write a custom EncryptedJsonConverterFactory
to provide the correct converter given the datatype being serialized.
But this approach also carried critical issues…
- Overriding the
JsonConverterAttribute
ultimately required using a Singleton pattern rather than clean Dependency Injection - System.Text.Json currently offers no ability to serialize non-public properties, nor fields of any visibility. For most DDD scenarios, this was also a non-starter.
Newtonsoft.Json
Newtonsoft.Json offers support for serializing private to public fields and properties. It’s a well-known mature library with a highly extensible API. It’s JsonConverterAttribute
is currently sealed
, so we can’t override that… but there are better options for configuring it, anyway, in order to take advantage of Dependency Injection and other better patterns than I was forced to use with System.Text.Json
.
The good news is that the exercise of implementing a solution for System.Text.Json
forced me to develop some core logic for converting different datatypes to and from byte
arrays, which would come in handy for encrypting a wide variety of datatypes. Another issue with the other libraries and approaches I mentioned earlier is that they only handled a tiny number of potential datatypes. I wanted a set-and-forget solution that would work widely, so being able to convert all built-in types and any nested combination thereof was essential.
Adding support for Cryptography best practices
I began with a custom implementation and abstraction of the core Encrypter that I was using throughout the library. It was basic and structured largely using inspiration from the two approaches discussed earlier.
It worked.
But then I attended a great session at CodeMash 2020 called Practical Cryptography for Developers. Without getting into the weeds of cryptography, I was exposed for the first time to the concept of key/algorithm rotation and management and cryptographic best practices.
Writing these features into my library would take me far outside its immediate domain, and far outside my expertise. Surely, I thought, there must be some libraries that handle this already…
Switching to Microsoft.AspNetCore.DataProtection underneath
… yes, there is. Obviously.
The open-source package Microsoft.AspNetCore.DataProtection
was designed to provide
a simple, easy to use cryptographic API a developer can use to protect data, including key management and rotation
https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-3.1
It’s highly configurable, easy to bootstrap, built to promote testability, and built for .NET Core. It handles key management and algorithm management, written by dedicated experts in the field.
So I used that instead of my own Encrypter
.
Closing
In the end, I kept both the System.Text.Json implementation (JsonCryption.System.Text.Json
), and the Newtonsoft.Json implementation (JsonCryption.Newtonsoft
).
JsonCryption.Newtonsoft is better for the moment, allowing encryption/serialization of private to public fields and properties, shallow or nested, of (theoretically) any data type that is also serializable by Newtonsoft.Json.
Check it out. Try it out.
And tell me what you think needs changed to make it better.