如何在不安全代码中连接和散列用户名和密码(存​​储在安全字符串中)

How to concatenate and hash a username and password (stored in a secure string) in unsafe code

我试图坚持上次执行程序时用户名和密码组合是否有效,但不存储用户名和密码本身。目标不是验证,只是为了防止不必要地尝试使用无效凭据,这可能会使用户被锁定在服务之外(在本例中为 SharePoint,但这与此处无关)。

我的方法是连接用户名和密码并采用 MD5 散列(速度很快,并且会根据提供的 username/password 组合进行验证)。

原来这需要一堆我不知道的东西。请在下面查看我当前(无效)的方法,如果有人可以提供有关我应该做什么的指导,那将非常有用。

unsafe
{
    byte[] usernamePart = Encoding.Unicode.GetBytes(this.Username);
    IntPtr unmanagedPwd = IntPtr.Zero;
    unmanagedPwd = Marshal.SecureStringToGlobalAllocUnicode(this.Password);

    // Question 1: How many bytes do I need to copy?
    int lenPasswordArray = somemethod(this.Password);
    IntPtr unsafeBuffer = Marshal.AllocHGlobal(usernamePart.Length + lenPasswordArray);
    Marshal.Copy(usernamePart, 0, unsafeBuffer, usernamePart.Length);

    // Question 2: Marshal.Copy takes a byte[]; I have an IntPtr. How to copy after the username
    Marshal.Copy(unmanagedPwd, 0, IntPtr.Add(unsafeBuffer, lenPasswordArray), lenPasswordArray);

    var provider = new System.Security.Cryptography.MD5CryptoServiceProvider();
    //Question 3: I now have an IntPtr with username and password together. But 
    // provider takes a byte[]... I don't want to convert to byte[], because it'll end up
    // with the same System.String problem
    var targetHash = provider.ComputeHash(unsafeBuffer);

    // Question 4: How do I clean up safely?
    Marshal.ZeroFreeGlobalAllocUnicode(unmanagedPwd);
    Marshal.Copy(new byte[usernamePart.Length + lenPasswordArray], 0, unsafeBuffer, usernamePart.Length + lenPasswordArray);
    Marshal.FreeHGlobal(unsafeBuffer);
}

如评论中所述,我需要知道 4 件事:

编辑:为清楚起见,我想要的是安全版本:

byte[] usernamePart = Encoding.Unicode.GetBytes(this.Username);
byte[] passwordPart = Encoding.Unicode.GetBytes(this.Password.ConvertToUnsecureString());
byte[] all = usernamePart.Concat(passwordPart).ToArray();
var provider = new System.Security.Cryptography.MD5CryptoServiceProvider();
return provider.ComputeHash(all).ToString();

遗憾的是,如果没有更多详细信息,将很难知道最佳答案是什么。这里缺少的一个特定细节是 SecureString 对象的来源。您创建它是为了执行此哈希吗?或者密码是否已由 SecureString 对象表示,您正在将其传递给其他 APIs?

如果是前者,则表明您的进程中已经有一个未加密的、非确定性生命周期字符串,其中包含密码。如果是后者,那么虽然密码的未加密版本的生命周期可能是确定的,但请注意,密码在不同的执行点最终仍会被解密。

也就是说,就您的具体问题而言:

How to work out the number of bytes allocated by SecureStringToGlobalAllocUnicode

在我看来,您应该能够相信将原文的长度加倍是可靠的。 SecureString.Length 属性 returns组成字符串的char个对象的个数,即16位UTF16值的个数,所以字节正好是它的两倍。 Length 属性 没有考虑采用两个 16 位值(即低位和高位代理)的 Unicode 代码点,因此字节长度计算应该是准确的。

就是说,如果您不相信……分配的字符串应该以 null 结尾,这样您就可以对字符串进行正常扫描。请注意,如果您对字符串使用 BSTR 方法,则该字符串会以 32 位 byte 计数(不是字符计数)为前缀来表示该字符串,不包括其空终止符;您可以通过从返回的 IntPtr 中减去 4 来检索它,从那里获取四个字节,然后将其转换回 int 值。

The appropriate function to use when I need n bytes after an IntPtr and don't want to allocate a managed byte[] and use Marshal.Copy

有很多方法可以做到这一点。我认为一种更简单的方法是 p/invoke Windows CopyMemory() 函数:

[DllImport("kernel32.dll")]
unsafe extern static void CopyMemory(void* destination, void* source, IntPtr size_t);

只需将适当的 IntPtr 值传递给该方法,使用 IntPtr.ToPointer() 方法或显式转换为可用的 void*。像这样使用:

unsafe
{
    CopyMemory(IntPtr.Add(unsafeBuffer, usernamePart.Length).ToPointer(),
        unmanagedPwd.ToPointer(), new IntPtr(lenPasswordArray));
}

在 .NET 4.6 中(根据 MSDN...我自己还没有使用过...仍然停留在 4.5 上),您可以(将能够)使用 Buffer.MemoryCopy() 方法。例如:

Buffer.MemoryCopy(unmanagedPwd.ToPointer(),
    IntPtr.Add(unsafeBuffer, usernamePart.Length).ToPointer(),
    lenPasswordArray,
    lenPasswordArray);

(请注意,我认为您在原始示例中有一个类型;您将 lenPasswordArray 添加到 unsafeBuffer 指针以确定要将密码数据复制到的位置。我已经更正了上面示例中使用用户名长度的错误,因为您似乎想在已复制的用户名数据之后立即复制密码数据。

How to encrypt those bytes

你这是什么意思?您是在问如何 散列 字节吗? IE。 运行 MD5散列算法在他们身上?请注意,这不是加密;没有实用的方法来解密该值(尽管存在 MD5 安全漏洞)。

如果您只是想散列字节,则需要一个可以在非托管内存上运行的 MD5 实现。我不确定 Windows 是否具有非托管 MD5 API,但它通常具有密码学。因此,您可以 p/invoke 访问这些功能。有关详细信息,请参阅 Cryptographic Service Providers

我会注意到,此时,您现在在内存中有未加密的数据,位于两个不同的位置:调用 SecureStringToGlobalAllocUnicode() 时最初解密的内存块,当然还有您创建的新副本复制到 unsafeBuffer。与 System.String 对象相比,您可以更严格地控​​制这些缓冲区的生命周期,但除此之外,在恶意代码检查您的进程并恢复明文的生命周期中,您面临着相同的风险。

如果您的意思不是散列,请更具体地说明您希望如何以及为什么要 "encrypt those bytes"。

How to reliably zero out and free anything I've allocated (I'm very new to unsafe code)

我不知道 unsafe 和这个问题有什么关系。的确,除了你需要用void*的地方,你的代码示例本身不需要unsafe.

至于将内存缓冲区清零,你的代码对我来说似乎没问题。如果你想要比分配一个全新的 byte[] 缓冲区更有效的东西,只是为了将另一个内存位置设置为全零,你可以 p/invoke SecureZeroMemory() Windows 函数相反(类似于上面的 CopyMemory() 示例)。


现在,所有上面所说的,正如我在评论中提到的,在我看来,有一些方法可以在托管的、安全的代码中做到这一点,只需自己明确地控制中间对象的生命周期。例如:

static string SecureComputeHash(string username, SecureString password)
{
    byte[] textBytes = null;
    IntPtr textChars = IntPtr.Zero;

    try
    {
        byte[] userNameBytes = Encoding.Unicode.GetBytes(username);

        textChars = Marshal.SecureStringToGlobalAllocUnicode(password);
        int passwordByteLength = password.Length * 2;
        textBytes = new byte[userNameBytes.Length + passwordByteLength];

        userNameBytes.CopyTo(textBytes, 0);
        Marshal.Copy(textChars, textBytes, userNameBytes.Length, passwordByteLength);

        using (MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider())
        {
            return Convert.ToBase64String(provider.ComputeHash(textBytes));
        }
    }
    finally
    {
        // Clean up temporary buffers
        if (textChars != IntPtr.Zero)
        {
            Marshal.ZeroFreeGlobalAllocUnicode(textChars);
        }

        if (textBytes != null)
        {
            for (int i = 0; i < textBytes.Length; i++)
            {
                textBytes[i] = 0;
            }
        }
    }
}

(我使用 base64 编码将您的散列 byte[] 结果转换为字符串。您在示例中显示的对 ToString() 的简单调用不会做任何有用的事情,因为它只是 returns byte[] 对象的类型名称。我认为 base64 是存储散列数据最有效、最有用的方法,但您当然可以使用您认为有用的任何表示形式)。

以上假定您的密码已经在 SecureString 对象中。当然,如果你只是简单地从其他一些非加密对象初始化一个 SecureString 对象,你可以以不同的方式执行上述操作,例如直接从非加密对象创建一个 char[] (这可能是例如 stringStringBuilder).

我看不出您的非托管方法比上述方法有何显着优势。

我能看到的唯一例外是,如果您担心 MD5CryptoServiceProvider class 可能会在它自己的内部数据结构中留下一些数据副本。这可能是一个有效的担忧,但是你也会对你的非托管方法有这种担忧,因为你没有展示你实际会在那里使用什么 MD5 实现(你必须确保你使用的任何实现都小心不留下您的数据副本)。

就我个人而言,我怀疑(但不确定)给定 MD5CryptoServiceProvider class 名称中的 "crypto" 这个词,class 小心地清除临时内存缓冲区。

除了 可能 的担忧之外,完全托管的方法完成了同样的事情,恕我直言,没有那么大惊小怪。