JsonCryption.Utf8Json offers Field Level Encryption (FLE) when serializing/deserializing between .NET objects and JSON.
// Package Manager
Install-Package JsonCryption.Utf8Json
// .NET CLI
dotnet add package JsonCryption.Utf8Json
Field Level Encryption of C# objects during JSON serialization/deserialization should be:
- Relatively easy to use
- Powered by industry-standard cryptography best-practices
With default configuration, encrypting a field/property just requires decorating it with EncryptAttribute
, and serializing the object as usual:
// decorate properties to be encrypted
class Foo
{
[Encrypt]
public string MySecret { get; set; }
}
// serialize as normal
Foo foo = new Foo() { ... };
JsonSerializer.Serialize(foo);
More details on usage scenarios can be found below.
Currently, JsonCryption.Utf8Json is built on top of the Microsoft.AspNetCore.DataProtection
library for handling encryption-related responsibilities:
- Encryption/decryption
- Key management
- Algorithm management
- etc.
Internally, we only depend on the two interfaces IDataProtector
and IDataProtectionProvider
. If you don't want to use Microsoft's implementations, you could just depend on Microsoft.AspNetCore.DataProtection.Abstractions
and provide alternative implementations of IDataProtector
and IDataProtectionProvider
. One use case for this functionality might be creating a segregated IDataProtector
per user, potentially making it easy to support GDPR's "right to forget" user data.
JsonCryption.Utf8Json should support any type serializable by Utf8Json. If you spot a missing type or find odd behavior, please let me know (or better yet, create a PR!).
JsonCryption.Utf8Json depends on the Microsoft.AspNetCore.DataProtection
library. Therefore, you should first ensure that your DataProtection layer is configured properly.
Next, you'll need to set your default IJsonFormatterResolver
to be an instance of EncryptedResolver
, which should have a Singleton lifetime in your app.
EncryptedResolver
takes two arguments:
- An instance of
Utf8Json.IJsonFormatterResolver
- An instance of
Microsoft.AspNetCore.DataProtection.Abstractions.IDataProtectionProvider
The IJsonFormatterResolver
serves two purposes. It is used to:
- Serialize/deserialize when encryption isn't needed for a given field/property
- Do unencrypted serialization/deserialization in the encrypted chain, prior to encrypting and after decrypting the resulting JSON string
The IDataProtectionProvider
will provide an instance of IDataProtector
, which is what encrypts your data.
IJsonFormatterResolver fallbackResolver = StandardResolver.AllowPrivate;
IDataProtectionProvider dataProtectionProvider = ...;
IJsonFormatterResolver encryptedResolver = new EncryptedResolver(fallbackResolver, dataProtectionProvider);
JsonSerializer.SetDefaultResolver(encryptedResolver);
Alternatively, you can abstain from setting the default IJsonFormatterResolver
, and instead explicitly pass an instance of EncryptedResolver
whenever you want encryption functionality:
JsonSerializer.Serialize(foo, encryptedResolver);
JsonSerializer.Deserialize<Foo>(json, encryptedResolver);
Once configured, using JsonCryption.Utf8Json is just a matter of decorating the properties/fields you wish to encrypt with the EncryptAttribute
and serializing your C# objects as you normally would:
var myFoo = new Foo("If the Foo shits, wear it.", "JsonCryption.Utf8Json");
class Foo
{
[Encrypt]
public string LaunchCodes { get; }
public string FavoriteNugetPackage { get; }
public Foo(string launchCodes, string favoriteNugetPackage)
{
LaunchCodes = launchCodes;
FavoriteNugetPackage = favoriteNugetPackage;
}
}
// serializing
var bytes = JsonSerializer.Serialize(myFoo);
var json = Encoding.Utf8.GetString(bytes);
// deserializing
var fromBytes = JsonSerializer.Deserialize<Foo>(bytes);
var fromJson = JsonSerializer.Deserialize<Foo>(json);
As much as possible, I'm trying to keep annotations usage as close to parity with Utf8Json as possible. Here's a current sampling:
JsonCryption.Utf8Json resolves the constructor used during deserialization in a couple steps. It shouldn't matter whether or not the constructor is public or private. See the tests for details.
- If a constructor is decorated with
SerializationConstructorAttribute
, it's the constructor that will be used
class Foo
{
[Encrypt]
public int MyInt { get; }
// This constructor will be used
[SerializationConstructor]
private Foo() { }
public Foo(int myInt) { ... }
}
- Otherwise, we try to find the constructor with the most parameter matches (by name, case-insensitive)
class Foo
{
[Encrypt]
public int MyInt { get; }
[Encrypt]
public string MyString { get; }
public Foo() { }
public Foo(int myInt) { ... }
// This constructor will be used
public Foo(int myInt, string myString) { ... }
}
- Otherwise, we use the default constructor
After the object is rehydrated via the resolved constructor, individual serialized fields and properties not covered by the constructor will still be set.
Set the fallbackResolver
of the EncryptedResolver
to any IJsonFormatterResolver
with AllowPrivate
set to true. Then it should just work.
To customize the name used for the field/property in the resulting JSON, decorate the field/property with DataMemberAttribute
and provide a Name
:
class Foo
{
[Encrypt]
[DataMember(Name = "launchCode")]
public int MyInt { get; }
}
To ignore a given field/property, decorate it with IgnoreDataMemberAttribute
:
class Foo
{
[IgnoreDataMember]
public int MyInt { get; }
}
JsonCryption.Utf8Json is open to PRs...
Future projects/enhancements:
- Benchmarking