C# - 比较两个SecureStrings是否相等

41
我有一个WPF应用程序,其中有两个PasswordBoxes,一个用于密码,另一个用于再次输入密码以进行确认。 我想使用PasswordBox.SecurePassword来获取密码的SecureString,但在接受密码之前,我需要能够比较两个PasswordBoxes的内容以确保它们相等。 但是,两个相同的SecureStrings被视为不相等:
var secString1 = new SecureString();
var secString2 = new SecureString();
foreach (char c in "testing")
{
    secString1.AppendChar(c);
    secString2.AppendChar(c);
}
Assert.AreEqual(secString1, secString2); // This fails

我认为比较 PasswordBoxes 的 Password 属性将会破坏访问仅限 SecurePassword 的目的,因为这样我将读取明文密码。那么,在不牺牲安全性的情况下,如何比较这两个密码呢?

编辑:根据这个问题,我正在查看这篇博客文章,了解如何“使用 Marshal 类将 SecureString 转换为 ANSI 或 Unicode 或 BSTR”,然后也许我就可以比较它们了。

5个回答

40

这个代码块没有不安全的部分,也不会以明文形式显示密码:

public static bool IsEqualTo(this SecureString ss1, SecureString ss2)
{
 IntPtr bstr1 = IntPtr.Zero;
 IntPtr bstr2 = IntPtr.Zero;
 try
 {
  bstr1 = Marshal.SecureStringToBSTR(ss1);
  bstr2 = Marshal.SecureStringToBSTR(ss2);
  int length1 = Marshal.ReadInt32(bstr1, -4);
  int length2 = Marshal.ReadInt32(bstr2, -4);
  if (length1 == length2)
  {
   for (int x = 0; x < length1; ++x)
   {
    byte b1 = Marshal.ReadByte(bstr1, x);
    byte b2 = Marshal.ReadByte(bstr2, x);
    if (b1 != b2) return false;
   }
  }
  else return false;
  return true;
 }
 finally
 {
  if (bstr2 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr2);
  if (bstr1 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr1);
 }
}

编辑:根据Alex J.的建议修复了泄漏问题。


1
如果在Marshal.SecureStringToBSTR()调用之后,但在try块之前发生异常,则可能会泄漏IntPtrs。您应该将它们放在try块内。 - Alex J
1
应该在类中构建类似于 SecureString.Equals(secstringone, secstringtwo); 这样的更加“兄弟化”的功能,做得不错。 - stackuser83
1
这段代码容易受到时间攻击的威胁。对于原始问题来说这并不重要,但如果你正在使用它来验证密码(你几乎肯定不应该这样做,因为你应该使用哈希值),可以通过在循环之前声明 int result = 0;,将循环体更改为 result |= Marshal.ReadByte(bstr1, x) ^ Marshal.ReadByte(bstr2, x); 并返回 result == 0 来缓解这种情况。这将使比较成为恒定时间,因为它总是访问字符串中的每个字符 - 它仍然会泄漏长度,但这是不可避免的。 - DaveRandom
2
@NikolaNovak,好的,我想我只是不明白你在回答中所说的“不会以明文形式显示密码”的意思,因为Marshal.SecureStringToBSTR确实会解密SecureString的内容,并将解密后的副本加载到返回的内存地址中,这就是MS所谓的“非托管内存中的明文字符串” https://msdn.microsoft.com/en-us/library/system.security.securestring(v=vs.110).aspx#interop - Gregor y
1
@Gregory 我的意思就是这样。它不会以纯文本形式显示。你将看到 65 而不是 'A'。虽然不是很安全,但如果你的潜在攻击者在你调试时恰好在你身边观察,那么这已经足够了。 - Nikola Novak
显示剩余6条评论

19

看起来你可以使用这个来比较两个SecureStrings

它使用不安全的代码来遍历字符串:

bool SecureStringEqual(SecureString s1, SecureString s2)  
{  
    if (s1 == null)  
    {  
        throw new ArgumentNullException("s1");  
    }  
    if (s2 == null)  
    {  
        throw new ArgumentNullException("s2");  
    }  

    if (s1.Length != s2.Length)  
    {  
        return false;  
    }  

    IntPtr bstr1 = IntPtr.Zero;  
    IntPtr bstr2 = IntPtr.Zero;  

    RuntimeHelpers.PrepareConstrainedRegions();  

    try 
    {  
        bstr1 = Marshal.SecureStringToBSTR(s1);  
        bstr2 = Marshal.SecureStringToBSTR(s2);  

        unsafe 
        {  
            for (Char* ptr1 = (Char*)bstr1.ToPointer(), ptr2 = (Char*)bstr2.ToPointer();  
                *ptr1 != 0 && *ptr2 != 0;  
                 ++ptr1, ++ptr2)  
            {  
                if (*ptr1 != *ptr2)  
                {  
                    return false;  
                }  
            }  
        }  

        return true;  
    }  
    finally 
    {  
        if (bstr1 != IntPtr.Zero)  
        {  
            Marshal.ZeroFreeBSTR(bstr1);  
        }  

        if (bstr2 != IntPtr.Zero)  
        {  
            Marshal.ZeroFreeBSTR(bstr2);  
        }  
    }  
} 

我已经修改了以下代码,使其不需要使用不安全的代码(请注意,当调试时您仍可以以纯文本形式查看字符串):

  Boolean SecureStringEqual(SecureString secureString1, SecureString secureString2)
  {
     if (secureString1 == null)
     {
        throw new ArgumentNullException("s1");
     }
     if (secureString2 == null)
     {
        throw new ArgumentNullException("s2");
     }

     if (secureString1.Length != secureString2.Length)
     {
        return false;
     }

     IntPtr ss_bstr1_ptr = IntPtr.Zero;
     IntPtr ss_bstr2_ptr = IntPtr.Zero;

     try
     {
        ss_bstr1_ptr = Marshal.SecureStringToBSTR(secureString1);
        ss_bstr2_ptr = Marshal.SecureStringToBSTR(secureString2);

        String str1 = Marshal.PtrToStringBSTR(ss_bstr1_ptr);
        String str2 = Marshal.PtrToStringBSTR(ss_bstr2_ptr);

        return str1.Equals(str2);
     }
     finally
     {
        if (ss_bstr1_ptr != IntPtr.Zero)
        {
           Marshal.ZeroFreeBSTR(ss_bstr1_ptr);
        }

        if (ss_bstr2_ptr != IntPtr.Zero)
        {
           Marshal.ZeroFreeBSTR(ss_bstr2_ptr);
        }
     }
  }

2
SecureString并没有重写Equals方法,因此这个方法只检查引用相等性。 - Will Vousden
1
SwDevMan81: 那个 social.msdn 的链接建议对我有用。我使用了 http://www.csharpfriends.com/Articles/getArticle.aspx?articleID=351 来允许我的项目使用不安全的代码。你应该将你链接的 MSDN 文章作为答案提供,我会选择它。如果有人有关于修复代码而不使用 unsafe 的建议,那也会很有帮助! - Sarah Vessels
6
@SwDevMan表示:天啊,这个代码肯定可以运行,并且没有“unsafe”块,但是通过调试器逐步执行它会显示那两个名为“str1”和“str2”的字符串最终以明文形式呈现出敏感数据。我更喜欢“unsafe”版本,因为它只比较指针;在使用调试器逐步执行时,我不会看到以明文形式呈现的可读数据。 - Sarah Vessels
1
@MikeChristian - 我发布了一个不安全的解决方案链接。我只是提供了一个替代解决方案示例,它不使用不安全的代码。如果您担心暴露明文,可以随意使用不安全的解决方案。 - SwDevMan81
为什么要在 null 上抛出异常?只需返回一个事实上的答案(如果两个都是 null,则为 true,如果其中一个为 null,则为 false)。 - Mike Bruno
显示剩余6条评论

1
如果代码运行在Windows Vista或更高版本上,这里有一个基于CompareStringOrdinal Windows函数的版本,因此没有纯文本,所有缓冲区都保持未管理状态。额外的好处是它支持不区分大小写的比较。
public static bool EqualsOrdinal(this SecureString text1, SecureString text2, bool ignoreCase = false)
{
    if (text1 == text2)
        return true;

    if (text1 == null)
        return text2 == null;

    if (text2 == null)
        return false;

    if (text1.Length != text2.Length)
        return false;

    var b1 = IntPtr.Zero;
    var b2 = IntPtr.Zero;
    try
    {
        b1 = Marshal.SecureStringToBSTR(text1);
        b2 = Marshal.SecureStringToBSTR(text2);
        return CompareStringOrdinal(b1, text1.Length, b2, text2.Length, ignoreCase) == CSTR_EQUAL;
    }
    finally
    {
        if (b1 != IntPtr.Zero)
        {
            Marshal.ZeroFreeBSTR(b1);
        }

        if (b2 != IntPtr.Zero)
        {
            Marshal.ZeroFreeBSTR(b2);
        }
    }
}

public static bool EqualsOrdinal(this SecureString text1, string text2, bool ignoreCase = false)
{
    if (text1 == null)
        return text2 == null;

    if (text2 == null)
        return false;

    if (text1.Length != text2.Length)
        return false;

    var b = IntPtr.Zero;
    try
    {
        b = Marshal.SecureStringToBSTR(text1);
        return CompareStringOrdinal(b, text1.Length, text2, text2.Length, ignoreCase) == CSTR_EQUAL;
    }
    finally
    {
        if (b != IntPtr.Zero)
        {
            Marshal.ZeroFreeBSTR(b);
        }
    }
}

private const int CSTR_EQUAL = 2;

[DllImport("kernel32")]
private static extern int CompareStringOrdinal(IntPtr lpString1, int cchCount1, IntPtr lpString2, int cchCount2, bool bIgnoreCase);

[DllImport("kernel32")]
private static extern int CompareStringOrdinal(IntPtr lpString1, int cchCount1, [MarshalAs(UnmanagedType.LPWStr)] string lpString2, int cchCount2, bool bIgnoreCase);

1

将 @NikolaNovák 的答案翻译为普通的 PowerShell:

param(
[Parameter(mandatory=$true,position=0)][SecureString]$ss1,
[Parameter(mandatory=$true,position=1)][SecureString]$ss2
)

function IsEqualTo{
   param(
    [Parameter(mandatory=$true,position=0)][SecureString]$ss1,
    [Parameter(mandatory=$true,position=1)][SecureString]$ss2
   )

  begin{
    [IntPtr] $bstr1 = [IntPtr]::Zero;
    [IntPtr] $bstr2 = [IntPtr]::Zero;
    [bool]$answer=$true;
  }

  process{
    try{
        $bstr1 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss1);
        $bstr2 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss2);
        [int]$length1 = [System.Runtime.InteropServices.Marshal]::ReadInt32($bstr1, -4);
        [int]$length2 = [System.Runtime.InteropServices.Marshal]::ReadInt32($bstr2, -4);

        if ($length1 -eq $length2){
            for ([int]$x -eq 0; $x -lt $length1; ++$x){
                [byte]$b1 = [System.Runtime.InteropServices.Marshal]::ReadByte($bstr1, $x);
                [byte]$b2 = [System.Runtime.InteropServices.Marshal]::ReadByte($bstr2, $x);
                if ($b1  -ne $b2){
                    $answer=$false;
                }
            }
        }
        else{ $answer=$false;}
    }
    catch{
    }
    finally
    {
        if ($bstr2 -ne [IntPtr]::Zero){ [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr2)};
        if ($bstr1 -ne [IntPtr]::Zero){ [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr1)};
    }
  }
  END{
    return $answer
  }
}
IsEqualTo -ss1 $ss1  -ss2 $ss2

0

你可以采用不同的方法。我在我的代码中遇到了同样的问题,即密码和确认密码都是 SecureString 类型的比较。我意识到最终目标是将新密码存储为 base-64 字符串。所以我所做的就是简单地将确认字符串通过与写入数据库相同的代码传递。然后,当我有两个 base-64 字符串时,在那一点上进行比较,这是一个简单的字符串比较。

需要更多的管道来将任何失败通信传递回 UI 层,但最终结果似乎是可以接受的。这段代码希望足以给出基本思路。

private string CalculateHash( SecureString securePasswordString, string saltString )
{
    IntPtr unmanagedString = IntPtr.Zero;
    try
    {
        unmanagedString = Marshal.SecureStringToGlobalAllocUnicode( securePasswordString );
        byte[] passwordBytes = Encoding.UTF8.GetBytes( Marshal.PtrToStringUni( unmanagedString ) );
        byte[] saltBytes = Encoding.UTF8.GetBytes( saltString );
        byte[] passwordPlusSaltBytes = new byte[ passwordBytes.Length + saltBytes.Length ];
        Buffer.BlockCopy( passwordBytes, 0, passwordPlusSaltBytes, 0, passwordBytes.Length );
        Buffer.BlockCopy( saltBytes, 0, passwordPlusSaltBytes, passwordBytes.Length, saltBytes.Length );
        HashAlgorithm algorithm = new SHA256Managed();
        return Convert.ToBase64String( algorithm.ComputeHash( passwordPlusSaltBytes ) );
    }
    finally
    {
        if( unmanagedString != IntPtr.Zero )
            Marshal.ZeroFreeGlobalAllocUnicode( unmanagedString );
    }
}

string passwordSalt = "INSERT YOUR CHOSEN METHOD FOR CONSTRUCTING A PASSWORD SALT HERE";
string passwordHashed = CalculateHash( securePasswordString, passwordSalt );
string confirmPasswordHashed = CalculateHash( secureConfirmPasswordString, passwordSalt );
if( passwordHashed == confirmPasswordHashed )
{
    // Both matched so go ahead and persist the new password.
}
else
{
    // Strings don't match, so communicate the failure back to the UI.
}

我在安全编程方面还是个新手,所以非常欢迎任何改进建议。


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