Using Newtonsoft JsonConverter to Encrypt JSON object

1.1k views Asked by At

I'm developing a project that will require me to include credentials for things like an SMTP server. I'd like to store this information along with the complete details of the endpoint in an embedded JSON file, but I would like to have that information encrypted and then let my application decrypt it when it needs to establish a connection and log in. The JSON structure looks something like this:

{
    "Endpoints" : [
        {
            "Endpoint" : {
                "Host": "smtp.mydomain.tld",
                "Port": 587,
                "Username": "[email protected]",
                "Password": "mYp@s$w0?d"
            }
        }
    ]
}

While what I'd really like to have actually stored in the file would look something like this:

{
    "Endpoints" : [
        {
            "Endpoint" : "<BASE64_ENCODED_STRING>"
        }
    ]
}

Using Newtonsoft's Json.NET, I've built the class object/properties to desriealize this structure:

<JsonProperty("Endpoints")>
Public Property Endpoints As List(Of EndpointContainer) = Nothing

Public Class EndpointContainer
    <EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
    Private Const EncryptedPrefix As String = "myappcipher:"

    <EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
    <JsonProperty("Endpoint")> <JsonConverter(GetType(EndpointProtector))>
    Public Property Endpoint As Endpoint = Nothing
End Class

And I've built the inherited JsonConverter class ("EndpointProtector") like this:

Public Class EndpointProtector
    Inherits JsonConverter

    Public Sub New()
        Using SHAEncryption = New SHA256Managed()
            _EncryptionKey = SHAEncryption.ComputeHash(Encoding.UTF8.GetBytes(TestEncryptionKey))
        End Using
    End Sub

    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
        Dim clearText As String = JsonConvert.SerializeObject(value)

        If clearText Is Nothing Then
            Throw New ArgumentNullException(NameOf(clearText))
        End If

        writer.WriteValue(EncryptEndpoint(clearText))
    End Sub

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
        Dim DecryptString As String = TryCast(reader.Value, String)

        If String.IsNullOrEmpty(DecryptString) Then
            Return reader.Value
        ElseIf Not DecryptString.StartsWith(EncryptedPrefix, StringComparison.OrdinalIgnoreCase) Then
            Return DecryptString
        Else
            Return DecryptEndpoint(DecryptString)
        End If
    End Function

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Throw New NotImplementedException()
    End Function
End Class

Currently I have the JSON file itself with the full object definition (as in the first code block). When my application reads that JSON, it correctly moves to the overridden ReadJson() method I have, but the reader.Value is null (Nothing), so it never actually gets to the DecryptEndpoint() method. Of course, that means there's nothing to encrypt, so the application won't even step into the WriteJson() method.

I've tried a couple of variations, including making the Endpoint property into a private variable with a generic Object type, and then having a separate public property with the <JsonIgnore> decoration to "read" from that, but nothing seems to get me where I need to be. I'm sure I'm overlooking something here, but I can't seem to figure out why it's not getting anything at all.


I looked at a few other SO questions like Encrypt and JSON Serialize an object, but I've still not yet been able to figure out quite where I've gone wrong here.


NOTE: I intentionally didn't include the code for the EncryptEndpoint() or DecryptEndpoint() methods here simply because the code is never making it that far in the process. If you feel it's needed to fully answer the question, please let me know.

1

There are 1 answers

2
Power Mouse On BEST ANSWER

this is a linqpad example of working encrypt/decrypt base on JsonAttribute

void Main()
{
    string str = "";
    var t = new Test() { encName = "some long text some long text some long text", Name = "test" };
    JsonSerializerSettings theJsonSerializerSettings = new JsonSerializerSettings();
    theJsonSerializerSettings.TypeNameHandling = TypeNameHandling.None;

    str = JsonConvert.SerializeObject(t, theJsonSerializerSettings).Dump();
    JsonConvert.DeserializeObject<Test>(str, theJsonSerializerSettings).Dump();
}



public class Test
{
    [JsonConverter(typeof(EncryptingJsonConverter))]
    public string encName { get; set; }
    public string Name { get; set; }
}

/// <summary>[JsonConverter(typeof(EncryptingJsonConverter), string 32byte array)]</summary>
public class EncryptingJsonConverter : JsonConverter
{
    private readonly byte[] _encryptionKeyBytes;
    private readonly string _encryptionKeyString;
    ///<summary>Key must be 32char length</summary>
    public EncryptingJsonConverter()
    {
        string encryptionKey = "E546C8DF278CD5931069B522E695D4F2"; //get from config
        if (string.IsNullOrEmpty(encryptionKey))
            throw new ArgumentNullException(nameof(encryptionKey));

        _encryptionKeyString = encryptionKey;
        _encryptionKeyBytes = Convert.FromBase64String(encryptionKey);

    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var stringValue = (string)value;
        if (string.IsNullOrEmpty(stringValue))
        {
            writer.WriteNull();
            return;
        }
        //string enc = stringValue.Encrypt(_encryptionKeyString);
        string enc = Crypto.Encrypt(stringValue, _encryptionKeyBytes);
        writer.WriteValue(enc);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var value = reader.Value as string;
        if (string.IsNullOrEmpty(value))
            return reader.Value;

        try
        {
            //return value.Decrypt(_encryptionKeyString);
            return Crypto.Decrypt(value, _encryptionKeyBytes);
        }
        catch
        {
            return string.Empty;
        }
    }

    /// <inheritdoc />
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(string);
    }
}


public static class Crypto
{
    public static string Encrypt(this string text, string key)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("Key must have valid value.", nameof(key));
        if (string.IsNullOrEmpty(text))
            throw new ArgumentException("The text must have valid value.", nameof(text));

        var buffer = Encoding.UTF8.GetBytes(text);
        var hash = SHA512.Create();
        var aesKey = new byte[24];
        Buffer.BlockCopy(hash.ComputeHash(Encoding.UTF8.GetBytes(key)), 0, aesKey, 0, 24);

        using (var aes = Aes.Create())
        {
            if (aes == null)
                throw new ArgumentException("Parameter must not be null.", nameof(aes));

            aes.Key = aesKey;

            using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
            using (var resultStream = new MemoryStream())
            {
                using (var aesStream = new CryptoStream(resultStream, encryptor, CryptoStreamMode.Write))
                using (var plainStream = new MemoryStream(buffer))
                {
                    plainStream.CopyTo(aesStream);
                }

                var result = resultStream.ToArray();
                var combined = new byte[aes.IV.Length + result.Length];
                Array.ConstrainedCopy(aes.IV, 0, combined, 0, aes.IV.Length);
                Array.ConstrainedCopy(result, 0, combined, aes.IV.Length, result.Length);

                return Convert.ToBase64String(combined);
            }
        }
    }

    public static string Decrypt(this string encryptedText, string key)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("Key must have valid value.", nameof(key));
        if (string.IsNullOrEmpty(encryptedText))
            throw new ArgumentException("The encrypted text must have valid value.", nameof(encryptedText));

        var combined = Convert.FromBase64String(encryptedText);
        var buffer = new byte[combined.Length];
        var hash = new SHA512CryptoServiceProvider();
        var aesKey = new byte[24];
        Buffer.BlockCopy(hash.ComputeHash(Encoding.UTF8.GetBytes(key)), 0, aesKey, 0, 24);

        using (var aes = Aes.Create())
        {
            if (aes == null)
                throw new ArgumentException("Parameter must not be null.", nameof(aes));

            aes.Key = aesKey;

            var iv = new byte[aes.IV.Length];
            var ciphertext = new byte[buffer.Length - iv.Length];

            Array.ConstrainedCopy(combined, 0, iv, 0, iv.Length);
            Array.ConstrainedCopy(combined, iv.Length, ciphertext, 0, ciphertext.Length);

            aes.IV = iv;

            using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
            using (var resultStream = new MemoryStream())
            {
                using (var aesStream = new CryptoStream(resultStream, decryptor, CryptoStreamMode.Write))
                using (var plainStream = new MemoryStream(ciphertext))
                {
                    plainStream.CopyTo(aesStream);
                }

                return Encoding.UTF8.GetString(resultStream.ToArray());
            }
        }
    }

    

    public static string Encrypt(string text, byte[] key)
    {
        //string keyString = "encrypt123456789";
        //var key = Encoding.UTF8.GetBytes(keyString);//16 bit or 32 bit key string
        using (var aesAlg = Aes.Create())
        {
            using (var encryptor = aesAlg.CreateEncryptor(key, aesAlg.IV))
            {
                using (var msEncrypt = new MemoryStream())
                {
                    using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    using (var swEncrypt = new StreamWriter(csEncrypt))
                    {
                        swEncrypt.Write(text);
                    }

                    var iv = aesAlg.IV;

                    var decryptedContent = msEncrypt.ToArray();

                    var result = new byte[iv.Length + decryptedContent.Length];

                    Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
                    Buffer.BlockCopy(decryptedContent, 0, result, iv.Length, decryptedContent.Length);

                    return Convert.ToBase64String(result);
                }
            }
        }

    }

    public static string Decrypt(string cipherText, byte[] key)
    {
        var fullCipher = Convert.FromBase64String(cipherText);

        var iv = new byte[16];
        var cipher = new byte[fullCipher.Length - iv.Length];//new byte[16];

        Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length);
        Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, cipher.Length);
        //var key = Encoding.UTF8.GetBytes(keyString);//same key string

        using (var aesAlg = Aes.Create())
        {
            using (var decryptor = aesAlg.CreateDecryptor(key, iv))
            {
                string result;
                using (var msDecrypt = new MemoryStream(cipher))
                {
                    using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                    {
                        using (var srDecrypt = new StreamReader(csDecrypt))
                        {
                            result = srDecrypt.ReadToEnd();
                        }
                    }
                }

                return result;
            }
        }
    }

}

to activate encryption just add a tag

[JsonConverter(typeof(EncryptingJsonConverter))]

enter image description here