https://stackoverflow.com/questions/67109653/json-deserialize-encrypted-object

CSharp · Console

/Program.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters;
using System.IO;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Security.Cryptography;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;	

using NUnit.Framework; // Throws NUnit.Framework.AssertionException

//https://stackoverflow.com/questions/67109653/json-deserialize-encrypted-object

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

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

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

	public partial class TopLevelModel
	{    
		[JsonEncrypt]
		[JsonConverter(typeof(RequiresConverterConverter))]
		public RequiresConverter RequiresConverter { get; set; }
	}

	public class RequiresConverter
	{
		readonly string value;
		
		public RequiresConverter(string value) { this.value = value; }
		public string GetValue() => value;
	}

	class RequiresConverterConverter : JsonConverter<RequiresConverter>
	{
		public override void WriteJson(JsonWriter writer, RequiresConverter value, JsonSerializer serializer)
		{
			writer.WriteValue(value.GetValue());
		}

		public override RequiresConverter ReadJson(JsonReader reader, Type objectType, RequiresConverter existingValue, bool hasExistingValue, 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));
			return new RequiresConverter((string)reader.Value);
		}
	}

	// Rewritten from this answer https://stackoverflow.com/a/29240043/3744182
	// By https://stackoverflow.com/users/10263/brian-rogers
	// To https://stackoverflow.com/questions/29196809/how-can-i-encrypt-selected-properties-when-serializing-my-objects
	// To use a JsonConverter instead of a value provider.

    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
	{
	}

	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;
        }
	}

	public class DummyEncryptionService : IEncryptionService
	{
		// Some dummy test implementation.  Do not use in production!
		public string Encrypt(string input) =>
			Convert.ToBase64String(Encoding.UTF8.GetBytes(input));
		
		public string Decrypt(string input) =>
			Encoding.UTF8.GetString(Convert.FromBase64String(input));
	}

	public class AesEncryptionService : IEncryptionService
	{
		// Some test implementation.
		// Adapted from this answer https://stackoverflow.com/a/29240043/3744182
		// By https://stackoverflow.com/users/10263/brian-rogers
		// To https://stackoverflow.com/questions/29196809/how-can-i-encrypt-selected-properties-when-serializing-my-objects.
		// Not security reviewed!

		readonly private byte[] encryptionKeyBytes;
		
		public AesEncryptionService(string encryptionKey)
		{
			if (encryptionKey == null)
				throw new ArgumentNullException("encryptionKey");

			// This is needed to ensure the key is exactly 256 bits long
			using (SHA256Managed sha = new SHA256Managed())
			{
				this.encryptionKeyBytes = 
					sha.ComputeHash(Encoding.UTF8.GetBytes(encryptionKey));
			}
		}
	
		public string Encrypt(string input)
		{
			byte[] buffer = Encoding.UTF8.GetBytes(input);
			
			using (MemoryStream inputStream = new MemoryStream(buffer, false))
				using (MemoryStream outputStream = new MemoryStream())
				using (AesManaged aes = new AesManaged { Key = encryptionKeyBytes })
			{
				byte[] iv = aes.IV;  // first access generates a new IV
				outputStream.Write(iv, 0, iv.Length);
				outputStream.Flush();
				
				ICryptoTransform encryptor = aes.CreateEncryptor(encryptionKeyBytes, iv);
				using (CryptoStream cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write))
				{
					inputStream.CopyTo(cryptoStream);
				}
				
				return Convert.ToBase64String(outputStream.ToArray());
			}
		}
		
		public string Decrypt(string input)
		{
			byte[] buffer = Convert.FromBase64String(input);
			
			using (MemoryStream inputStream = new MemoryStream(buffer, false))
				using (MemoryStream outputStream = new MemoryStream())
				using (AesManaged aes = new AesManaged { Key = encryptionKeyBytes })
			{
				byte[] iv = new byte[16];
				int bytesRead = inputStream.Read(iv, 0, 16);
				if (bytesRead < 16)
				{
					throw new CryptographicException("IV is missing or invalid.");
				}
				
				ICryptoTransform decryptor = aes.CreateDecryptor(encryptionKeyBytes, iv);
				using (CryptoStream cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read))
				{
					cryptoStream.CopyTo(outputStream);
				}
				
				string decryptedValue = Encoding.UTF8.GetString(outputStream.ToArray());
				return decryptedValue;
			}
		}
	}

    class TestClass
    {
        public static void Test()
        {
			var model = new TopLevelModel
			{
				PrivateText = "secret",
				Note = "public",
				
				FlatStringModels = new List<TopLevelModel.InternalFlatStringModel>
				{
					new() { PrivateText = "secret", Note = "public" },
				},
				
				RequiresConverter = new RequiresConverter("secret"),
			};
			var plainJson = JsonConvert.SerializeObject(model, Formatting.Indented);
			
			var _encryptionService = new AesEncryptionService("test password");

			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);
			
			Console.WriteLine(plainJson);
			Console.WriteLine(encryptedJson);
			Assert.IsTrue(!encryptedJson.Contains("secret"));

			var plainJson2 = JsonConvert.SerializeObject(model2, Formatting.Indented);
			Assert.AreEqual(plainJson, plainJson2);
		}	
	}

	public class Program
	{
		public static void Main()
		{
			Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , GetNetCoreVersion());
			Console.WriteLine("{0} version: {1}", typeof(JsonSerializer).Assembly.GetName().Name,  typeof(JsonSerializer).Assembly.FullName);
			Console.WriteLine();

			try
			{
				TestClass.Test();
			}
			catch (Exception ex)
			{
				Console.WriteLine("Failed with unhandled exception: ");
				Console.WriteLine(ex);
				throw;
			}
		}
		
		public static string GetNetCoreVersion()
		{
			//https://techblog.dorogin.com/how-to-detect-net-core-version-in-runtime-ecd65ad695be
			//
			var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;
			var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
			int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
			if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
				return assemblyPath[netCoreAppIndex + 1];
			return null;
		}
	}
;