使用 EF Core 2.2 使用 SQL Server DECRYPTBYKEY 解密字符串
Using EF Core 2.2 to decrypt a string using SQL Server DECRYPTBYKEY
基本上我有一个带有加密字符串的 POCO 模型。使用 EF 核心 2.2.
我们使用 DECRYPTBYKEY 使用 SYMMETRIC KEY 解密字符串。
我正在使用 DBSet.FromSQL 传递调用打开对称密钥的 SQL 查询,获取包括解密值的数据,关闭对称密钥。
FromSQL 只允许您带回实体而不是字符串本身。
我尝试在模型上添加一个解密的字符串值,然后尝试在 FromSQL 查询中设置它。
当存储库 DBSet 中没有任何 .Include 时,这实际上填充正常。
当 DBSet 确实有 .Include(在外键上过滤 DBSet table)时会出现运行时错误,抱怨解密的字符串不是数据库中的列 table -当然不是。所以 .Include 是先在基础 table 上调用 SQL。
如果我将 [NotMapped] 属性放在解密的字符串列上,那么当 FromSQL 查询运行时它不会填充它。
那么如何在不使用 [NotMapped] 的情况下使用这个解密的字符串列,而是在 DBSet 上使用 .Include?
我添加了代码,这样您就可以看到更多问题。无法按照一个答案中的建议在模型上添加 Decrypt 的实现。解密方法需要 DbSet 调用 FromSQL。 DbSet 源自 ConcreteRepository。我也看不到调用临时 SQL 查询到 return 1 个字符串。
原始片段 SQL(SQL 服务器)
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; }
}
所以正如我在评论中暗示的那样,确实有可能侵入 EFCore 的管道并使其执行自定义 SQL 功能。 functional console app 可以做到这一点。
我会预先声明,我在数据库上进行了实验(请参阅我的存储库 link 中的 SQL 脚本),其中 DECRYPTBYPASSPHRASE
函数为我没有钥匙。我也只安装了 .net core 2.1。 None the less,我希望你能明白要点。话虽如此,我将强调几点,让您进一步探索解决方案:
我最终像这样定义了我的模型:
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; }
}
鉴于我应该能够 select 该值而无需进行第二次往返,我稍微修改了您的 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
、DecryptByPassphrase
和 EncryptByPassphrase
,例如:
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;
}
}
毕竟是非常琐碎的字符串构建练习。希望这能为您提供足够的燃料来启动您的解决方案并 运行ning。
UPD EF Core 3 似乎仍然支持 IMethodCallTranslator,因此上面的解决方案应该仍然适用。
UPD2:确实可以做到。看我的 updated repo on github.
基本上我有一个带有加密字符串的 POCO 模型。使用 EF 核心 2.2.
我们使用 DECRYPTBYKEY 使用 SYMMETRIC KEY 解密字符串。
我正在使用 DBSet.FromSQL 传递调用打开对称密钥的 SQL 查询,获取包括解密值的数据,关闭对称密钥。
FromSQL 只允许您带回实体而不是字符串本身。
我尝试在模型上添加一个解密的字符串值,然后尝试在 FromSQL 查询中设置它。
当存储库 DBSet 中没有任何 .Include 时,这实际上填充正常。
当 DBSet 确实有 .Include(在外键上过滤 DBSet table)时会出现运行时错误,抱怨解密的字符串不是数据库中的列 table -当然不是。所以 .Include 是先在基础 table 上调用 SQL。
如果我将 [NotMapped] 属性放在解密的字符串列上,那么当 FromSQL 查询运行时它不会填充它。
那么如何在不使用 [NotMapped] 的情况下使用这个解密的字符串列,而是在 DBSet 上使用 .Include?
我添加了代码,这样您就可以看到更多问题。无法按照一个答案中的建议在模型上添加 Decrypt 的实现。解密方法需要 DbSet 调用 FromSQL。 DbSet 源自 ConcreteRepository。我也看不到调用临时 SQL 查询到 return 1 个字符串。
原始片段 SQL(SQL 服务器)
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; }
}
所以正如我在评论中暗示的那样,确实有可能侵入 EFCore 的管道并使其执行自定义 SQL 功能。 functional console app 可以做到这一点。
我会预先声明,我在数据库上进行了实验(请参阅我的存储库 link 中的 SQL 脚本),其中 DECRYPTBYPASSPHRASE
函数为我没有钥匙。我也只安装了 .net core 2.1。 None the less,我希望你能明白要点。话虽如此,我将强调几点,让您进一步探索解决方案:
我最终像这样定义了我的模型:
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; }
}
鉴于我应该能够 select 该值而无需进行第二次往返,我稍微修改了您的 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
、DecryptByPassphrase
和 EncryptByPassphrase
,例如:
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;
}
}
毕竟是非常琐碎的字符串构建练习。希望这能为您提供足够的燃料来启动您的解决方案并 运行ning。
UPD EF Core 3 似乎仍然支持 IMethodCallTranslator,因此上面的解决方案应该仍然适用。 UPD2:确实可以做到。看我的 updated repo on github.