Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

P/Invoke CryptUnprotectData breaks SqlConnection constructor

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:
like image 459
Jussi Kosunen Avatar asked Dec 20 '16 11:12

Jussi Kosunen


People also ask

How does the cryptunprotectdata function work?

The CryptUnprotectData function decrypts and does an integrity check of the data in a DATA_BLOB structure. Usually, only a user with the same logon credentials as the encrypter can decrypt the data. In addition, the encryption and decryption must be done on the same computer.

What is the cryptprotect_promptstruct structure?

The CRYPTPROTECT_PROMPTSTRUCT structure provides the text of a prompt and information about when and where that prompt is to be displayed when using the CryptProtectData and CryptUnprotectData functions. The CryptProtectData function performs encryption on the data in a DATA_BLOB structure.

How do I use cryptprotectdata to decrypt data?

The CryptProtectData function creates a session key when the data is encrypted. That key is derived again and used to decrypt the data BLOB. The Message Authentication Code (MAC) hash added to the encrypted data can be used to determine whether the encrypted data was altered in any way.

What is P/Invoke in C++?

P/Invoke is a technology that allows you to access structs, callbacks, and functions in unmanaged libraries from your managed code. Most of the P/Invoke API is contained in two namespaces: System and System.Runtime.InteropServices.


2 Answers

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); 
like image 56
Yusuf Tarık Günaydın Avatar answered Oct 28 '22 11:10

Yusuf Tarık Günaydın


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.

like image 30
Steve Schafer Avatar answered Oct 28 '22 13:10

Steve Schafer