【问题标题】:Using EF Core 2.2 to decrypt a string using SQL Server DECRYPTBYKEY使用 EF Core 2.2 使用 SQL Server DECRYPTBYKEY 解密字符串
【发布时间】:2020-03-30 04:34:33
【问题描述】:

基本上我有一个带有加密字符串的 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-server entity-framework-core asp.net-core-2.2


【解决方案1】:

正如我在评论中暗示的那样,确实可以侵入 EFCore 的管道并使其执行自定义 SQL 函数。这是一个 functional console app 可以做到的。

我会提前声明,因为我没有密钥,所以我用DECRYPTBYPASSPHRASE 函数对数据库进行了试验(请参阅我的回购链接中的 SQL 脚本)。我也只安装了 .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;
    }
}

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

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:确实可以。见我的updated repo on github

【讨论】:

  • ,解密/加密方法在数据库中,依赖于 ef dbset.fromsql 来执行。我看不出如何在模型中实现。要遵循的代码..
  • 我现在已经添加了代码,以便您可以看到更多问题。无法在模型上添加 Decrypt 的实现。 Decrypt 需要 DbSet 调用 FomSQL。 DbSet 仅来自 ConcreteRepository。我也看不到调用即席 SQL 查询来返回 1 个字符串。
  • 是的,我现在看到了问题。这篇博文:weblogs.thinktecture.com/pawel/2019/07/…(不是我的)让我相信确实可以定义一个解密/加密函数调用,EF 将透明地转换为 SQL。我建议您在这里查看 EF 核心源代码:github.com/aspnet/EntityFrameworkCore/blob/release/2.0/src/… 我知道这可能不是您正在寻找的答案,但如果我要探索 t
  • 哇。那里的细节丢失。多谢了。我会在星期一试一试
  • 感谢您的帮助。您知道 EFCore 3 解决方案吗?
猜你喜欢
  • 2023-03-31
  • 1970-01-01
  • 2020-06-10
  • 2017-05-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-20
相关资源
最近更新 更多