在.net中散列SecureString
本文关键字:SecureString net | 更新日期: 2023-09-27 17:52:42
在。net中,我们有SecureString类,这一切都很好,直到你来尝试和使用它,(例如)散列字符串,你需要明文。我在这里试着写了一个函数来散列SecureString,给定一个散列函数,它接受一个字节数组并输出一个字节数组。
private static byte[] HashSecureString(SecureString ss, Func<byte[], byte[]> 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;
}
我相信这将正确地散列字符串,并且在函数返回时将正确地从内存中清除明文的任何副本,假设提供的散列函数表现良好,并且不生成任何输入的副本,它不会自己清除。我有遗漏什么吗?
我有遗漏什么吗?
是的,你有,一个相当基本的。当垃圾收集器压缩堆时,无法清除遗留的数组副本。securestringtobstr (ss)是可以的,因为BSTR是在非托管内存中分配的,所以会有一个不会改变的可靠指针。换句话说,洗那个没有问题。
您的byte[] bytes
数组包含字符串的副本,是在GC堆上分配的。您可以使用散列[]数组诱导垃圾收集。这很容易避免,但当然您无法控制进程中分配内存和诱导集合的其他线程。或者当你的代码开始运行时,后台GC已经在进行了。
SecureString的要点是永远不会在垃圾收集的内存中拥有字符串的明文副本。将其复制到托管数组违反了该保证。如果你想使这段代码安全,那么你将不得不编写一个hash()方法,该方法接受IntPtr并且只读取该指针。
请注意,如果您的哈希需要匹配另一台机器上计算的哈希,那么您不能忽略该机器将用于将字符串转换为字节的编码。
总是有可能使用非托管的CryptoApi或CNG函数。请记住,SecureString
是用非托管消费者设计的,它完全控制内存管理。
如果你想坚持c#,你应该固定临时数组,以防止GC在你有机会清理它之前移动它:
private static byte[] HashSecureString(SecureString input, Func<byte[], byte[]> 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的回答的补充,这里有一个如何实现哈希器的建议。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
实现来说明接口:
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)
方法,这将保持接口的管理(尽管这将取决于您是否及时处置流)。
当然,对于每次在内存中最终有多少数据,您可以由hashalgalgorithm实现来决定(但是,当然,这就是参考源的作用!)
只是一个想法…
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)));
}
}
}