SQL CLR 标量 UDF 中的 DES 解密不工作
DES Decryption in SQL CLR Scalar UDF not working
我们有一个 legacy 应用程序,它使用 SQL 服务器作为其后端。作为一些安全问题的一部分,它使用(单个)DES 和硬编码到应用程序代码中的密钥和 IV 对从用户收集的一些字段进行加密,然后 Base64 对加密的字节进行编码,最后将该字符串存储在 varchar 中数据库中的列。在这一点上(可能是在第一次编码时)非常不安全,还有一个有问题的设计/实现,但它就是这样。我的任务是在 SQL 服务器中实现一个 CLR 用户定义的标量函数,可以解密此类数据。
作为概念证明,我创建了以下简短的控制台应用程序以确保我了解 C# 中的 DES 解密过程:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
class My_Decrypt
{
static void Main(string[] args)
{
DES des = new DESCryptoServiceProvider();
byte[] IV = BitConverter.GetBytes(0xFECAEFBEEDFECEFA);
byte[] Key = Encoding.ASCII.GetBytes("password");
foreach (string cipherText in args)
{
byte[] cipherBytes = Convert.FromBase64String(cipherText);
MemoryStream es = new MemoryStream(cipherBytes);
CryptoStream cs = new CryptoStream(
es,
des.CreateDecryptor(Key, IV),
CryptoStreamMode.Read
);
byte[] plainBytes = new byte[cipherBytes.Length];
cs.Read(plainBytes, 0, plainBytes.Length);
string plainText = Encoding.ASCII.GetString(plainBytes);
Console.WriteLine(
"'{0}' == '{1}'\nusing key = '{2}', IV = '{3}'\ndecrypts to '{4}' == '{5}'.\n",
cipherText,
BitConverter.ToString(cipherBytes),
BitConverter.ToString(Key),
BitConverter.ToString(IV),
BitConverter.ToString(plainBytes),
plainText
);
}
}
}
编译后,我可以运行以下内容:
C:\>My_Decrypt.exe KDdSnfYYnMQawhwuaWo2WA==
'KDdSnfYYnMQawhwuaWo2WA==' == '28-37-52-9D-F6-18-9C-C4-1A-C2-1C-2E-69-6A-36-58'
using key = '70-61-73-73-77-6F-72-64', IV = 'FA-CE-FE-ED-BE-EF-CA-FE'
decrypts to '73-65-63-72-65-74-20-64-61-74-61-00-00-00-00-00' == 'secret data '.
这看起来是正确的,我使用 openssl 进行了验证。
因此,确定了这一点后,我接下来尝试在 CLR 标量 UDF 中使用相同的代码,如下所示:
[Microsoft.SqlServer.Server.SqlFunction(IsDeterministic = true)]
public static SqlString DES_Decrypt( SqlString CipherText, SqlBinary DES_Key, SqlBinary DES_IV )
{
if (CipherText.IsNull || DES_Key.IsNull || DES_IV.IsNull)
return SqlString.Null;
string cipherText = CipherText.ToString();
byte[] cipherBytes = Convert.FromBase64String(cipherText);
MemoryStream es = new MemoryStream(cipherBytes);
DES des = new DESCryptoServiceProvider();
byte[] IV = (byte[]) DES_IV;
byte[] Key = (byte[]) DES_Key;
CryptoStream cs = new CryptoStream(
es, des.CreateEncryptor(Key, IV), CryptoStreamMode.Read );
byte[] plainBytes = new byte[cipherBytes.Length];
cs.Read(plainBytes, 0, plainBytes.Length);
cs.Close();
string plainText = new ASCIIEncoding().GetString(plainBytes);
return new SqlString(plainText);
}
然而,在编译之后,将程序集加载到 MSSQL,创建函数并尝试执行它——我得到了垃圾输出。因此,在多次尝试完成这项工作(包括创建上面的 POC 应用程序)之后,我将最后一行中的 return
替换为以下内容:
throw new ArgumentException(String.Format(
"\n'{0}' == '{1}'\nusing key = '{2}', IV = '{3}'\ndecrypts to '{4}' == '{5}'.",
cipherText,
BitConverter.ToString(cipherBytes),
BitConverter.ToString(Key),
BitConverter.ToString(IV),
BitConverter.ToString(plainBytes),
plainText
));
现在,当我在 MS运行 中 SQL 查询 SELECT dbo.DES_Decrypt(N'KDdSnfYYnMQawhwuaWo2WA==', CAST('password' AS binary(8)), 0xFACEFEEDBEEFCAFE);
时,我收到异常错误消息:
A .NET Framework error occurred during execution of user-defined routine or aggregate "DES_Decrypt":
System.ArgumentException:
'KDdSnfYYnMQawhwuaWo2WA==' == '28-37-52-9D-F6-18-9C-C4-1A-C2-1C-2E-69-6A-36-58'
using key = '70-61-73-73-77-6F-72-64', IV = 'FA-CE-FE-ED-BE-EF-CA-FE'
decrypts to '47-F7-06-E4-88-C4-50-5B-E5-4D-CC-C9-32-C7-8F-BB' == 'G????P[?M??2???'.
System.ArgumentException:
at DES_Decryptor.Decrypt(SqlString CipherText, SqlBinary DES_Key, SqlBinary DES_IV)
.
输入处理看起来不错:base64 解码字节匹配,传入的 Key 和 IV 的二进制版本也是如此。所以,在我看来,C# DES 解密例程出现问题时它是从 CLR 标量 UDF 调用的,但我很沮丧,完全没有想法。关于这里可能出了什么问题的任何线索?
在两个地方重现您的结果并更改一些内容但未更改输出后,我检查了两组代码以确保它们相同并发现了问题:
在对 new CryptoStream()
的调用中,您在控制台应用程序中使用 des.CreateDecryptor(Key, IV)
(正确),但在 SQLCLR 函数中使用 des.CreateEncryptor(Key, IV)
(与不正确)。将 SQLCLR 函数更改为使用 des.CreateDecryptor(Key, IV)
会产生预期的输出。
关于代码的一些一般说明:
- 您应该使用
Sql*
类型(即输入参数)的 Value
属性 而不是调用 ToString()
或转换。例如:
string cipherText = CipherText.Value;
byte[] IV = DES_IV.Value;
byte[] Key = DES_Key.Value;
- 您应该将
MemoryStream
、DESCryptoServiceProvider
、CryptoStream
的实例化包装在 using()
块中,以便正确清理外部资源。所有这 3 个都实现了 IDisposable 接口。
- 由于为 3 个输入参数中的任何一个传入的
NULL
都会 return 一个 NULL
,您可以通过在创建时设置选项来绕过代码中处理该问题的需要T-SQL 包装函数。当然,这不能通过 SSDT / Visual Studio 发布操作自动执行,但您可以手动处理部署,在这种情况下您自己发布 CREATE FUNCTION
,或者您可以添加一个 post -发布部署脚本以执行 ALTER FUNCTION
。因此,从代码中删除它:
if (CipherText.IsNull || DES_Key.IsNull || DES_IV.IsNull)
return SqlString.Null;
并将以下内容添加到 post 发布部署 SQL 脚本中(SSDT / Visual Studio 至少会处理):
ALTER FUNCTION [dbo].[DES_Decrypt](
@CipherText [nvarchar](max),
@DES_Key [varbinary](8000),
@DES_IV [varbinary](8000)
)
RETURNS [nvarchar](max)
WITH EXECUTE AS CALLER, RETURNS NULL ON NULL INPUT
AS EXTERNAL NAME [YourAssembly].[YourClass].[DES_Decrypt];
WITH
子句中的 RETURNS NULL ON NULL INPUT
选项起到了神奇的作用 ;-)。 NULL
越多,效率就越高,因为 SQL 服务器不需要调用代码,因为它已经知道答案。请记住,此选项 returns a NULL
if any input is NULL
,因此如果任何输入参数都需要传入 NULL
,则此选项无效。
有关使用 SQLCLR 的更多信息,请访问我的网站:SQLCLR Info
我们有一个 legacy 应用程序,它使用 SQL 服务器作为其后端。作为一些安全问题的一部分,它使用(单个)DES 和硬编码到应用程序代码中的密钥和 IV 对从用户收集的一些字段进行加密,然后 Base64 对加密的字节进行编码,最后将该字符串存储在 varchar 中数据库中的列。在这一点上(可能是在第一次编码时)非常不安全,还有一个有问题的设计/实现,但它就是这样。我的任务是在 SQL 服务器中实现一个 CLR 用户定义的标量函数,可以解密此类数据。
作为概念证明,我创建了以下简短的控制台应用程序以确保我了解 C# 中的 DES 解密过程:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
class My_Decrypt
{
static void Main(string[] args)
{
DES des = new DESCryptoServiceProvider();
byte[] IV = BitConverter.GetBytes(0xFECAEFBEEDFECEFA);
byte[] Key = Encoding.ASCII.GetBytes("password");
foreach (string cipherText in args)
{
byte[] cipherBytes = Convert.FromBase64String(cipherText);
MemoryStream es = new MemoryStream(cipherBytes);
CryptoStream cs = new CryptoStream(
es,
des.CreateDecryptor(Key, IV),
CryptoStreamMode.Read
);
byte[] plainBytes = new byte[cipherBytes.Length];
cs.Read(plainBytes, 0, plainBytes.Length);
string plainText = Encoding.ASCII.GetString(plainBytes);
Console.WriteLine(
"'{0}' == '{1}'\nusing key = '{2}', IV = '{3}'\ndecrypts to '{4}' == '{5}'.\n",
cipherText,
BitConverter.ToString(cipherBytes),
BitConverter.ToString(Key),
BitConverter.ToString(IV),
BitConverter.ToString(plainBytes),
plainText
);
}
}
}
编译后,我可以运行以下内容:
C:\>My_Decrypt.exe KDdSnfYYnMQawhwuaWo2WA==
'KDdSnfYYnMQawhwuaWo2WA==' == '28-37-52-9D-F6-18-9C-C4-1A-C2-1C-2E-69-6A-36-58'
using key = '70-61-73-73-77-6F-72-64', IV = 'FA-CE-FE-ED-BE-EF-CA-FE'
decrypts to '73-65-63-72-65-74-20-64-61-74-61-00-00-00-00-00' == 'secret data '.
这看起来是正确的,我使用 openssl 进行了验证。
因此,确定了这一点后,我接下来尝试在 CLR 标量 UDF 中使用相同的代码,如下所示:
[Microsoft.SqlServer.Server.SqlFunction(IsDeterministic = true)]
public static SqlString DES_Decrypt( SqlString CipherText, SqlBinary DES_Key, SqlBinary DES_IV )
{
if (CipherText.IsNull || DES_Key.IsNull || DES_IV.IsNull)
return SqlString.Null;
string cipherText = CipherText.ToString();
byte[] cipherBytes = Convert.FromBase64String(cipherText);
MemoryStream es = new MemoryStream(cipherBytes);
DES des = new DESCryptoServiceProvider();
byte[] IV = (byte[]) DES_IV;
byte[] Key = (byte[]) DES_Key;
CryptoStream cs = new CryptoStream(
es, des.CreateEncryptor(Key, IV), CryptoStreamMode.Read );
byte[] plainBytes = new byte[cipherBytes.Length];
cs.Read(plainBytes, 0, plainBytes.Length);
cs.Close();
string plainText = new ASCIIEncoding().GetString(plainBytes);
return new SqlString(plainText);
}
然而,在编译之后,将程序集加载到 MSSQL,创建函数并尝试执行它——我得到了垃圾输出。因此,在多次尝试完成这项工作(包括创建上面的 POC 应用程序)之后,我将最后一行中的 return
替换为以下内容:
throw new ArgumentException(String.Format(
"\n'{0}' == '{1}'\nusing key = '{2}', IV = '{3}'\ndecrypts to '{4}' == '{5}'.",
cipherText,
BitConverter.ToString(cipherBytes),
BitConverter.ToString(Key),
BitConverter.ToString(IV),
BitConverter.ToString(plainBytes),
plainText
));
现在,当我在 MS运行 中 SQL 查询 SELECT dbo.DES_Decrypt(N'KDdSnfYYnMQawhwuaWo2WA==', CAST('password' AS binary(8)), 0xFACEFEEDBEEFCAFE);
时,我收到异常错误消息:
A .NET Framework error occurred during execution of user-defined routine or aggregate "DES_Decrypt":
System.ArgumentException:
'KDdSnfYYnMQawhwuaWo2WA==' == '28-37-52-9D-F6-18-9C-C4-1A-C2-1C-2E-69-6A-36-58'
using key = '70-61-73-73-77-6F-72-64', IV = 'FA-CE-FE-ED-BE-EF-CA-FE'
decrypts to '47-F7-06-E4-88-C4-50-5B-E5-4D-CC-C9-32-C7-8F-BB' == 'G????P[?M??2???'.
System.ArgumentException:
at DES_Decryptor.Decrypt(SqlString CipherText, SqlBinary DES_Key, SqlBinary DES_IV)
.
输入处理看起来不错:base64 解码字节匹配,传入的 Key 和 IV 的二进制版本也是如此。所以,在我看来,C# DES 解密例程出现问题时它是从 CLR 标量 UDF 调用的,但我很沮丧,完全没有想法。关于这里可能出了什么问题的任何线索?
在两个地方重现您的结果并更改一些内容但未更改输出后,我检查了两组代码以确保它们相同并发现了问题:
在对 new CryptoStream()
的调用中,您在控制台应用程序中使用 des.CreateDecryptor(Key, IV)
(正确),但在 SQLCLR 函数中使用 des.CreateEncryptor(Key, IV)
(与不正确)。将 SQLCLR 函数更改为使用 des.CreateDecryptor(Key, IV)
会产生预期的输出。
关于代码的一些一般说明:
- 您应该使用
Sql*
类型(即输入参数)的Value
属性 而不是调用ToString()
或转换。例如:string cipherText = CipherText.Value; byte[] IV = DES_IV.Value; byte[] Key = DES_Key.Value;
- 您应该将
MemoryStream
、DESCryptoServiceProvider
、CryptoStream
的实例化包装在using()
块中,以便正确清理外部资源。所有这 3 个都实现了 IDisposable 接口。 - 由于为 3 个输入参数中的任何一个传入的
NULL
都会 return 一个NULL
,您可以通过在创建时设置选项来绕过代码中处理该问题的需要T-SQL 包装函数。当然,这不能通过 SSDT / Visual Studio 发布操作自动执行,但您可以手动处理部署,在这种情况下您自己发布CREATE FUNCTION
,或者您可以添加一个 post -发布部署脚本以执行ALTER FUNCTION
。因此,从代码中删除它:
并将以下内容添加到 post 发布部署 SQL 脚本中(SSDT / Visual Studio 至少会处理):if (CipherText.IsNull || DES_Key.IsNull || DES_IV.IsNull) return SqlString.Null;
ALTER FUNCTION [dbo].[DES_Decrypt]( @CipherText [nvarchar](max), @DES_Key [varbinary](8000), @DES_IV [varbinary](8000) ) RETURNS [nvarchar](max) WITH EXECUTE AS CALLER, RETURNS NULL ON NULL INPUT AS EXTERNAL NAME [YourAssembly].[YourClass].[DES_Decrypt];
WITH
子句中的RETURNS NULL ON NULL INPUT
选项起到了神奇的作用 ;-)。NULL
越多,效率就越高,因为 SQL 服务器不需要调用代码,因为它已经知道答案。请记住,此选项 returns aNULL
if any input isNULL
,因此如果任何输入参数都需要传入NULL
,则此选项无效。
有关使用 SQLCLR 的更多信息,请访问我的网站:SQLCLR Info