The purpose here is to make it easy to use sensitive data that is already in the form of a SecureString
(example) without converting it to a String
object and risking more leaks than necessary.
SecureString isn't about total security, but it is about reducing attack surface. For example, when you call SecureString.AppendChar
there is a brief flash where it decrypts the contents, adds your character, and reencrypts. This is still better than storing your password in the clear on the heap for any amount of time.
So in a similar vein, if I'm to use a SecureString
as a SqlParameter
value, it's best to do as little as possible with the contents in the clear and erase it as soon as possible. This isn't about transport security to SQL server, just C# process memory that has the potential to be paged to disk and end up unerased, in the clear, for years.
Usage:
var secureString = new SecureString();
secureString.AppendChar('a');
secureString.AppendChar('q');
secureString.AppendChar('1');
using (var command = new SqlCommand("select case when @secureParam = 'aq1' then 'yes' else 'no' end", connection))
{
object returnValue;
using (command.Parameters.AddSecure("secureParam", secureString))
{
// At this point no copies exist in the clear
returnValue = (string)command.ExecuteScalar();
// Now one pinned String object exists in the clear (referenced at the internal property command.Parameters[0].CoercedValue)
}
// At this point no copies exist in the clear
}
Code:
public static class SecureSqlParameterExtensions
{
[DllImport("kernel32.dll", EntryPoint = "CopyMemory")]
private static extern void CopyMemory(IntPtr dest, IntPtr src, IntPtr count);
[DllImport("kernel32.dll", EntryPoint = "RtlZeroMemory")]
private static extern void ZeroMemory(IntPtr ptr, IntPtr count);
/// <summary>
/// You must dispose the return value as soon as SqlCommand.Execute* is called.
/// </summary>
public static IDisposable AddSecure(this SqlParameterCollection collection, string name, SecureString secureString)
{
var value = new SecureStringParameterValue(secureString);
collection.Add(name, SqlDbType.NVarChar).Value = value;
return value;
}
private sealed class SecureStringParameterValue : IConvertible, IDisposable
{
private readonly SecureString secureString;
private int length;
private string insecureManagedCopy;
private GCHandle insecureManagedCopyGcHandle;
public SecureStringParameterValue(SecureString secureString)
{
this.secureString = secureString;
}
#region IConvertible
public TypeCode GetTypeCode()
{
return TypeCode.String;
}
public string ToString(IFormatProvider provider)
{
if (insecureManagedCopy != null) return insecureManagedCopy;
if (secureString == null || secureString.Length == 0) return string.Empty;
// We waited till the last possible minute.
// Here's the plan:
// 1. Create a new managed string initialized to zero
// 2. Pin the managed string so the GC leaves it alone
// 3. Copy the contents of the SecureString into the managed string
// 4. Use the string as a SqlParameter
// 5. Zero the managed string after Execute* is called and free the GC handle
length = secureString.Length;
insecureManagedCopy = new string('\0', length);
insecureManagedCopyGcHandle = GCHandle.Alloc(insecureManagedCopy, GCHandleType.Pinned); // Do not allow the GC to move this around and leave copies behind
try
{
// This is the only way to read the contents, sadly.
// SecureStringToBSTR picks where to put it, so we have to copy it from there and zerofree the unmanaged copy as fast as possible.
var insecureUnmanagedCopy = Marshal.SecureStringToBSTR(secureString);
try
{
CopyMemory(insecureManagedCopyGcHandle.AddrOfPinnedObject(), insecureUnmanagedCopy, (IntPtr)(length * 2));
}
finally
{
if (insecureUnmanagedCopy != IntPtr.Zero) Marshal.ZeroFreeBSTR(insecureUnmanagedCopy);
}
// Now the string managed string has the contents in the clear.
return insecureManagedCopy;
}
catch
{
Dispose();
throw;
}
}
public void Dispose()
{
if (insecureManagedCopy == null) return;
insecureManagedCopy = null;
ZeroMemory(insecureManagedCopyGcHandle.AddrOfPinnedObject(), (IntPtr)(length * 2));
insecureManagedCopyGcHandle.Free();
}
public bool ToBoolean(IFormatProvider provider)
{
throw new NotImplementedException();
}
public char ToChar(IFormatProvider provider)
{
throw new NotImplementedException();
}
public sbyte ToSByte(IFormatProvider provider)
{
throw new NotImplementedException();
}
public byte ToByte(IFormatProvider provider)
{
throw new NotImplementedException();
}
public short ToInt16(IFormatProvider provider)
{
throw new NotImplementedException();
}
public ushort ToUInt16(IFormatProvider provider)
{
throw new NotImplementedException();
}
public int ToInt32(IFormatProvider provider)
{
throw new NotImplementedException();
}
public uint ToUInt32(IFormatProvider provider)
{
throw new NotImplementedException();
}
public long ToInt64(IFormatProvider provider)
{
throw new NotImplementedException();
}
public ulong ToUInt64(IFormatProvider provider)
{
throw new NotImplementedException();
}
public float ToSingle(IFormatProvider provider)
{
throw new NotImplementedException();
}
public double ToDouble(IFormatProvider provider)
{
throw new NotImplementedException();
}
public decimal ToDecimal(IFormatProvider provider)
{
throw new NotImplementedException();
}
public DateTime ToDateTime(IFormatProvider provider)
{
throw new NotImplementedException();
}
public object ToType(Type conversionType, IFormatProvider provider)
{
throw new NotImplementedException();
}
#endregion
}
}
SecureString
in aDbParameter
, not necessarily aSqlParameter
at all. \$\endgroup\$