0

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": "user@mydomain.tld",
                "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.

G_Hosa_Phat
  • 976
  • 2
  • 18
  • 38
  • 1
    Does [Using Json.NET, how can I encrypt selected properties of any type when serializing my objects?](https://stackoverflow.com/q/67109653/3744182) answer your question? – dbc Apr 22 '22 at 20:52
  • Well, my Google-fu has apparently failed me once again. That's one I never ran across, but I'll run some tests and see if it produces my desired results. If so, I'll let you know. Thanks, @dbc – G_Hosa_Phat Apr 22 '22 at 21:18
  • @dbc - Thanks for the suggestion. The answer you recommended does, in fact, encrypt individual properties of a JSON object, but it's not *quite* what I was hoping to achieve. As stated in my question, I was hoping to basically take the entire JSON object as a raw JSON string, encrypt ***that*** for storage in the file as a Base64 string, then be able to decrypt ***that*** Base64 string back to the JSON object for deserialization. – G_Hosa_Phat Apr 24 '22 at 00:13
  • 1
    That's what `EncryptingJsonConverter` does. It serializes the entire incoming value (here presumably your `Endpoint` object) to a string, encrypts the string, then writes the resulting byte array to the JSON stream as Base64. – dbc Apr 24 '22 at 13:26
  • 1
    Demo fiddle here: https://dotnetfiddle.net/m8ra96 – dbc Apr 24 '22 at 14:44
  • Thanks for that. I must have something in my "production" implementation that's causing a problem. I started over with a temporary project and tried again with *just* your code (converted to VB.NET) and it worked pretty much exactly the way I'm wanting, so I'm going back through my "live" code to find where I messed up. Regardless, it looks like that *is* what I was trying to accomplish. I'm not sure if this, then, constitutes a duplicate question. – G_Hosa_Phat Apr 25 '22 at 02:15
  • 1
    Note that Base64 is much closer to "obfuscation" than "encryption" since it doesn't involve any secrets. A sufficiently motivated adversary would likely be able to recognize Base64 and decode it. – Craig Apr 25 '22 at 13:43
  • Thanks for the reminder. I'm using the `AesEncryptionService` object, so that should help at least. Unfortunately, I'm still having problems with my "live" implementation for some reason, though. When it goes to deserialize the JSON, the `ReadJson()` method is hitting the exception condition for a `StartObject` token type. I'm still fumbling around with it, but I'm not sure how I've managed to "break" it. – G_Hosa_Phat Apr 25 '22 at 17:34
  • AHHH! I think I (at least partially) figured a bit of it out. The JSON I'm working with hasn't actually had the encryption applied to it yet, so the encryption service is failing when it tries to read the nested objects as if they were. I took the resolver out of the initial deserialization and it parsed correctly (with plain values). A bit more testing to do, but it's finally starting to come along. – G_Hosa_Phat Apr 25 '22 at 17:50
  • 1
    I just have one more thing I want to figure out how to implement in there, but I believe I got it pretty much figured out. I had to create a separate, `WriteOnly` property in my object model to handle unencrypted JSON from the file that populates the "usable" `Endpoint` object. I'm restructuring a few things now, but it all seems to be working the way I was wanting, so thank you VERY much, @dbc. The last thing I want to try to do is to add an optional property to the `JsonEncryptAttribute` for a `DataProtectionScope` that the encryption service(s) can use. – G_Hosa_Phat Apr 26 '22 at 13:56

1 Answers1

0

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

Power Mouse
  • 727
  • 6
  • 16
  • Thank you for that. It looks similar to what I had implemented above. There are a few elements in there that look interesting (I've not seen the use of `Array.ConstrainedCopy` before), so I'm definitely going to take a deeper look at it. However, I've worked through my conversion/implementation issues from the solution that @dbc posted in the comments so I'll probably stick with that as an *overall* solution. – G_Hosa_Phat Apr 27 '22 at 00:40
  • please, don't forget to mark an answer – Power Mouse Apr 27 '22 at 13:26