使用Entity Framework加密列

11

有没有人找到了一种能够通过Entity Framework 4从数据库中提取加密值的好方法?

我有一个使用des_encrypt加密的MySql数据库,需要尽可能轻松地获取这些值,并且当然还要能够更新和插入它们。

我认为很奇怪EF中似乎没有内置支持此功能。即使是我们自己构建的ORM系统也支持此功能。我们只需为每个加密字段添加注释“encrypted”,ORM工具将在查询中添加des_decrypt(column)和des_encrypt(column)。

有人知道吗?

5个回答

15

这是@TheCloudlessSky提出的答案的一个实现示例。我认为它会帮助任何想知道如何实现它的人。

我正在使用一个现有的数据库,所以基本的模型类已经为我自动生成了。

自动生成的User.cs:

namespace MyApp.Model 
{
    public partial class User
    {
        public int UserId { get; set; }
        public byte[] SSN { get; set; }
        ...
    }
}

我创建了自己的User.cs。(注意它与自动生成的User.cs在相同的命名空间中,并且没有编译器错误,因为自动生成的User.cs被声明为partial class!此外,我的User.cs不能与自动生成的User.cs在同一文件夹中,因为文件名冲突!)

namespace MyApp.Model 
{
    public partial class User
    {
        public string DecryptedSSN { get; set; }
        ...
    }
}

现在,每当我从我的DbContext中检索User时,我将看到所有在自动生成的类中定义的属性,以及在我的增强类中定义的属性。

这里是我的UserRepository.cs的实现:

namespace MyApp.Model
{
    public interface IUserRepository 
    {
        User Get(int userId);
        ...
    }

    public class UserRepository : IUserRepository
    {
        public User GetById(int userId)
        {
            using (var dataContext = MyDbContext())
            {
                var user = dataContext.Users.Find(u => u.UserId == userId);
                var decryptedSSNResult = dataContext.Decrypt(u.SSN);
                user.DecryptedSSN = decryptedSSNResult.FirstOrDefault();
                return user;
            }
        }
    }
}

现在你可能会想知道我从哪里得到了MyDbContext.Decrypt()方法?

这不是为您自动生成的。但是,您可以将此存储过程导入到自动生成的Model.Context.cs文件中。(这个过程在官方EntityFramework文章中有很好的文档说明:如何导入存储过程(Entity Data Model Tools),网址为:http://msdn.microsoft.com/en-us/library/vstudio/bb896231(v=vs.100).aspx

如果您不知道最终结果应该是什么样子的,这是我的Model.Context.cs自动生成的内容:

namespace MyApp.Model
{
    // using statements found here

    public partial class MyDbContext : DbContext
    {
        public MyDbContext()
            : base("name = MyDbContext")
        { }

        public virtual ObjectResult<string> Decrypt(byte[] encryptedData)
        {
            var encryptedDataParameter = encryptedData != null ? 
                            new ObjectParameter("encryptedData", encryptedData) :
                            new ObjectParameter("encryptedData", typeof(byte[]));

            return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction<string>("Decrypt", encryptedDataParameter);
        }

        // similar function for Encrypt 
    }
}

这是我的Decrypt存储过程的样子:

CREATE PROCEDURE decrypt
    @encryptedData VARBINARY(8000)
AS
BEGIN
    OPEN SYMMETRIC KEY xxx_Key DECRYPTION BY CERTIFICATE xxx_Cert;

    SELECT CAST(DECRYPTIONBYKEY(@encryptedData) AS NVARCHAR(MAX)) AS data;

    CLOSE ALL SYMMETRIC KEYS;
END;
GO

性能考虑

现在我已经展示了@TheCloudlessSky提供的答案实现,我想快速强调一些与性能相关的要点。

1) 每次检索用户对象时,将进行两次数据库访问而不是一次。第一次访问是为了检索对象; 第二次访问是为了解密SSN。如果您不小心,这可能会导致性能问题。

建议: 不要自动解密加密字段! 在上面展示的示例中,我在检索用户对象时解密了SSN。我那样做只是为了演示目的!问问自己是否真的需要每次检索用户时都获取SSN。如果可能,选择懒惰解密而不是急切解密!

2) 尽管我没有展示这个,但每次创建/更新用户对象时,也将进行两次数据库访问。第一次访问是为了加密SSN; 第二次访问是为了插入对象。同样,如果您不小心,这也可能会导致性能问题。

建议: 确保意识到此性能问题,但不要将加密和保存SSN委托为不同的方法。将其全部包含在一个操作中,否则您可能会忘记完全保存它。因此,创建/更新的建议与检索相反:选择急切加密而不是懒惰加密!


2
我给你的努力投一票。但是我有点不同意你的方法。我不想让我的数据库解密值。我可以将所有这些信息存储在我的应用程序中,而不必进行两次数据库访问。 - mac10688
@mac10688 我同意你关于多次访问数据库会带来性能损失的观点。一般来说,为CUD操作编写专用的存储过程,并通过实体框架将它们链接到模型中比拥有像加密/解密这样的存储过程更有效。但我想演示一种使用由数据库管理而不是应用程序管理的密钥实现加密/解密的方法。 - Parth Shah
我提供一个链接,介绍如何通过实体框架将CUD存储过程连接到CUD操作,以防有人想了解如何实现此功能:https://msdn.microsoft.com/en-us/data/jj593489。我知道链接通常不被欢迎,但由于这个链接是到MSDN Entity Framework文档站点的,我希望人们不会介意它。 - Parth Shah
非常好的信息,详尽且有用,包括了答案和评论中的链接!非常感谢! - Dov Miller
我喜欢这个解决方案,因为密钥实际上存储在数据库中。然而,这样做有什么好处吗?我的意思是,将密钥存储在数据库中真的比将其存储在应用程序中更好吗?我只能看到一个缺点(例如,需要往返数据库)。如果有好处,我会改变我的应用程序使用这种方法,因为我真的很喜欢密钥在数据库中的想法。 - Sam

2

如果您在谷歌上搜索这个问题,并且想要一种简单的方法来解密单个列/行(而不是整个表/类),使用对称加密和EF,您可以通过以下两种(简单)方式之一实现。

第一种方法:创建一个存储过程来执行解密操作:

CREATE PROCEDURE [dbo].[Decrypt_Credential]
@User_Name varchar(50) = NULL
AS
BEGIN
OPEN SYMMETRIC KEY My_Key_01 DECRYPTION BY CERTIFICATE MyCertName;

SELECT CONVERT(varchar, DecryptByKey(Encrypted_Password)) FROM Application_Credentials WHERE User_Name = @User_Name;

CLOSE SYMMETRIC KEY My_Key_01; 
END;

...然后直接在代码中调用存储过程,将结果作为字符串检索:

using (var context = new YourDatabaseContext())
        {
            var result = context.Database.SqlQuery<string>("Decrypt_Credential @user", new SqlParameter("user", TheUserName)).FirstOrDefault();
        }

你可以通过数据库事务来完成同样的操作,这是第二种方法。请注意,我完全意识到此示例不符合SQL注入保护规范,因为我在使用参数化查询时遇到了一些问题,所以才使用了这个示例。如果你选择这种方法,你需要使用参数化查询。
 using (var context = new YourDatabaseContext())
        {       
            using (var dbContextTransaction = context.Database.BeginTransaction())
            {
                try
                {
                    var sql = String.Format("OPEN SYMMETRIC KEY {0} DECRYPTION BY CERTIFICATE {1}", KeyName, CertName);
                    context.Database.ExecuteSqlCommand(sql);

                    sql = String.Format("SELECT CONVERT(varchar, DecryptByKey(Encrypted_Password)) FROM Application_Credentials WHERE User_Name = '{0}'", UserNameToDecryptCredsFor);
                    var result = context.Database.SqlQuery<string>(sql).FirstOrDefault();

                    sql = String.Format("CLOSE SYMMETRIC KEY {0}", KeyName);
                    context.Database.ExecuteSqlCommand(sql);
                }
                catch (Exception exp)
                {
                    var x = exp.ToString(); //do something with exception
                }
            }
        }

你写道这仅适用于单个列/行,但不适用于整个表/类。如果我需要加密/解密表中的所有或大多数列,我该怎么做? - Dov Miller
数据可以在传输到存储过程的中途被嗅探,这不是一种安全的方式。 - Leandro Bardelli

2

在将数据存入数据库之前,建议先进行加密并将其存储为二进制数据。这样,您就可以轻松地使用 EF 获取 byte[]

编辑:如果您使用存储过程来执行所有的 des_encryptdes_decrypt 以及 selects/inserts/deletes 操作,那么 EF 仍然可以为您完成映射工作吗?


似乎是一个不错的解决方案,如果我创建了一个新的数据库,我可能会选择这个方案。问题是,这个数据库非常大,并且被许多项目使用,所以要检查代码并进行更改将是一项巨大的工作。 - Andreas
谢谢你的建议。 这看起来是一个非常好的主意,我试了一下。不幸的是,如果我想对表中的所有解密数据进行LINQ查询,必须先执行存储过程。由于有250,000多行,每行有5列需要解密,所以这个过程会花费很长时间。 因此,使用context.AllMembers().Where(x=>x.MemberId == 1)会花费太长时间。 当然,我可以创建一个带有memberid参数的存储过程,但如果我想用LINQ搜索例如firstname呢? 也许我在这里漏掉了一些重要的东西... - Andreas
@Andreas - 从我的理解来看... MemberId 是加密的?你只需要解密那些被加密的列(比如密码...尽管你应该使用单向哈希)就可以了,如果 MemberId 没有被加密,你不需要解密它来进行“SELECT”。还有其他人在使用这个表吗?为什么不将其全部解密并创建一个新的非加密数据表呢? - TheCloudlessSky
不,除了memberid外,一切都加密了(它是主键)。我们解密像社会安全号码、姓名和地址这样的数据,因为对我们来说这是敏感数据。密码已经被散列,所以那方面没有问题。 这个表经常被使用,但我们真的不希望有另一个解密版本的表,因为那会与加密的想法背道而驰。抱歉我回复晚了,我在度假... :) - Andreas

2

您可以使用AES加密(双向加密)。当您需要查询数据库时,您可以发送代表目标值的加密字符串。

您可以创建一个扩展程序来解密实体。

MyTableEntitiesSet.Where(c=>c.MyField == MySeekValue.Encrypt()).First().Decrypt();

这可以执行数据库查询。

请注意数据大小,加密数据会更大...


4
老问题和答案,但请注意,如果你尝试这样做,只有在使用固定的初始向量时才会起作用,但这并不被推荐,因为这可能会让攻击者获知数据。应该使用每次加密都是随机的 IV,这意味着你每次加密都会得到一个不同的值。 - Joe Enos

2
您可以选择DIY/自己编写加密安全措施,但每个安全专家都会告诉您永远不要这样做。数据安全和加密中最困难的部分实际上不是"AES"或某种算法,而是密钥管理。迟早有一天,您将面对这个难题,而且它非常困难。
幸运的是,有一个名为Crypteron CipherDb的工具可以解决这个问题。实际上,它不仅提供了实体框架加密,还提供了自动篡改保护、安全密钥存储、安全密钥分发、密钥翻转、密钥缓存、访问控制列表等功能。有一个免费社区版,只需要几分钟即可添加到您的应用程序中。
在与实体框架集成时,您只需使用[Secure]注释数据模型或将属性命名为Secure_SocialSecurityNumberSecure_是关键部分),CipherDb会处理剩下的部分。
例如,您的数据模型可能是:
public class Patient
{
    public int Id {get; set;}

    [Secure]
    public string FullName {get; set;}

    [Secure]
    public string SocialSecurityNumber {get; set;}
}

"你的 web.config 文件会是这样的:"
<configuration>
  <configSections>
    <section 
        name="crypteronConfig" 
        type="Crypteron.CrypteronConfig, CipherCore, Version=2017, Culture=neutral, PublicKeyToken=e0287981ec67bb47" 
        requirePermission="false" />
  </configSections>

  <crypteronConfig>
    <myCrypteronAccount appSecret="Get_this_from_http://my.crypteron.com" />
  </crypteronConfig>
</configuration>

建议您保护您的web.config文件,或通过编程方式插入Crypteron API密钥(AppSecret)(文档)。
您可以在GitHub上找到示例应用程序,网址为https://github.com/crypteron/crypteron-sample-apps
顺便说一下,免费版也享受商业服务的好处,因此除了以上功能外,您还可以从一个地方保护流、文件、对象、消息队列、NoSQL数据库等。 免责声明:我在那里工作,我们确实有一个任何人都可以使用的免费社区版(我们不赚任何钱)。如果您认为它很酷,请告诉我们,告诉您的朋友。如果您有预算,请购买商业许可证。这有助于我们向每个人提供免费版 :)

但只免费三个月。 - Leandro Bardelli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接