Eastrall / EntityFrameworkCore.DataEncryption

A plugin for Microsoft.EntityFrameworkCore to add support of encrypted fields using built-in or custom encryption providers.
MIT License
326 stars 54 forks source link

Decryption error for data on .NET v5 platform after updating to .NET v7? (not a valid Base-64 string) #50

Closed gcatwork closed 9 months ago

gcatwork commented 1 year ago

Any suggestions regarding the following. Have migrated a .NET API layer from .NET v5 to v7. Key package versions:

Creating new records and reading (encrypting / decrypting) on newer versions (.NET 7) are fine.

Trying to decrypt data columns (nvarchar(max)) from records in the database from the .NET v5 versions, give a "not a valid Base-64 string". I note that the encrypted string in the database from such earlier version (.NET 5) created record has a length of 155 which is suspect. However this was being decrypted ok with DataEncryption v2.0.0 (.NET v5)

Extract from exception:


System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
   at System.Convert.FromBase64CharPtr(Char* inputPtr, Int32 inputLength)
   at System.Convert.FromBase64String(String s)
   at Microsoft.EntityFrameworkCore.DataEncryption.Internal.EncryptionConverter`2.Decrypt[TInput,TOupout](TProvider input, IEncryptionProvider encryptionProvider, StorageFormat storageFormat)
   at lambda_method2044(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinat
or)

Also I noted that I had to move from using a decorator to using fluent, and pass in the StoreFormat.Base64 option. (e.g. builder.Property(x => x.DateOfBirth).IsRequired().IsEncrypted(StorageFormat.Base64);). If I change this setting to binary I get below:

System.InvalidCastException: Unable to cast object of type 'System.String' to type 'System.Byte[]'.
   at Microsoft.Data.SqlClient.SqlDataReader.GetFieldValueFromSqlBufferInternal[T](SqlBuffer data, _SqlMetaData metaData, Boolean isAsync)
   at Microsoft.Data.SqlClient.SqlDataReader.GetFieldValueInternal[T](Int32 i, Boolean isAsync)
   at Microsoft.Data.SqlClient.SqlDataReader.GetFieldValue[T](Int32 i)
   at lambda_method1954(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()

The newer custom enyption provider "Decrypt" method had to change to use byte arrays instead of strings too FYI.

        public byte[] Decrypt(byte[] input)
            var protector = _provider.CreateProtector(Purpose);
            var decryptedString = protector.Unprotect(input);
            return decryptedString;
        }

Any thoughts/direction?

Eastrall commented 1 year ago

Version 2.0.0 only encrypts string properties into Base64 strings. Version 3+ adds the supports for both string and byte arrays. Also, using data annotation should not break your code since we just added the fluent notation in version 4.X. Below the code of the encrypt method from version 2.0.0 : https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/blob/68f1a9dc04f3449f51aaac1196eb9f66ffb75745/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs#L58-L82

I notice that the IV is also encrypted along the data. (16 first bytes) This practice has been deprecated. You can refer to this issue : https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/issues/46#issuecomment-1401939203 to use a provider using dynamic IV.

gcatwork commented 1 year ago

thanks - from what I see the change below would suffice

EncryptionConverter.cs

    private static TModel Decrypt<TInput, TOupout>(TProvider input, IEncryptionProvider encryptionProvider, StorageFormat storageFormat)
    {
        Type destinationType = typeof(TModel);
        byte[] inputData;

        if (storageFormat is StorageFormat.Default or StorageFormat.Base64)
            // ORIG
            //inputData = Convert.FromBase64String(input.ToString());
            // ALTERNATIVE
        {
            var strToDescrypt = input.ToString();
            inputData = System.Text.Encoding.UTF8.GetBytes(strToDescrypt);
        }

        else
            inputData = input as byte[];

        byte[] decryptedRawBytes = encryptionProvider.Decrypt(inputData);
        object decryptedData = null;

        if (destinationType == typeof(string))
        {
            decryptedData = Encoding.UTF8.GetString(decryptedRawBytes).Trim('\0');
        }
        else if (destinationType == typeof(byte[]))
        {
            decryptedData = decryptedRawBytes;
        }

        return (TModel)Convert.ChangeType(decryptedData, typeof(TModel));
    }

Noting our decryption provider could then be adjusted to the following :


  public byte[] Decrypt(byte[] input)
        {
            var protector = _provider.CreateProtector(Purpose);
            var inputStr = System.Text.Encoding.UTF8.GetString(input);
            var decryptedString = protector.Unprotect(inputStr);
            var decrtypedByteArray = System.Text.Encoding.UTF8.GetBytes(decryptedString);
            return decrtypedByteArray;
        }