首页 > 解决方案 > 使用 EF Core 2.2 使用 SQL Server DECRYPTBYKEY 解密字符串

问题描述

基本上我有一个带有加密字符串的 POCO 模型。使用 EF 核心 2.2。

我们使用 DECRYPTBYKEY 使用 SYMMETRIC KEY 解密字符串。

我正在使用 DBSet.FromSQL 传入调用打开对称密钥的 SQL 查询,获取包括解密值的数据,关闭对称密钥。

FromSQL 只允许您返回一个实体而不是一个字符串本身。

我尝试在模型上添加一个解密的字符串值,然后尝试在 FromSQL 查询中设置它。

当存储库 DBSet 中没有任何 .Include 时,这实际上可以填充。

当 DBSet 确实有 .Include (在外键表上过滤 DBSet)时,会出现运行时错误,抱怨解密的字符串不是数据库表上的列 - 当然不是。所以拥有 .Include 是首先在基表上调用 SQL。

如果我将 [NotMapped] 属性放在解密的字符串列上,那么当 FromSQL 查询运行时它不会填充它。

那么如何在不使用 [NotMapped] 而是在 DBSet 上使用 .Include 的情况下使用这个解密的字符串列?

我添加了代码,以便您可以看到更多问题。无法按照一个答案中的建议在模型上添加 Decrypt 的实现。Decrypt 方法需要 DbSet 调用 FromSQL。DbSet 起源于 ConcreteRepository。我也看不到调用临时 SQL 查询来返回 1 个字符串。

从原始 SQL (SQL Server) 截取

    OPEN SYMMETRIC KEY {1} DECRYPTION BY PASSWORD = '{2}';

    SELECT  * , --other fields
            CONVERT(VARCHAR(60), DECRYPTBYKEY(A.Encrypted)) AS Decrypted
    FROM    dbo.Model A
    JOIN table2 t2 ON ...
    JOIN table3 t3 ON ...

   WHERE A.Id= 123

   CLOSE SYMMETRIC KEY {1};",

具体存储库

public async Task<IEnumerable<Model>> GetAllById(int id)
{

            var filteredSet = Set.Where(x => x.Id == id)
               .Include(x => x.Table2)
               .Where(x => x.Table2.IsSomething).ToList();

            var models = filteredSet.Select(f =>
                GetDecryptValue($"Id = {f.Id}");

            return models;

}


基础存储库

protected DbSet<TEntity> Set => _dbContext.Set<TEntity>();

public virtual TEntity GetDecryptValue(string filterCriteria)
        {
            string buildSelectStmt = $"SELECT TOP 1 Encrypted FROM Model";
            string buildSelectStmt2 = $"SELECT *, CONVERT(VARCHAR(MAX), DECRYPTBYKEY(@Value)) AS Decrypted FROM Model";

            buildSelectStmt = $"{buildSelectStmt} WHERE {filterCriteria}";
            buildSelectStmt2 = $"{buildSelectStmt2} WHERE {filterCriteria}";

            string sql = string.Format(@"
                DECLARE @Value NVARCHAR(MAX)
                SET @Value = ({0});
                OPEN SYMMETRIC KEY {1} DECRYPTION BY PASSWORD = '{2}';
                {3};
                CLOSE SYMMETRIC KEY {1};",
                buildSelectStmt, SymmetricKeyName, SymmetricKeyPassword, buildSelectStmt2);

            var result = Set.FromSql(sql);

            return result.FirstOrDefault();
        }

模型

    public partial class Model
    {
        public int Id { get; set; }
        public string Encrypted { get; set; }
        [NotMapped]
        public string Decrypted { get; set; }
    }

标签: c#sql-serverentity-framework-coreasp.net-core-2.2

解决方案


因此,正如我在评论中暗示的那样,确实有可能侵入 EFCore 的管道并使其执行自定义 SQL 函数。这是一个功能性的控制台应用程序

我会预先声明,我在一个数据库上进行了实验(请参阅我的 repo 链接中的 SQL 脚本),DECRYPTBYPASSPHRASE因为我没有密钥。我也只安装了.net core 2.1。尽管如此,我还是希望你能明白要点。话虽如此,我将强调几点,让您进一步探索解决方案:

我最终像这样定义了我的模型:

public partial class Model
{
    public int Id { get; set; }
    public byte[] Encrypted { get; set; } // apparently encrypted data is stored in `VARBINARY`, which translates to `byte[]`, so I had to tweak it here
    [NotMapped] // this is still required as EF will not know where to get the data unless we tell it (see down below)
    public string Decrypted { get; set; } // the whole goal of this exercise here
    public Table2 Table2 { get; set; }
}

鉴于我应该能够只选择值而不必进行第二次往返,我稍微修改了您的Concrete Repository代码:

public IEnumerable<Model> GetAllById(int id)
{
    // you will need to uncomment the following line to work with your key
    //_dbContext.Database.ExecuteSqlCommand("OPEN SYMMETRIC KEY {1} DECRYPTION BY PASSWORD = '{2}';", SymmetricKeyName, SymmetricKeyPassword);
    var filteredSet = Set.Include(x => x.Table2)
        .Where(x => x.Id == id)
        .Where(x => x.Table2.IsSomething)
        .Select(m => new Model
    {
        Id = m.Id,
        //Decrypted = EF.Functions.DecryptByKey(m.Encrypted), // since the key's opened for session scope - just relying on it should do the trick
        Decrypted = EF.Functions.Decrypt("test", m.Encrypted),
        Table2 = m.Table2,
        Encrypted = m.Encrypted
    }).ToList();
    // you will need to uncomment the following line to work with your key
    //_dbContext.Database.ExecuteSqlCommand("CLOSE SYMMETRIC KEY {1};", SymmetricKeyName);
    return filteredSet;
}

现在,定义EF.Functions.Decrypt是这里的关键。我们基本上必须做两次:1)作为扩展方法,以便我们可以在 LINQ 中使用 then 和 2)作为 EF 表达式树节点。然后 EF 会做什么,对于它发现的每个方法调用,它会检查内部列表,IMethodCallTranslator如果发现匹配 - 它将函数推迟到 SQL。否则它将必须在 C# 中运行。因此,您将看到的所有管道基本上都需要注入TranslateImpl该列表。

IMethodCallTranslator 本身

public class TranslateImpl : IMethodCallTranslator
{

    private static readonly MethodInfo _encryptMethod
        = typeof(DbFunctionsExtensions).GetMethod(
            nameof(DbFunctionsExtensions.Encrypt),
            new[] { typeof(DbFunctions), typeof(string), typeof(string) });
    private static readonly MethodInfo _decryptMethod
        = typeof(DbFunctionsExtensions).GetMethod(
            nameof(DbFunctionsExtensions.Decrypt),
            new[] { typeof(DbFunctions), typeof(string), typeof(byte[]) });

    private static readonly MethodInfo _decryptByKeyMethod
        = typeof(DbFunctionsExtensions).GetMethod(
            nameof(DbFunctionsExtensions.DecryptByKey),
            new[] { typeof(DbFunctions), typeof(byte[]) });

    public Expression Translate(MethodCallExpression methodCallExpression)
    {
        if (methodCallExpression.Method == _encryptMethod)
        {
            var password = methodCallExpression.Arguments[1];
            var value = methodCallExpression.Arguments[2];
            return new EncryptExpression(password, value);
        }
        if (methodCallExpression.Method == _decryptMethod)
        {
            var password = methodCallExpression.Arguments[1];
            var value = methodCallExpression.Arguments[2];
            return new DecryptExpression(password, value);
        }

        if (methodCallExpression.Method == _decryptByKeyMethod)
        {
            var value = methodCallExpression.Arguments[1];
            return new DecryptByKeyExpression(value);
        }

        return null;
    }
}

我最终实现了三个表达式存根:和DecryptByKey,例如:DecryptByPassphraseEncryptByPassphrase

public class DecryptByKeyExpression : Expression
{
    private readonly Expression _value;

    public override ExpressionType NodeType => ExpressionType.Extension;
    public override Type Type => typeof(string);
    public override bool CanReduce => false;

    protected override Expression VisitChildren(ExpressionVisitor visitor)
    {
        var visitedValue = visitor.Visit(_value);

        if (ReferenceEquals(_value, visitedValue))
        {
            return this;
        }

        return new DecryptByKeyExpression(visitedValue);
    }

    protected override Expression Accept(ExpressionVisitor visitor)
    {
        if (!(visitor is IQuerySqlGenerator))
        {
            return base.Accept(visitor);
        }
        visitor.Visit(new SqlFragmentExpression("CONVERT(VARCHAR(MAX), DECRYPTBYKEY("));
        visitor.Visit(_value);
        visitor.Visit(new SqlFragmentExpression("))"));
        return this;
    }

    public DecryptByKeyExpression(Expression value)
    {
        _value = value;
    }
}

毕竟是非常微不足道的弦乐构建练习。希望这可以为您提供足够的燃料来启动并运行您的解决方案。

UPD EF Core 3 似乎仍支持IMethodCallTranslator,因此上述解决方案仍应适用。 UPD2:确实可以。在 github 上查看我更新的 repo 。


推荐阅读