P/Invoke CryptUnprotectData breaks SqlConnection constructor

449 views Asked by At

I'm attempting to use CryptUnprotectData to read a password protected using CryptProtectData into a SecureString and use that to connect to a database. I can get the correct password out, but trying to create a new SqlConnection after that fails with the following:

System.TypeInitializationException was unhandled
  HResult=-2146233036
  Message=The type initializer for 'System.Data.SqlClient.SqlConnection' threw an exception.
  Source=System.Data
  TypeName=System.Data.SqlClient.SqlConnection
  StackTrace:
       at System.Data.SqlClient.SqlConnection..ctor()
       at System.Data.SqlClient.SqlConnection..ctor(String connectionString, SqlCredential credential)
       at System.Data.SqlClient.SqlConnection..ctor(String connectionString)
       at ProtectedSqlTest.Program.Main() in C:\Git\ProtectedSqlTest\ProtectedSqlTest\Program.cs:line 16
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: 
       HResult=-2146233036
       Message=The type initializer for 'System.Data.SqlClient.SqlConnectionFactory' threw an exception.
       Source=System.Data
       TypeName=System.Data.SqlClient.SqlConnectionFactory
       StackTrace:
            at System.Data.SqlClient.SqlConnection..cctor()
       InnerException: 
            HResult=-2146233036
            Message=The type initializer for 'System.Data.SqlClient.SqlPerformanceCounters' threw an exception.
            Source=System.Data
            TypeName=System.Data.SqlClient.SqlPerformanceCounters
            StackTrace:
                 at System.Data.SqlClient.SqlConnectionFactory..cctor()
            InnerException: 
                 HResult=-2147024809
                 Message=The parameter is incorrect. (Exception from HRESULT: 0x80070057 (E_INVALIDARG))
                 Source=mscorlib
                 StackTrace:
                      at System.Globalization.TextInfo.InternalChangeCaseString(IntPtr handle, IntPtr handleOrigin, String localeName, String str, Boolean isToUpper)
                      at System.Globalization.TextInfo.ToLower(String str)
                      at System.String.ToLower(CultureInfo culture)
                      at System.Diagnostics.PerformanceCounterLib.GetPerformanceCounterLib(String machineName, CultureInfo culture)
                      at System.Diagnostics.PerformanceCounterLib.IsCustomCategory(String machine, String category)
                      at System.Diagnostics.PerformanceCounter.InitializeImpl()
                      at System.Diagnostics.PerformanceCounter.set_RawValue(Int64 value)
                      at System.Data.ProviderBase.DbConnectionPoolCounters.Counter..ctor(String categoryName, String instanceName, String counterName, PerformanceCounterType counterType)
                      at System.Data.ProviderBase.DbConnectionPoolCounters..ctor(String categoryName, String categoryHelp)
                      at System.Data.SqlClient.SqlPerformanceCounters..ctor()
                      at System.Data.SqlClient.SqlPerformanceCounters..cctor()
                 InnerException: 

It's enough to simply call CryptUnprotectData for the SqlConnection to fail, the connection itself doesn't need to use the returned SecureString.

I'm using the extension methods from here as described in this post for my minimal repro:

class Program
{
    const string ProtectedSecret = /* SNIP - base 64 encoded protected data here */;
    static void Main()
    {
        // calling AppendProtectedData breaks the following SqlConnection
        // without the following line the application works fine
        new SecureString().AppendProtectedData(Convert.FromBase64String(ProtectedSecret));

        using (var conn = new SqlConnection("Server=(localdb)\\MSSqlLocalDb;Trusted_Connection=true"))
        using (var cmd = new SqlCommand("select 1", conn))
        {
            conn.Open();
            cmd.ExecuteNonQuery();
        }
    }
}

If i create a new SqlConnection before I load the password, I can create new SqlConnections fine for the duration of the application as it seems to use the same SqlConnectionFactory, but that means as a workaround I have to do something like this at the start of the application:

new SqlConnection().Dispose();

... which I'd like to avoid.

The following do not help:

  • Debug vs Release build
  • Debugging in Visual Studio vs running through the command line
  • Changing the CryptProtectFlags that is passed to CryptUnprotectData.
  • Removing RuntimeHelpers.PrepareConstrainedRegions() from the protection method.

Windows 10, VS Enterprise 2015, Console Application (.NET 4.6.1)

UPDATE: Running the data protection code in another threads gives a similar exception with a different root cause:

System.TypeInitializationException was unhandled
  HResult=-2146233036
  Message=The type initializer for 'System.Data.SqlClient.SqlConnection' threw an exception.
  Source=System.Data
  TypeName=System.Data.SqlClient.SqlConnection
  StackTrace:
       at System.Data.SqlClient.SqlConnection..ctor()
       at System.Data.SqlClient.SqlConnection..ctor(String connectionString, SqlCredential credential)
       at System.Data.SqlClient.SqlConnection..ctor(String connectionString)
       at ProtectedSqlTest.Program.Main() in C:\Git\ProtectedSqlTest\ProtectedSqlTest\Program.cs:line 17
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: 
       HResult=-2146233036
       Message=The type initializer for 'System.Data.SqlClient.SqlConnectionFactory' threw an exception.
       Source=System.Data
       TypeName=System.Data.SqlClient.SqlConnectionFactory
       StackTrace:
            at System.Data.SqlClient.SqlConnection..cctor()
       InnerException: 
            HResult=-2146233036
            Message=The type initializer for 'System.Data.SqlClient.SqlPerformanceCounters' threw an exception.
            Source=System.Data
            TypeName=System.Data.SqlClient.SqlPerformanceCounters
            StackTrace:
                 at System.Data.SqlClient.SqlConnectionFactory..cctor()
            InnerException: 
                 BareMessage=Configuration system failed to initialize
                 HResult=-2146232062
                 Line=0
                 Message=Configuration system failed to initialize
                 Source=System.Configuration
                 StackTrace:
                      at System.Configuration.ClientConfigurationSystem.EnsureInit(String configKey)
                      at System.Configuration.ClientConfigurationSystem.PrepareClientConfigSystem(String sectionName)
                      at System.Configuration.ClientConfigurationSystem.System.Configuration.Internal.IInternalConfigSystem.GetSection(String sectionName)
                      at System.Configuration.ConfigurationManager.GetSection(String sectionName)
                      at System.Configuration.PrivilegedConfigurationManager.GetSection(String sectionName)
                      at System.Diagnostics.DiagnosticsConfiguration.Initialize()
                      at System.Diagnostics.DiagnosticsConfiguration.get_SwitchSettings()
                      at System.Diagnostics.Switch.InitializeConfigSettings()
                      at System.Diagnostics.Switch.InitializeWithStatus()
                      at System.Diagnostics.Switch.get_SwitchSetting()
                      at System.Data.ProviderBase.DbConnectionPoolCounters..ctor(String categoryName, String categoryHelp)
                      at System.Data.SqlClient.SqlPerformanceCounters..ctor()
                      at System.Data.SqlClient.SqlPerformanceCounters..cctor()
                 InnerException: 
                      HResult=-2147024809
                      Message=Item has already been added. Key in dictionary: 'MACHINE'  Key being added: 'MACHINE'
                      Source=mscorlib
                      StackTrace:
                           at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)
                           at System.Collections.Hashtable.Add(Object key, Object value)
                           at System.Configuration.Internal.InternalConfigRoot.GetConfigRecord(String configPath)
                           at System.Configuration.ClientConfigurationSystem.EnsureInit(String configKey)
                      InnerException:
2

There are 2 answers

1
Steve-OH On BEST ANSWER

I was recently experiencing similar symptoms, using the same code from http://www.griffinscs.com/?p=12: any call to CryptUnprotectData would lead to an exception in some unrelated code. Interestingly, the failure only occurred on a Windows 10 machine; the same code worked fine on a Windows 7 machine.

I fixed the problem by changing the declarations of the szDataDescr parameters in both CryptProtectData and CryptUnprotectData from string to IntPtr, and passing IntPtr.Zero instead of string.Empty in the two calls.

9
Yusuf Tarık Günaydın On

Interestingly the faulting code is:

internal static PerformanceCounterLib GetPerformanceCounterLib(string machineName, CultureInfo culture) {
    SharedUtils.CheckEnvironment();

    string lcidString = culture.LCID.ToString("X3", CultureInfo.InvariantCulture);
    if (machineName.CompareTo(".") == 0)
            machineName = ComputerName.ToLower(CultureInfo.InvariantCulture);
    else
        machineName = machineName.ToLower(CultureInfo.InvariantCulture);
    ...

the line that calls ComputerName.ToLower(CultureInfo.InvariantCulture) causes the exception.

You can reproduce the same behavior just calling code

new SecureString().AppendProtectedData(Convert.FromBase64String(ProtectedSecret));
string lower = "Something".ToLower(CultureInfo.InvariantCulture);

Somehow in the constructor of the TextInfo class

this.m_dataHandle = CompareInfo.InternalInitSortHandle(m_textInfoName, out handleOrigin);

returns invalid data if this is not called before CryptUnprotectData function.

This seems like a bug in the framework. You can submit it to Microsoft. In the meantime you can call this line beforehand to prevent the error.

"".ToLower(CultureInfo.InvariantCulture);