1

I would like to generalize this answer by Brian Rogers to How can I encrypt selected properties when serializing my objects? to make Json.NET automatically encrypt any type of property to which an attribute [JsonEncrypt] has been applied -- not just string valued properties which are supported by that answer. How can this be implemented?

Currently I am trying to achieve attribute based encryption/decryption by calling custom serializer and providers like this:

[Test]
public void MessageJsonSerializer_TopLevelModel_Success()
{    
    // arrange    
    var key = _fixture.Create<Guid>().ToString().ToUpper();    
    var model = _fixture.Create<TopLevelModel>();    
    var serializer = new MessageJsonSerializer<TopLevelModel>(_encryptionService);    

    // act    
    var serializedEncrypted = serializer.SerializeEncrypted(model, key);    
    var deserializedDecrypted = serializer.DeserializeEncrypted(serializedEncrypted, key);    

    // assert    
    model.Should().BeEquivalentTo(deserializedDecrypted, "After de/serialization models should be equal");    
    serializer.Invoking(x => x.Deserialize(serializedEncrypted))        
        .Should()        
        .Throw<SerializationException>("Full sub-class will be converted to encrypted string - couldn't be deserialized");}

Where Models are:

public class TopLevelModel
{    
    [JsonEncrypt]    
    public string PrivateText { get; set; }    
    public string Note { get; set; }    

    [JsonEncrypt]
    public IEnumerable<InternalFlatStringModel> FlatStringModels { get; set; }    

    public class InternalFlatStringModel : IEncryptedDTO    
    {        
        [JsonEncrypt]        
        public string PrivateText { get; set; }        
        public string Note { get; set; }    
    }
}

The provider:

public class EncryptedObjectValueProvider : IValueProvider
{
    private readonly IEncryptionService _encryptionService;
    private readonly PropertyInfo _targetProperty;
    private readonly string _encryptionKey;

    public EncryptedObjectValueProvider(IEncryptionService encryptionService, PropertyInfo targetProperty, string encryptionKey)
    {
        _encryptionService = encryptionService;
        _targetProperty = targetProperty;
        _encryptionKey = encryptionKey;

    }

    public object GetValue(object target)
    {
        var jsonString = JsonConvert.SerializeObject(_targetProperty.GetValue(target), Formatting.None);
        return _encryptionService.EncryptStringAES(jsonString, _encryptionKey);
    }
    public void SetValue(object target, object value)
    {
        // --> never hits this one
        var decryptedValue = _encryptionService.DecryptStringAES((string) value, _encryptionKey);
        var decryptedObject = JsonConvert.DeserializeObject(decryptedValue);
        _targetProperty.SetValue(target, decryptedObject);
    }
}

The problem is SetValue will never be called for nested object InternalFlatStringModel. Getting System.Runtime.Serialization.SerializationException : Error converting value %encrypted object%.

dbc
  • 104,963
  • 20
  • 228
  • 340
Andrii Horda
  • 170
  • 1
  • 16
  • Can you share a full [mcve]? I suspect that you are using some custom contract resolver adapted from the one from [this answer](https://stackoverflow.com/a/29240043/3744182) to [How can I encrypt selected properties when serializing my objects?](https://stackoverflow.com/q/29196809/3744182) but without a complete example showing how you generate your JSON we can't really tell you where you are going wrong. That answer only works for string properties; it seems you have tried to extend it to arbitrary properties and that is what is not working. – dbc Apr 15 '21 at 14:01
  • what is `JsonEncrypt`? – Daniel A. White Apr 15 '21 at 14:01
  • @dbc that's correct, sir. I'm trying to extend Brian Rogers' answer with objects and enumerable (of objects) – Andrii Horda Apr 15 '21 at 14:13
  • 1
    In that case 1) please [edit] your question to include a [mcve] showing what you have done that isn't working, and 2) please credit that answer as per https://stackoverflow.com/help/referencing. – dbc Apr 15 '21 at 14:16

1 Answers1

1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340