Home > front end >  Decryption Stops working after convert to json and convert back
Decryption Stops working after convert to json and convert back

Time:12-27

I have an encryption class that I'm using in the UI and in the server.

Encryption Class

public class AesEncryption
{
    private const string Password = "464DA0FE-A4B6-4D86-B68B-C5913779918B"; //TODO: change to long string 
    
    private const int AesBlockByteSize = 128 / 8;

    private const int PasswordSaltByteSize = 128 / 8;
    private const int PasswordByteSize = 256 / 8;
    private const int PasswordIterationCount = 100;

    private static readonly Encoding StringEncoding = Encoding.UTF8;
    private static readonly RandomNumberGenerator Random = RandomNumberGenerator.Create();

    public static byte[] EncryptString(string toEncrypt)
    {
        using var aes = Aes.Create();
        var keySalt = GenerateRandomBytes(PasswordSaltByteSize);
        var key = GetKey(Password, keySalt);
        var iv = GenerateRandomBytes(AesBlockByteSize);

        using var encryptor = aes.CreateEncryptor(key, iv);
        var plainText = StringEncoding.GetBytes(toEncrypt);
        var cipherText = encryptor
            .TransformFinalBlock(plainText, 0, plainText.Length);

        var result = MergeArrays(keySalt, iv, cipherText);
        return result;
    }

    public static string EncryptToBase64(string toEncrypt)
    {
        var bytes = EncryptString(toEncrypt);
        return Convert.ToBase64String(bytes);
    }

    public static string DecryptBase64(string base64)
    {
        var bytes = Convert.FromBase64String(base64);
        return DecryptString(bytes);
    }

    public static string DecryptString(byte[] encryptedData)
    {
        using var aes = Aes.Create();
        var keySalt = encryptedData.Take(PasswordSaltByteSize).ToArray();
        var key = GetKey(Password, keySalt);
        var iv = encryptedData
            .Skip(PasswordSaltByteSize).Take(AesBlockByteSize).ToArray();
        var cipherText = encryptedData
            .Skip(PasswordSaltByteSize   AesBlockByteSize).ToArray();

        using var encryptor = aes.CreateDecryptor(key, iv);
        var decryptedBytes = encryptor
            .TransformFinalBlock(cipherText, 0, cipherText.Length);
        return StringEncoding.GetString(decryptedBytes);
    }

        private static byte[] GetKey(string password, byte[] passwordSalt)
    {
        var keyBytes = StringEncoding.GetBytes(password);

        using var derivator = new Rfc2898DeriveBytes(
            keyBytes, passwordSalt, 
            PasswordIterationCount, HashAlgorithmName.SHA256);
        return derivator.GetBytes(PasswordByteSize);
    }

    private static byte[] GenerateRandomBytes(int numberOfBytes)
    {
        var randomBytes = new byte[numberOfBytes];
        Random.GetBytes(randomBytes);
        return randomBytes;
    }

    private static byte[] MergeArrays(params byte[][] arrays)
    {
        var merged = new byte[arrays.Sum(a => a.Length)];
        var mergeIndex = 0;
        for (int i = 0; i < arrays.GetLength(0); i  )
        {
            arrays[i].CopyTo(merged, mergeIndex);
            mergeIndex  = arrays[i].Length;
        }

        return merged;
    }

    public static string DecryptFromBase64(string base64)
    {
        var bytes = Convert.FromBase64String(base64);
        return DecryptString(bytes);
    }
}

LoginModel

public class LoginModel
{
    public LoginModel(string userName, string password)
    {
        UserName = AesEncryption.EncryptToBase64(userName);

        Password = AesEncryption.EncryptToBase64(password);
    }

    public string UserName { get; }
    public string Password { get; }

    public string GetDecryptedPassword() => AesEncryption.DecryptFromBase64(Password);
    public string GetDecryptedUserName() => AesEncryption.DecryptFromBase64(UserName);

    public void Deconstruct(out string username, out string password)
    {
        username = GetDecryptedUserName();
        password = GetDecryptedPassword();
    }
}

When I run these 2 Unit Tests everything works as expected.

[Fact]
public void ShouldEncryptAndDecrypt()
{
    var password = "Test";

    var encryption = AesEncryption.EncryptToBase64(password);

    var decrypt = AesEncryption.DecryptFromBase64(encryption);

    decrypt.Should().Be(password);
}

[Fact]
public void LoginModel_ShouldDecriptAndEncrypt()
{
    var userName = "test";
    var password = "hopethisworks";
    var model = new LoginModel(userName,password);
    
    userName.Should().NotBe(model.UserName);
    password.Should().NotBe(model.Password);


    var (decryptedUserName, decryptedPassword) = model;
    userName.Should().Be(decryptedUserName);
    password.Should().Be(decryptedPassword);
}

But when I run this test it stops decrypting

[Fact]
public void LoginModel_ConverterAndParsed()
{
    var userName = "test";
    var password = "hopethisworks";
    var model = new LoginModel(userName,password);
    var json = JsonConvert.SerializeObject(model);
    var parsedJsonModel = JsonConvert.DeserializeObject<LoginModel>(json);

    var (decryptedUserName, decryptedPassword) = parsedJsonModel;
    userName.Should().Be(decryptedUserName);
    password.Should().Be(decryptedPassword);
    
}

I am using Newtonsoft.Json XUnit and FluentAssertions

My question: What would cause this to stop working when I Serialize and Deserialize the LoginModel?

CodePudding user response:

In constructor of login model you encrypt username and password, so UserName and Password properties are in encrypted form and stored like this in json. However, to deserialize - JSON.NET has no other choice than to call that constructor again, because there is no other constructor and those properties are readonly. So on deserialization it calls this constructor again, passing encrypted username and password, and they are encrypted once again. So in result you have double encrypted username and password.

One solution is to add public constructor and make properties public too. Then JSON.NET will use that public constructor and set property values, without double encryption.

public class LoginModel
{
    public LoginModel(){

    }
    public LoginModel(string userName, string password)
    {
        UserName = AesEncryption.EncryptToBase64(userName);

        Password = AesEncryption.EncryptToBase64(password);
    }

    public string UserName { get; set;}
    public string Password { get; set;}

    public string GetDecryptedPassword() => AesEncryption.DecryptFromBase64(Password);
    public string GetDecryptedUserName() => AesEncryption.DecryptFromBase64(UserName);

    public void Deconstruct(out string username, out string password)
    {
        username = GetDecryptedUserName();
        password = GetDecryptedPassword();
    }
}

If that's not possible, you can do this:

public class LoginModel
{
    [JsonProperty("UserName")]
    private readonly string _userName;
    [JsonProperty("Password")]
    private readonly string _password;
    [JsonConstructor]
    private LoginModel(){

    }
    public LoginModel(string userName, string password)
    {
        _userName = AesEncryption.EncryptToBase64(userName);
        _password = AesEncryption.EncryptToBase64(password);
    }
    [JsonIgnore]
    public string UserName => _userName;
    [JsonIgnore]
    public string Password => _password;

    public string GetDecryptedPassword() => AesEncryption.DecryptFromBase64(Password);
    public string GetDecryptedUserName() => AesEncryption.DecryptFromBase64(UserName);

    public void Deconstruct(out string username, out string password)
    {
        username = GetDecryptedUserName();
        password = GetDecryptedPassword();
    }
}

Basically we guiding JSON.NET to not use that constructor and instead use private parameterless one, then set our readonly fields.

  • Related