Хеширование SecureString в .NET.

В .NET у нас есть класс SecureString, который очень хорошо, пока вы не попытаетесь его использовать, например (хэш-строку), вам нужен открытый текст. Я хотел бы здесь написать функцию, которая будет hash 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;
}

Я полагаю, что это правильно приведет к хэшированию строки и будет корректно вычищать любые копии открытого текста из памяти к моменту возвращения функции, предполагая, что предоставленная хэш-функция хорошо себя ведет и не делает никаких копий ввода, что она не сглаживает себя. Я что-то пропустил?

Ответ 1

Я что-то пропустил?

Да, у вас есть довольно фундаментальный вариант. Вы не можете очистить копию оставшегося массива, когда сборщик мусора сжимает кучу. Marshal.SecureStringToBSTR(ss) в порядке, потому что BSTR выделяется в неуправляемой памяти, поэтому будет иметь надежный указатель, который не изменится. Другими словами, никаких проблем с очисткой этого.

Однако ваш массив byte[] bytes содержит копию строки и выделяется в куче GC. Вы можете заставить его собирать мусор с хешированным [] массивом. Легко избегается, но, конечно, у вас мало контроля над другими потоками в процессе выделения памяти и создания коллекции. Или, если на то пошло, фоновый GC, который уже был запущен, когда ваш код начал работать.

Точка SecureString никогда не должна иметь текстовую копию строки в сборнике мусора. Копирование его в управляемый массив нарушило эту гарантию. Если вы хотите сделать этот код защищенным, вам придется написать метод hash(), который принимает IntPtr и только считывает этот указатель.

Помните, что если ваш хэш должен соответствовать хешу, вычисленному на другом компьютере, вы не можете игнорировать кодировку, которую машина будет использовать, чтобы превратить строку в байты.

Ответ 2

В качестве дополнения к 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; }
    }
}

Ответ 3

В качестве дополнительного дополнения вы могли бы не обернуть логику @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)));
        }
    }
}

Ответ 4

Всегда существует возможность использования неуправляемого CryptoApi или CNG функций. Имейте в виду, что SecureString был разработан с неуправляемым потребителем, который имеет полный контроль над управлением памятью в виду.

Если вы хотите придерживаться С#, вы должны прикрепить временный массив, чтобы предотвратить его перемещение, прежде чем вы получите возможность его счистить:

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();
    }
}