1

I would like to encrypt an existing database column with always encrypted. My project is a ASP.NET project using code first and database is SQL Server. The database has already data. I created a migration to achieve my goal.

First I tried to alter the column type, using the following.

ALTER TABLE [dbo].[TestDecrypted] ALTER COLUMN [FloatCol] [float] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK_Auto1], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL

I got the following error.

Operand type clash: float is incompatible with float encrypted with (encryption_type = 'RANDOMIZED', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = 'CEK_Auto1', column_encryption_key_database_name = 'TestEncrypt')

Then I decided to created another column and migrate the data.

ALTER TABLE [dbo].[TestDecrypted] ADD [FloatCol2] [float] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK_Auto1], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL
UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = [FloatCol]

And I got the same error.

After I looked at this, I noticed that it is possible to insert data like the following

DECLARE @floatCol FLOAT = 1.1
UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = @floatCol

But if I try to obtain the value from my existing column, it fails.

DECLARE @floatCol FLOAT = (SELECT TOP 1 FloatCol FROM TestDecrypted)
UPDATE [dbo].[TestDecrypted] SET FloatCol2 = @floatCol

The error follows.

Encryption scheme mismatch for columns/variables '@floatCol'. The encryption scheme for the columns/variables is (encryption_type = 'PLAINTEXT') and the expression near line '4' expects it to be (encryption_type = 'RANDOMIZED', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = 'CEK_Auto1', column_encryption_key_database_name = 'TestEncrypt'). 

Does anyone knows how can I achieve my goal?


Update 1

@Nikhil-Vithlani-Microsoft did some interesting suggestions.

  • Always Encrypted Wizard in SSMS - I would like to achieve my goal with code first migrations, so this idea does not fit.
  • SqlBulkCopy - It does not work inside migrations, because the new column will only exist after all 'Up' method is run. Therefore we cannot insert data into this column in this way inside this method.

Anyway, his suggestions drove me to another attempt: obtain the decrypted values and update the encrypted column with them.

var values = new Dictionary<Guid, double>();

var connectionString = ConfigurationManager.ConnectionStrings["MainDb"].ConnectionString;
using (var sourceConnection = new SqlConnection(connectionString))
{
    var myCommand = new SqlCommand("SELECT * FROM dbo.TestDecrypted", sourceConnection);
    sourceConnection.Open();

    using (var reader = myCommand.ExecuteReader())
    {
        while (reader.Read())
        {
            values.Add((Guid)reader["Id"], (double)reader["FloatCol"]);
        }
    }
}

Sql("ALTER TABLE [dbo].[TestDecrypted] ADD [FloatCol2] [float] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK_Auto1], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL");

foreach (var valuePair in values)
{
    // The error occurs here
    Sql($@"DECLARE @value FLOAT = {valuePair.Value}
UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = @value WHERE Id = '{valuePair.Key}'");
}

In fact, I did not try to create another column and to migrate the data, as mentioned in an example above. I tried it only on SSMS. And now I got a different error.

Transaction (Process ID 57) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

I tried to do it without encrypting the new column, and it worked properly.

Any idea why this error occurs?

4

1 回答 1

1

You will have to do the always encrypted related migration outside of entity framework. This blog should help https://blogs.msdn.microsoft.com/sqlsecurity/2015/08/27/using-always-encrypted-with-entity-framework-6/

If you want to encrypt an existing column, you can use Always Encrypted Wizard in SSMS, or use this article that explains how to migrate existing data.

Also, please note that doing bulk inserts through a C# (.NET 4.6.1+ client) app is supported.

You can do this in c# using SqlBulkCopy specifically using SqlBulkCopy.WriteToServer(IDataReader) Method.

  • Create a new table (encryptedTable) with the same schema as that of your plaintext table (unencryptedTable) but with the encryption turned on for the desired columns.
  • Do select * from unencryptedTable to load the data in a SqlDataReader then use SqlBulkCopy to load it to the encryptedTable using SqlBulkCopy.WriteToServer(IDataReader) Method

For example, Plaintext Table

CREATE TABLE [dbo].[Patients](
 [PatientId] [int] IDENTITY(1,1), 
 [SSN] [char](11) NOT NULL)

Encrypted Table

CREATE TABLE [dbo].[Patients](
 [PatientId] [int] IDENTITY(1,1), 
 [SSN] [char](11) COLLATE Latin1_General_BIN2 
     ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, 
     ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', 
     COLUMN_ENCRYPTION_KEY = CEK1) NOT NULL)

As for why your method does not work, when you use parameterization for always encrypted, the right hand side (RHS) of the declare statement needs to be a literal. Because the driver will identify the literal and encrypt it for you. So, the following will not work, since RHS is a sql expression and cannot be encrypted by the driver

DECLARE @floatCol FLOAT = (SELECT TOP 1 FloatCol FROM TestDecrypted)
UPDATE [dbo].[TestDecrypted] SET FloatCol2 = @floatCol

Update:
The following code will not work because parameterization for Always Encrypted only applies to SSMS

foreach (var valuePair in values)
{
    // The error occurs here
    Sql($@"DECLARE @value FLOAT = {valuePair.Value}
UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = @value WHERE Id = '{valuePair.Key}'");
}  

However, if you rewrite your code as follows, that should work

foreach (var valuePair in values)
{
    SqlCommand cmd = _sqlconn.CreateCommand();
    cmd.CommandText = @"UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = @FloatVar WHERE Id = '{valuePair.Key}'");";

    SqlParameter paramFloat = cmd.CreateParameter();
            paramFloat.ParameterName = @"@FloatVar";
            paramFloat.DbType = SqlDbType.Float;
            paramFloat.Direction = ParameterDirection.Input;
            paramFloat.Value = floatValue;
            cmd.Parameters.Add(paramFloat);

    cmd.ExecuteNonQuery();


}

Hope that helps, if you have additional question, please leave them in the comments.

于 2017-04-05T18:17:45.433 回答