c# .net password-encryption securestring

c# - Hashing SecureString en.NET



password-encryption (4)

¿Me he perdido algo aquí?

Sí, lo tienes, bastante fundamental en eso. No puede arrastrar la copia de la matriz que queda cuando el recolector de basura compacta el montón. Marshal.SecureStringToBSTR (ss) está bien porque un BSTR se asigna en una memoria no administrada, por lo que tendrá un puntero confiable que no cambiará. En otras palabras, no hay problema en fregar esa.

Sin embargo, la matriz de byte[] bytes su byte[] bytes contiene la copia de la cadena y se asigna en el montón del GC. Es probable que induzca una recolección de basura con la matriz hash []. Se evita fácilmente pero, por supuesto, tiene poco control sobre otros subprocesos en su proceso de asignación de memoria e inducción de una colección. O para el caso, un GC de fondo que ya estaba en progreso cuando su código comenzó a ejecutarse.

El punto de SecureString es nunca tener una copia clara de la cadena en la memoria recolectada en la basura. Copiarlo en un array gestionado violaba esa garantía. Si desea que este código sea seguro, tendrá que escribir un método hash () que tome el IntPtr y solo lea a través de ese puntero.

Tenga en cuenta que si su hash necesita coincidir con un hash computado en otra máquina, entonces no puede ignorar la codificación que esa máquina usaría para convertir la cadena en bytes.

En .NET, tenemos la clase SecureString, que está muy bien hasta que usted trata de usarla, ya que (por ejemplo) hash la cadena, necesita el texto plano. He intentado escribir una función que hace un hash de SecureString, dada una función de hash que toma una matriz de bytes y genera una matriz de bytes.

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

Creo que esto hará que la secuencia se distorsione correctamente y que se borrarán correctamente de la memoria cualquier copia del texto sin formato en el momento en que se devuelve la función, asumiendo que la función hash proporcionada se comporta bien y no hace ninguna copia de la entrada que no lo hace. fregarse a sí mismo. ¿Me he perdido algo aquí?


Como complemento a la respuesta de Hans, aquí hay una sugerencia sobre cómo implementar el hasher. Hans sugiere pasar el puntero a la cadena no administrada a la función hash, pero eso significa que el código del cliente (= la función hash) necesita lidiar con la memoria no administrada. Eso no es lo ideal.

Por otro lado, puede reemplazar la devolución de llamada por una instancia de la siguiente interfaz:

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

De esa manera, el hasher (aunque se vuelve un poco más complejo) puede implementarse totalmente en terrenos administrados sin filtrar información segura. Tu HashSecureString se vería como sigue:

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

Tenga en cuenta el bloque finally para asegurarse de que la memoria no administrada esté en cero, sin importar qué chanchullos haga la instancia de hasher.

Aquí hay una implementación simple (y no muy útil) de Hasher para ilustrar la interfaz:

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


Como complemento adicional, ¿no podría incluir la lógica @KonradRudolph y @HansPassant suministrada en una implementación personalizada de Stream ?

Esto le permitiría utilizar el HashAlgorithm.ComputeHash(Stream) , que mantendría la interfaz administrada (aunque le correspondería disponer de la transmisión a tiempo).

Por supuesto, usted está a merced de la implementación de HashAlgorithm en cuanto a la cantidad de datos que termina en la memoria a la vez (pero, por supuesto, ¡para eso es la fuente de referencia!)

Solo una idea...

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


Siempre existe la posibilidad de utilizar las CryptoApi no administradas de CryptoApi o CNG . Tenga en cuenta que SecureString se diseñó con un consumidor no administrado que tiene en mente el control total sobre la administración de la memoria.

Si desea mantener el C #, debe fijar la matriz temporal para evitar que el GC se mueva antes de tener la oportunidad de borrarlo:

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