Brian Roger's custom contract resolver uses a custom IValueProvider
to inject logic to encrypt and decrypt string-valued properties. You would like to generalize this to encrypt and decrypt any type of property by doing a nested serialization or deserialization to or from JSON, and then encrypting or decrypting the resulting JSON string. Unfortunately, a custom value provider is not really suited for this purpose. The responsibility of a value provider is to set and get a value from the database. It isn't designed to do arbitrarily complex transformations of that value, or to do nested serializations. (The current JsonSerializer
isn't even passed into the value provider.)
Instead of a value provider, a custom JsonConverter
can be applied by the contact resolver to do the necessary nested serialization and encryption. A JsonConverter
has access to the current serializer, and its responsibilities include transforming JSON from and to the final data model, so it is better suited to this task.
First, define the following attribute and interface:
public interface IEncryptionService
{
/// <summary>
/// Encrypt the incoming string into another Utf8 string using whatever algorithm, password and salt are appropiate.
/// </summary>
/// <param name="input">The string to be encrypted</param>
/// <returns>The encypted string</returns>
public string Encrypt(string input);
/// <summary>
/// Decrypt the incoming string into another Utf8 string using whatever algorithm, password and salt are appropiate.
/// </summary>
/// <param name="input">The encrypted string</param>
/// <returns>The decrypted string value</returns>
public string Decrypt(string input);
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class JsonEncryptAttribute : Attribute
{
}
Here I assume that the required _encryptionKey
and salt will be encapsulated inside your concrete implementation of IEncryptionService
.
Next, define the following contract resolver:
public class EncryptedPropertyContractResolver : DefaultContractResolver
{
IEncryptionService EncryptionService { get; }
public EncryptedPropertyContractResolver(IEncryptionService encryptionService) => this.EncryptionService = encryptionService ?? throw new ArgumentNullException(nameof(encryptionService));
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
return ApplyEncryption(property);
}
protected override JsonProperty CreatePropertyFromConstructorParameter(JsonProperty matchingMemberProperty, ParameterInfo parameterInfo)
{
var property = base.CreatePropertyFromConstructorParameter(matchingMemberProperty, parameterInfo);
return ApplyEncryption(property, matchingMemberProperty);
}
JsonProperty ApplyEncryption(JsonProperty property, JsonProperty matchingMemberProperty = null)
{
if ((matchingMemberProperty ?? property).AttributeProvider.GetAttributes(typeof(JsonEncryptAttribute), true).Any())
{
if (property.ItemConverter != null)
throw new NotImplementedException("property.ItemConverter");
property.Converter = new EncryptingJsonConverter(EncryptionService, property.Converter);
}
return property;
}
class EncryptingJsonConverter : JsonConverter
{
IEncryptionService EncryptionService { get; }
JsonConverter InnerConverter { get; }
public EncryptingJsonConverter(IEncryptionService encryptionService, JsonConverter innerConverter)
{
this.EncryptionService = encryptionService ?? throw new ArgumentNullException(nameof(encryptionService));
this.InnerConverter = innerConverter;
}
public override bool CanConvert(Type objectType) => throw new NotImplementedException(nameof(CanConvert));
object ReadInnerJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (InnerConverter?.CanRead == true)
return InnerConverter.ReadJson(reader, objectType, existingValue, serializer);
else
return serializer.Deserialize(reader, objectType);
}
public void WriteInnerJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (InnerConverter?.CanWrite == true)
InnerConverter.WriteJson(writer, value, serializer);
else
serializer.Serialize(writer, value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
else if (reader.TokenType != JsonToken.String)
throw new JsonSerializationException(string.Format("Unexpected token type {0}", reader.TokenType));
var encryptedString = (string)reader.Value;
var jsonString = EncryptionService.Decrypt(encryptedString);
using (var subReader = new JsonTextReader(new StringReader(jsonString)))
return ReadInnerJson(subReader.MoveToContentAndAssert(), objectType, existingValue, serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
using var textWriter = new StringWriter();
using (var subWriter = new JsonTextWriter(textWriter))
WriteInnerJson(subWriter, value, serializer);
var encryptedString = EncryptionService.Encrypt(textWriter.ToString());
writer.WriteValue(encryptedString);
}
}
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
And now you can apply [JsonEncrypt]
to your model as shown in your question, and serialize and deserialize instances of it as follows:
var resolver = new EncryptedPropertyContractResolver(_encryptionService);
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
};
var encryptedJson = JsonConvert.SerializeObject(model, Formatting.Indented, settings);
var model2 = JsonConvert.DeserializeObject<TopLevelModel>(encryptedJson, settings);
Notes:
Collections for which JsonPropertyAttribute.ItemConverterType
is set are not implemented.
I have not tested encryption of parameterized constructor arguments.
This answer doesn't address how the IEncryptionService
should be implemented. Secure implementation of encryption algorithms is outside its scope.
Demo fiddle here.