24

在 .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;
}

我相信这将正确地散列字符串,并在函数返回时正确地从内存中清除任何明文副本,假设提供的散列函数表现良好并且不会复制它没有的输入擦洗自己。我在这里错过了什么吗?

4

4 回答 4

14

我在这里错过了什么吗?

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

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

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

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

于 2013-01-12T12:51:44.417 回答
5

总是有可能使用非托管CryptoApiCNG函数。请记住,这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();
    }
}
于 2015-04-17T12:12:27.863 回答
3

作为对汉斯回答的补充,这里有一个如何实现哈希器的建议。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; }
    }
}
于 2013-01-12T13:27:52.863 回答
2

作为进一步的补充,您能否不将 @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)));
        }
    }
}
于 2014-01-08T21:43:26.010 回答