在.NET中散列SecureString

在.NET中,我们有SecureString类,在你尝试使用它之前一切都很好,因为(例如)散列字符串,你需要明文。 我已经开始编写一个函数来散列SecureString,给定一个带字节数组并输出字节数组的散列函数。

private static byte[] HashSecureString(SecureString ss, Func hash) { // Convert the SecureString to a BSTR IntPtr bstr = Marshal.SecureStringToBSTR(ss); // BSTR contains the length of the string in bytes in an // Int32 stored in the 4 bytes prior to the BSTR pointer int length = Marshal.ReadInt32(bstr, -4); // Allocate a byte array to copy the string into byte[] bytes = new byte[length]; // Copy the BSTR to the byte array Marshal.Copy(bstr, bytes, 0, length); // Immediately destroy the BSTR as we don't need it any more Marshal.ZeroFreeBSTR(bstr); // Hash the byte array byte[] hashed = hash(bytes); // Destroy the plaintext copy in the byte array for (int i = 0; i < length; i++) { bytes[i] = 0; } // Return the hash return hashed; } 

我相信这将正确地对字符串进行哈希处理,并且在函数返回时将正确地从内存中擦除明文的任何副本,假设提供的哈希函数表现良好并且不进行任何不具有的输入副本擦洗自己。 我在这里错过了什么吗?

我在这里错过了什么吗?

是的,你有一个相当基础的。 当垃圾收集器压缩堆时,您无法清除留下的数组副本。 Marshal.SecureStringToBSTR(ss)没问题,因为BSTR是在非托管内存中分配的,因此将有一个不会改变的可靠指针。 换句话说,没有问题擦洗那一个。

但是, byte[] bytes数组包含字符串的副本,并在GC堆上分配。 您可能会使用散列[]数组引发垃圾回收。 很容易避免但当然你几乎无法控制进程中分配内存和引入集合的其他线程。 或者就此而言,代码开始运行时已经在进行的后台GC。

SecureString的要点是永远不要在垃圾收集的内存中拥有字符串的明文副本。 将其复制到托管数组会违反该保证。 如果你想使这段代码安全,那么你将不得不编写一个hash()方法,该方法接受IntPtr并只读取该指针。

请注意,如果您的哈希需要匹配在另一台机器上计算的哈希,那么您不能忽略该机器将用于将字符串转换为字节的编码。

始终有可能使用非托管CryptoApi或CNGfunction。 请记住, SecureStringSecureString托管消费者设计的,它完全控制了内存管理。

如果你想坚持使用C#,你应该固定临时数组以防止GC在你有机会擦除它之前移动它:

 private static byte[] HashSecureString(SecureString input, Func hash) { var bstr = Marshal.SecureStringToBSTR(input); var length = Marshal.ReadInt32(bstr, -4); var bytes = new byte[length]; var bytesPin = GCHandle.Alloc(bytes, GCHandleType.Pinned); try { Marshal.Copy(bstr, bytes, 0, length); Marshal.ZeroFreeBSTR(bstr); return hash(bytes); } finally { for (var i = 0; i < bytes.Length; i++) { bytes[i] = 0; } bytesPin.Free(); } } 

作为汉斯答案的补充,这里提出了如何实施哈希的建议。 Hans建议将指向非托管字符串的指针传递给哈希函数,但这意味着客户端代码(=哈希函数)需要处理非托管内存。 那不太理想。

另一方面,您可以通过以下接口的实例替换回调:

 interface Hasher { void Reinitialize(); void AddByte(byte b); byte[] Result { get; } } 

这样,哈希(虽然它变得稍微复杂一些)可以完全在管理的土地上实现,而不会泄漏安全信息。 您的HashSecureString将如下所示:

 private static byte[] HashSecureString(SecureString ss, Hasher hasher) { IntPtr bstr = Marshal.SecureStringToBSTR(ss); try { int length = Marshal.ReadInt32(bstr, -4); hasher.Reinitialize(); for (int i = 0; i < length; i++) hasher.AddByte(Marshal.ReadByte(bstr, i)); return hasher.Result; } finally { Marshal.ZeroFreeBSTR(bstr); } } 

注意finally块以确保非托管内存为零,无论hasher实例做什么恶作剧。

这是一个简单(并不是非常有用)的Hasher实现来说明接口:

 sealed class SingleByteXor : Hasher { private readonly byte[] data = new byte[1]; public void Reinitialize() { data[0] = 0; } public void AddByte(byte b) { data[0] ^= b; } public byte[] Result { get { return data; } } } 

作为进一步的补充,您是否可以将提供的逻辑@KonradRudolph和@HansPassant包装到自定义Stream实现中?

这将允许您使用HashAlgorithm.ComputeHash(Stream)方法,该方法将保持接口的管理(尽管您可以及时处理流)。

当然,你受HashAlgorithm实现的支配,因为一次有多少数据在内存中结束(当然,这就是参考源的用途!)

只是一个想法……

 public class SecureStringStream : Stream { public override bool CanRead { get { return true; } } public override bool CanWrite { get { return false; } } public override bool CanSeek { get { return false; } } public override long Position { get { return _pos; } set { throw new NotSupportedException(); } } public override void Flush() { throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } private readonly IntPtr _bstr = IntPtr.Zero; private readonly int _length; private int _pos; public SecureStringStream(SecureString str) { if (str == null) throw new ArgumentNullException("str"); _bstr = Marshal.SecureStringToBSTR(str); try { _length = Marshal.ReadInt32(_bstr, -4); _pos = 0; } catch { if (_bstr != IntPtr.Zero) Marshal.ZeroFreeBSTR(_bstr); throw; } } public override long Length { get { return _length; } } public override int Read(byte[] buffer, int offset, int count) { if (buffer == null) throw new ArgumentNullException("buffer"); if (offset < 0) throw new ArgumentOutOfRangeException("offset"); if (count < 0) throw new ArgumentOutOfRangeException("count"); if (offset + count > buffer.Length) throw new ArgumentException("offset + count > buffer"); if (count > 0 && _pos++ < _length) { buffer[offset] = Marshal.ReadByte(_bstr, _pos++); return 1; } else return 0; } protected override void Dispose(bool disposing) { try { if (_bstr != IntPtr.Zero) Marshal.ZeroFreeBSTR(_bstr); } finally { base.Dispose(disposing); } } } void RunMe() { using (SecureString s = new SecureString()) { foreach (char c in "jimbobmcgee") s.AppendChar(c); s.MakeReadOnly(); using (SecureStringStream ss = new SecureStringStream(s)) using (HashAlgorithm h = MD5.Create()) { Console.WriteLine(Convert.ToBase64String(h.ComputeHash(ss))); } } }