CSharp · Console
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;
}
}