Search code examples
vb.netsecurestring

Is it possible to safely get a SecureString value from VB .NET?


I've always felt like SecureString was a little odd, but assumed most of my issues with it were due to security problems I don't understand. Today I decided to sit down and teach myself about it, but I've hit what seems like a fatal snag.

The scenario I envision is "user enters password into text box, that password is hashed and compared to a stored hash". At first I was worried that the text box contained the string, but then I realized you could roll a custom text box that uses SecureString as its store. Cool. It's the "that password is hashed and compared..." part that is giving me trouble.

My first hack at the problem in VB .NET was naive and wrong:

Dim passwordHandle As IntPtr
Dim insecurePassword As String = Nothing
Try
    passwordHandle = Marshal.SecureStringToBSTR(_password)
    insecurePassword = Marshal.PtrToStringBSTR(passwordHandle)
Catch ex As Exception

Finally
    If passwordHandle <> IntPtr.Zero Then
        Marshal.ZeroFreeBSTR(passwordHandle)
    End If
End Try

If insecurePassword <> Nothing Then
    ' Do hash and comparison
End If

This just stuffs the password into a regular string and defeats the purpose of using SecureString in the first place. So I kept searching and found a blog post that solves the problem nicely in C#: the string is made into a BSTR, copied into a pinned string, then both the BSTR and pinned string are zeroed after use. This seems like a much better idea because it minimizes the amount of time that the insecure string is in memory. However, it doesn't look like there's a way to pull this off in VB .NET. C# is using its unsafe code feature to do pointer manipulations, but VB .NET cannot do this. I had a look at Marhsall.Copy(), but it looks like it's oriented towards arrays. I thought about trying to cast the IntPtr variables for the object and BSTR to Strings, but that still left me using a method like String.Replace(), which will create a new string.

Is it not possible to do this from VB .NET at all, or is there something I'm missing?

edit I'm accepting AMissico's answer with only slight reservations. The Marshal.ReadByte() method will copy a byte from unmanaged memory and create a byte in unmanaged memory. This produces the small chance that an attacker could find the individual characters of the password. I think that's far less than the odds of finding an entire string, but the C# in the (apparently defunct) article I referenced was able to use unsafe code to avoid this neatly. The thought process was it used GCHandle to pin a string in memory, then used unsafe code to get around the immutability of .NET strings. Clever trick that seems impossible in VB .NET. I'll try to come back with the C# code itself.


Solution

  • The link to "Marshaling SecureString Passwords to String - Mark Nicholson " at http://dotnet.org.za/markn/archive/2008/10/04/handling-passwords.aspx finally displayed.

    So your statement, "C# is using its unsafe code feature to do pointer manipulations," seems to be addressed with "HOW TO: Wrap a UCOMIStream in a Stream Class in Visual Basic .NET" at http://support.microsoft.com/kb/321695. (I searched on "AddrOfPinnedObject".)

    I did not read your whole question (link to post always timed out), but are these classes and testing code helpful? The password never lives as a System.String; therefore, you need a SecureStringTextBox implementation as noted in your question.

    I dislike adding all this code. Let me know what code is helpful, and I will edit the answer to keep only the helpful stuff.

    Imports System.Security
    Imports System.Security.Principal
    Imports System.Security.Permissions
    Imports System.Runtime.InteropServices
    
    ''' <summary>
    ''' Helper class to programmatically impersonate a user, load and unload a user's profile, and perform other maintenance-related tasks for impersonating a user.
    ''' </summary>
    Public Class ImpersonationHelper
        Implements IDisposable
    
    #Region " IDisposable Implementaton "
    
        Private _disposed As Boolean
    
        Protected Overrides Sub Finalize()
            Dispose(False)
            MyBase.Finalize()
        End Sub
    
        ''' <summary>
        ''' Implementation of the <b>IDisposable</b> interface.
        ''' </summary>
        ''' <remarks>This method calls <see>Undo</see> if impersonation is still being performed. This method calls the common language runtime version of the Dispose method.</remarks>
        Public Overloads Sub Dispose() Implements IDisposable.Dispose
            Dispose(True)
            System.GC.SuppressFinalize(Me)
        End Sub
    
        ''' <summary>
        ''' Implementation of the <b>IDisposable</b> interface.
        ''' </summary>
        ''' <param name="disposing">If <b>true</b>, the object to be disposed is finalized and collected by the garbage collector; otherwise, <b>false</b>.</param>
        ''' <remarks>This method calls Undo if impersonation is still being performed. This method calls the common language runtime version of the Dispose method.</remarks>
        Protected Overloads Sub Dispose(ByVal disposing As Boolean)
            If Not _disposed Then
                If disposing Then
                    If Not IsNothing(_impersonationContext) Then
                        _impersonationContext.Undo()
                        _impersonationContext.Dispose()
                    End If
                End If
                _impersonationContext = Nothing
            End If
            _disposed = True
        End Sub
    
    #End Region
    
        '2009.02.12 AMJ
        '   Modified From:
        '       How to implement impersonation in an ASP.NET application (KB306158)
        '       http://support.microsoft.com/kb/306158
        '   Implemented IDisposable based on ImpersonationHelper class of
        '       Namespace: Microsoft.Office.Excel.Server.Addins.ComputeCluster.Security
        '       Assembly: Microsoft.Office.Excel.Server.Addins.ComputeCluster (in microsoft.office.excel.server.addins.computecluster.dll)
    
        Const LOGON32_LOGON_INTERACTIVE As Integer = 2
        Const LOGON32_LOGON_BATCH As Integer = 4
        Const LOGON32_LOGON_SERVICE As Integer = 5
    
        Const LOGON32_PROVIDER_DEFAULT As Integer = 0
        Const LOGON32_PROVIDER_WINNT35 As Integer = 1
    
        Private Enum SECURITY_IMPERSONATION_LEVEL
            SecurityAnonymous = 0
            SecurityIdentification = 1
            SecurityImpersonation = 2
            SecurityDelegation = 3
        End Enum
    
        Private Declare Auto Function LogonUser Lib "advapi32.dll" ( _
            ByVal username As String, _
            ByVal domain As String, _
            ByVal password As IntPtr, _
            ByVal logonType As Integer, _
            ByVal logonProvider As Integer, _
            ByRef token As IntPtr) As Boolean
    
        Private Declare Auto Function DuplicateToken Lib "advapi32.dll" ( _
            ByVal ExistingTokenHandle As IntPtr, _
            ByVal ImpersonationLevel As SECURITY_IMPERSONATION_LEVEL, _
            ByRef DuplicateTokenHandle As IntPtr) As Integer
    
        Private Declare Auto Function RevertToSelf Lib "advapi32.dll" () As Long
        Private Declare Auto Function CloseHandle Lib "kernel32.dll" (ByVal handle As IntPtr) As Long
    
        Dim _impersonationContext As WindowsImpersonationContext
        Dim _domain As String
        Dim _login As String
        Dim _password As SecureString
    
    #Region " Standard Constructor & Properties "
    
        ''' <summary>
        ''' Initializes a new instance of the ImpersonationHelper class.
        ''' </summary>
        ''' <param name="domain">The domain or computer name of the user to impersonate.</param>
        ''' <param name="userName">The user name of the user to impersonate.</param>
        ''' <param name="password">The secure string password of UserName. For more information about secure strings, see the <see cref="System.Security.SecureString">SecureString</see> class.</param>
        <DebuggerNonUserCode()> _
        Public Sub New(ByVal domain As String, ByVal userName As String, ByVal password As SecureString)
            Me.Domain = domain
            Me.Login = userName
            Me.Password = password
        End Sub
    
        ''' <summary>
        ''' Do not allow a new instance of the ImpersonationHelper class without credentials.
        ''' </summary>
        Private Sub New()
    
        End Sub
    
        ''' <summary>
        ''' Gets or sets the domain of the user to impersonate.
        ''' </summary>
        ''' <value>The domain of the user.</value>
        <DebuggerNonUserCode()> _
        Public Property Domain() As String
            Get
                Return _domain
            End Get
            Set(ByVal value As String)
                _domain = value
            End Set
        End Property
    
        ''' <summary>
        ''' Gets or sets the user name of the user to impersonate.
        ''' </summary>
        ''' <value>The user name.</value>
        <DebuggerNonUserCode()> _
        Public Property Login() As String
            Get
                Return _login
            End Get
            Set(ByVal value As String)
                _login = value
            End Set
        End Property
    
        ''' <summary>
        ''' Sets the encrypted password of the user to impersonate. 
        ''' </summary>
        ''' <value>The encrypted password.</value>
        <DebuggerNonUserCode()> _
        Public WriteOnly Property Password() As SecureString
            Set(ByVal value As SecureString)
                _password = value
            End Set
        End Property
    
    #End Region
    
        ''' <summary>
        ''' Performs the impersonation of the user based on the parameters provided in the constructor. 
        ''' </summary>
        ''' <remarks>
        ''' <para>If logon fails using the supplied credentials, an exception is thrown. The exception is thrown because this method is unable to duplicate the logged-on user's token for purposes of impersonation or is unable to create a Windows identity from the user's impersonated token.</para>
        ''' <para>For details about the direct cause of the impersonation failure, you can inspect the inner exception.</para>
        ''' </remarks>
        <PermissionSetAttribute(SecurityAction.Demand, Name:="FullTrust")> _
        Public Sub ImpersonateUser()
    
            Dim fResult As Boolean = False  'assume impersonation failed
    
            Dim hPassword As IntPtr = IntPtr.Zero
            Dim hToken As IntPtr = IntPtr.Zero
            Dim hTokenDuplicate As IntPtr = IntPtr.Zero
            Dim oException As ImpersonationException = Nothing
    
            If RevertToSelf <> 0 Then
    
                hPassword = Marshal.SecureStringToGlobalAllocUnicode(_password)
    
                If LogonUser(Me.Login, Me.Domain, hPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, hToken) Then
                    If DuplicateToken(hToken, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, hTokenDuplicate) <> 0 Then
                        _impersonationContext = New WindowsIdentity(hTokenDuplicate).Impersonate()
                        If Not _impersonationContext Is Nothing Then
                            fResult = True
                        End If
                    End If
                Else
                    oException = New ImpersonationException(Me.Login, Me.Domain)
                End If
    
                If hPassword.Equals(IntPtr.Zero) = False Then
                    Marshal.ZeroFreeGlobalAllocUnicode(hPassword)
                End If
    
            End If
    
            If Not hTokenDuplicate.Equals(IntPtr.Zero) Then
                CloseHandle(hTokenDuplicate)
            End If
    
            If Not hToken.Equals(IntPtr.Zero) Then
                CloseHandle(hToken)
            End If
    
            If Not (oException Is Nothing) Then
                Throw oException
            End If
    
        End Sub
    
        ''' <summary>
        ''' Undoes the impersonation of the user, if it is impersonated.
        ''' </summary>
        ''' <remarks>Use this method to free the objects associated with impersonation.</remarks>
        <PermissionSetAttribute(SecurityAction.Demand, Name:="FullTrust")> _
        <DebuggerNonUserCode()> _
        Public Sub Undo()
            _impersonationContext.Undo()
            _impersonationContext = Nothing
        End Sub
    
        Public Shared Function InvokeAsUser(ByVal userName As String, ByVal domain As String, ByVal password As SecureString, ByVal methodToCall As [Delegate], ByVal ParamArray parameters() As Object) As Object
            Dim oResult As Object = Nothing
    
            Using oImpersonation As New ImpersonationHelper(domain, userName, password)
    
                oImpersonation.ImpersonateUser()
    
                oResult = methodToCall.DynamicInvoke(parameters)
    
            End Using
    
            Return oResult
        End Function
    
    End Class
    
    Public Class ImpersonationException
        Inherits System.Exception
    
        Public ReadOnly Login As String
        Public ReadOnly Domain As String
    
        Public Sub New(ByVal userName As String, ByVal domain As String)
            MyBase.New(String.Format("Impersonation failure: {1}\{0}", userName, domain), New System.ComponentModel.Win32Exception)
        End Sub
    
    End Class
    

    Imports Missico.Personal
    
    Imports System.Security
    
    Imports Microsoft.VisualStudio.TestTools.UnitTesting
    
    <TestClass()> _
    Public Class ImpersonationHelperTest
    
        Private testContextInstance As TestContext
    
        Public Property TestContext() As TestContext
            Get
                Return testContextInstance
            End Get
            Set(ByVal value As TestContext)
                testContextInstance = value
            End Set
        End Property
    
        <TestMethod()> _
        Public Sub ImpersonationHelperTest()
    
            'testing only, never initialize the characters of the password in this fashion
            'replace with valid password
    
            Dim oPassword As New System.Security.SecureString
    
            oPassword.AppendChar("o"c)
            oPassword.AppendChar("o"c)
            oPassword.AppendChar("p"c)
            oPassword.AppendChar("s"c)
            oPassword.AppendChar("!"c)
            oPassword.AppendChar(" "c)
            oPassword.AppendChar("n"c)
            oPassword.AppendChar("o"c)
            oPassword.AppendChar(" "c)
            oPassword.AppendChar("p"c)
            oPassword.AppendChar("a"c)
            oPassword.AppendChar("s"c)
            oPassword.AppendChar("s"c)
            oPassword.AppendChar("w"c)
            oPassword.AppendChar("o"c)
            oPassword.AppendChar("r"c)
            oPassword.AppendChar("d"c)
    
            Using oImpersonation As New ImpersonationHelper("ANTHONY", "amissico", oPassword)
    
                oImpersonation.ImpersonateUser()
    
                '...
    
            End Using
    
            Try
    
                Using oImpersonation As New ImpersonationHelper("INVALID", "amissico", oPassword)
    
                    oImpersonation.ImpersonateUser()
    
                    '...
    
                End Using
    
            Catch ex As ImpersonationException
                'expected
                '   due to invalid domain
            End Try
    
    
            Try
    
                Using oImpersonation As New ImpersonationHelper("ANTHONY", "INVALID", oPassword)
    
                    oImpersonation.ImpersonateUser()
    
                    '...
    
                End Using
    
            Catch ex As ImpersonationException
                'expected
                '   due to invalid user
    
            End Try
    
            Try
    
                oPassword.AppendChar(" "c) 'invalidate password
    
                Using oImpersonation As New ImpersonationHelper("ANTHONY", "amissico", oPassword)
    
                    oImpersonation.ImpersonateUser()
    
                    '...
    
                End Using
    
            Catch ex As ImpersonationException
                'expected
                '   due to invalid password
    
            End Try
    
    
        End Sub
    
    End Class
    

    Imports System.Security
    Imports System.Runtime.InteropServices
    Imports System.Runtime.CompilerServices
    
    Public Module SecureStringExtensions
    
        ''' <summary>
        ''' Determines whether the specified <see cref="System.Security.SecureString">System.Security.SecureString</see> instances are considered equal.
        ''' </summary>
        ''' <param name="valueA">The first <see cref="System.Security.SecureString">System.Security.SecureString</see> to compare.</param>
        ''' <param name="valueB">The second <see cref="System.Security.SecureString">System.Security.SecureString</see> to compare.</param>
        ''' <returns>True if valueA is equal to valueB; otherwise, False.</returns>
        <Extension()> _
        Public Function Equals(ByVal valueA As SecureString, ByVal valueB As SecureString) As Boolean
            Return IsEqual(valueA, valueB)
        End Function
    
        ''' <summary>
        ''' Determines whether the specified <see cref="System.Security.SecureString">System.Security.SecureString</see> instances are considered equal.
        ''' </summary>
        ''' <param name="valueA">The first <see cref="System.Security.SecureString">System.Security.SecureString</see> to compare.</param>
        ''' <param name="valueB">The second <see cref="System.Security.SecureString">System.Security.SecureString</see> to compare.</param>
        ''' <returns>True if valueA is equal to valueB; otherwise, False.</returns>
        ''' <remarks>Comparison loop based on Microsoft souce code for String.EqualsHelper method.</remarks>
        <Extension()> _
        Public Function IsEqual(ByVal valueA As SecureString, ByVal valueB As SecureString) As Boolean
            Dim fResult As Boolean = False  'assume failure
    
            'short-circuit if lengths are not the same
    
            If valueA.Length <> valueB.Length Then
                'cannot be the same value
                Return False
            End If
    
            Using oCopyA As SecureString = valueA.Copy, oCopyB As SecureString = valueB.Copy
    
                Dim iLength As Integer = oCopyA.Length
    
                Dim oPtrA As IntPtr = Marshal.SecureStringToBSTR(oCopyA)
                Dim oPtrB As IntPtr = Marshal.SecureStringToBSTR(oCopyB)
    
                Try
    
                    Do While (iLength > 0)
    
                        If Marshal.ReadByte(oPtrA, iLength) <> Marshal.ReadByte(oPtrB, iLength) Then
                            Exit Do
                        End If
    
                        iLength -= 1
    
                    Loop
    
                    fResult = (iLength <= 0)
    
                Finally
                    Marshal.ZeroFreeBSTR(oPtrA)
                    Marshal.ZeroFreeBSTR(oPtrA)
    
                End Try
    
            End Using
    
            Return fResult
        End Function
    
    End Module
    

    Imports System.Security
    Imports System.Diagnostics
    
    Imports Microsoft.VisualStudio.TestTools.UnitTesting
    
    Imports Missico.Security.SecureStringExtensions
    
    <TestClass()> _
    Public Class SecureStringExtensionsFixture
    
    #Region " TestContext "
    
        Private testContextInstance As TestContext
    
        Public Property TestContext() As TestContext
            Get
                Return testContextInstance
            End Get
            Set(ByVal value As TestContext)
                testContextInstance = value
            End Set
        End Property
    
    #End Region
    
        <TestMethod()> _
        Public Sub EqualsTest()
    
            Dim oValueA As New SecureString
            Dim oValueB As New SecureString
    
            oValueA.AppendChar("p"c)
            oValueA.AppendChar("a"c)
            oValueA.AppendChar("s"c)
            oValueA.AppendChar("s"c)
            oValueA.AppendChar("w"c)
            oValueA.AppendChar("o"c)
            oValueA.AppendChar("r"c)
            oValueA.AppendChar("d"c)
    
            oValueB.AppendChar("p"c)
            oValueB.AppendChar("a"c)
            oValueB.AppendChar("s"c)
            oValueB.AppendChar("s"c)
            oValueB.AppendChar("w"c)
            oValueB.AppendChar("o"c)
            oValueB.AppendChar("r"c)
            oValueB.AppendChar("d"c)
    
    
            'The Object.Equal method does not work because you cannot compare to secure strings.
    
            If oValueA.Equals(oValueB) Then
                'expected, but does not work
                'you cannot compare two secure strings
            Else
                'always fails
            End If
    
    
            'Using the fully-qualified path to the Equal extension method.
    
            If Missico.Security.SecureStringExtensions.Equals(oValueA, oValueB) Then
                'expected
            Else
                Assert.Fail("SecureString values are not equal, which is not expected.")
            End If
    
    
            'Using the IsEqual extension method that does not conflict with the Object.Equal method.
    
            If oValueA.IsEqual(oValueB) Then
                'expected
            Else
                Assert.Fail("SecureString values are not equal, which is not expected.")
            End If
    
    
            'change the second value
    
            oValueB.AppendChar(" "c)
    
            If oValueA.IsEqual(oValueB) Then
                Assert.Fail("SecureString values are equal, which is not expected.")
            Else
                'expected
            End If
    
        End Sub
    
    End Class