Encrypting data per-user in your .NET application without asking for a secret

When you encrypt something, you need some kind of secret to do so – a password that’s used to encrypt and decrypt your message, or a public/private key like a certificate. In order to get a secret, you usually have to ask the user for one – in most cases, a password – before they can decrypt their data and access it.

I had an application where I wanted to encrypt some database connection details between sessions. This is easy enough, but I wanted to do it without having to ask the user for a password – I just wanted it done transparently. I thought this meant storing a secret key somewhere, but there’s no safe place to keep a key -even if it’s embedded in your code, somebody who really wants it will always be able to get it. What I needed was a private place to store a key, where no other Windows user on the same computer would be able to access it.

That’s when I discovered the DPAPI – it’s been around since .NET 2.0, and it’s available through the System.Security.Cryptography namespace. What’s great about this cryptographic feature is that you can have Windows encrypt something with the same key it uses to encrypt files with EFS – a private user key that’s based on a hash of their Windows password. This means that you don’t need to ask the user for a secret – you’ve already got one handy, and Windows will prevent other users of the system from being able to decrypt your data. The downside of this is that if the user resets their password, you’re toast – if you’re only keep the data as a convenience (as I am), that’s no problem, but if you can’t afford to lose the data, you’ll need to ask the user for the secret instead of relying on this method.

This MSDN article that detailed its use, and after adding some code that made it easy to encrypt and decrypt strings, here’s what I ended up with:

Imports System
Imports System.Security.Cryptography

''' <summary>
''' Encrypts and Decrypts information using the current Windows user key
''' </summary>
''' <remarks></remarks>
Public Class DPAPI

    Private Shared EntropyString As String = "Some value I made up"

    ' Create byte array for additional entropy when using Protect/Unprotect method.
    Private Shared Function AdditionalEntropy() As Byte()
        Dim encoder As New System.Text.ASCIIEncoding
        Return encoder.GetBytes(EntropyString)
    End Function

#Region " Encrypt "

    Public Shared Function Protect(ByVal data() As Byte) As Byte()
        Try
            ' Encrypt the data using DataProtectionScope.CurrentUser. The result can be decrypted
            ' only by the same current user.
            Return ProtectedData.Protect(data, AdditionalEntropy, DataProtectionScope.CurrentUser)
        Catch e As CryptographicException
            Console.WriteLine("Data was not encrypted. An error occurred.")
            Console.WriteLine(e.Message.ToString())
            Throw
        End Try
    End Function

    Public Shared Function ProtectString(ByVal data As String) As String

        Dim encoder As New System.Text.ASCIIEncoding
        Return Convert.ToBase64String(Protect(encoder.GetBytes(data)))

    End Function

#End Region

#Region " Decrypt "

    Public Shared Function Unprotect(ByVal data() As Byte) As Byte()
        Try
            'Decrypt the data using DataProtectionScope.CurrentUser.
            Return ProtectedData.Unprotect(data, AdditionalEntropy, DataProtectionScope.CurrentUser)
        Catch e As CryptographicException
            Console.WriteLine("Data was not decrypted. An error occurred.")
            Console.WriteLine(e.Message.ToString())
            Throw
        End Try

    End Function

    Public Shared Function UnprotectString(ByVal data As String) As String

        Dim encoder As New System.Text.ASCIIEncoding
        Dim b() As Byte = Convert.FromBase64String(data)
        Return encoder.GetString(Unprotect(b))

    End Function

#End Region

End Class

Download the code here

To use the class, just change the entropy value at the top (this is combined with the user’s key to create a new key used to encrypt your data), call Protect() or ProtectString() and pass it the required data, and you’re good to go! I added the ProtectString and UnprotectString because I was storing the values in the user’s app.config file, and needed an easy string representation.

If you use the class or have any questions, please let me know!

Accessing a clustered SQL Server instance without the instance name

When I clustered SQL Server 2005 the first time, it bothered me that I had to access each clustered instance using both the cluster DNS name and the instance name. If my SQL Cluster is called SQL-CLUSTER and the DNS alias of my first instance is SQL-INSTANCE1, I had to connect to SQL-INSTANCE1INSTANCE1. Since each instance has a dedicated name and IP address that uniquely identifies it, shouldn’t that be enough? Why is the instance name required?

It turns out it’s not! With a little tweaking, you can access the instance using just the SQL DNS name, without the instance name, and it works for as many SQL instances as you have on a cluster, not just one (you’ll need to repeat this process for each instance). Here’s how:

  1. On the active node for the instance you’re adjusting, open the “SQL Server Configuration Manager” from the start menu.
  2. Expand “SQL Server Network Configuration” and select the instance you want to edit.
  3. In the right window, right-click “TCP/IP” and select “Properties.”
  4. On the “Protocol” tab, ensure that “Listen All” is set to “Yes”
  5. On the “IP Addresses” tab, scroll all the way to the bottom, to the “IPAll” section.
  6. The “TCP port” box is blank by default – set it to 1433, the default port for SQL Server.
  7. Click “OK” – you’ll receive a message that you need to restart the SQL Service.
  8. Restart the SQL Service on that node.

Presto – done! You can now access this SQL Server instance using either “SQL-INSTANCE1\INSTANCE1” or just “SQL-INSTANCE”. It’s also worth noting that this network settings follows the instance between active nodes – if you failover the SQL instance to the other node, it should still be addressable the same way, and if you check the protocol settings, you’ll see that your change is active on that server now as well.

To me, it’s so much easier to not have to use the instance name when addressing the server, and since the service is responding on port 1433, the default port, this change should be compatible with every application that connects, and I haven’t had any problems at all.

If you try this and run into any problems, please leave a comment below.

UPDATE:

After playing with this a little more, it seems like it only works with the SQL Native Client. If you attempt to connect via the SQL Server OLEDB driver or the old SQL 2000 driver, you get the error that you’re not able to connect to the specified instance. Switching your connection method to the SQL Native Client (SQLNCLI) allows you to connect, assuming your application allows that. Has anybody else experienced this problem?

Scheduled Task “Could not start” when installing SQL Server on a Windows Cluster

I ran into this error while deploying SQL Server 2005 Enterprise to a two-node Windows Server 2003 cluster. The SQL Server installation checked all the prereqs with no problems, and as soon as it was time to actually do the installation, it paused for about 5 minutes, with the message “SQL Server Setup is preparing to make the requested configuration changes…” After a few minutes with no activity, the installation fails with the following error message:

I checked the “Scheduled Tasks” list on the passive node, and found the following:

Starting the task manually, even while setup was still running on the active node, had no effect. Also, the following entry appeared in the Log (Advanced Menu -> View Log):

“SQL Server Remote Setup .job” (setup.exe) 5/1/2009 11:29:45 AM ** ERROR **

Unable to start task.
The specific error is:
0x80070005: Access is denied.
Try using the Task page Browse button to locate the application.


SOLUTION:

It turns out it’s not an “Access denied” message at all! This occurs when you’re logged in to the desktop of the passive node while you’re doing the SQL installation. I had a remote desktop session open to both nodes, which caused this problem. Simply logging out of the passive node, then attempting the installation again from the active node, will allow setup to complete successfully.

Please, Microsoft: Add a pre-check to the setup process, or at the very least, give me a real error!

Resolving “STOP 0x0000007B INACCESSIBLE_BOOT_DEVICE” after you make changes to your hard drive configuration

I’d heard that, unlike previous versions of Windows, a Vista installation could be moved to a new computer without any problems, and would just re-detect all the hardware and reconfigure itself. So when I got my new computer, I imaged my boot partition to the new hard drive (it was much bigger than my old one), attempted to boot Vista, and got this error:

STOP: 0x0000007B (0x818B51B0, 0xC00000010, 0x00000000,0x00000000) INACCESSIBLE_BOOT_DEVICE

WTF? After much digging, it turns out that my new motherboard had AHCI enabled by default, whereas my old one didn’t. This is a SATA standard that supports Native Command Queuing (NCQ), special power management, and some other features. While Vista supports it out of the box, if no AHCI drives are connected when Vista is installed, it disables the drivers, and any attempt to switch to AHCI mode later will give you a blue screen at boot time. The general consensus online is that you have to reinstall Windows, but you do not!

To resolve the error:

    1. In the system BIOS, switch AHCI mode off. This will probably mean something like “Compatibility mode” for the drive – look for a setting that sounds like it does this, either for the controller or the drive itself. This will allow you to boot into Windows again
      1. a.  If you still can’t boot into Windows, you may need to rebuild the boot sector – not as scary as it sounds! Boot the the Vista install DVD, and when prompted, select “Repair installation”. After thinking for about 15 seconds, the install should say that it’s found the problem and corrected it, and you can reboot – Vista should come right up after that.
    2. Once you’re in Windows, load REGEDIT and navigate to [HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Msahci]
    3. Set the value “Start” to “0”. This will tell Windows to search for AHCI drives when it boots.

Reboot, and switch your hard drive back into AHCI mode when you do. Windows will boot, detect it, and install drivers. It will probably ask you to reboot again

  1. Resolved! Windows boots on your old Vista installation. Aside from this minor hiccup, Vista did indeed cooperate with my new hardware – it detected everything and came right up. It asked me to reactivate, but since it’s a corporate copy, that was no big deal.

Logon Failure: The user has not been granted the requested logon type at this computer

The printer I use to connected to a domain computer, but the computer I print from is not on the domain, so I ran into this ugly error when I tried to map the printer:

Logon Failure: The user has not been granted the requested logon type at this computer

This occurs because the workgroup computer is trying to pre-connect to the domain computer, and since it’s un-authenticated, it connects as “Guest”. Under normal circumstances, the guest account is disabled or, at least, it has been denied rights to connect over the network. Here’s how you remove this restriction:

  1. Download and install the Windows Resource Kit Tools (http://download.microsoft.com/download/8/e/c/8ec3a7d8-05b4-440a-a71e-ca3ee25fe057/rktools.exe). On Vista, you may receive an error that these tools are not compatible, buy you can ignore it – at least in this case, it doesn’t cause any problems, and currently. There’s no new version of the tools available.
  2. From the start menu, click “All Programs” -> “Windows Resource Kit Tools” -> “Command Shell” Run the following commands, in this order (and they’re case sensitive):
    1. net user guest /active:yes
    2. ntrights +r SeNetworkLogonRight -u Guest
    3. ntrights -r SeDenyNetworkLogonRight -u Guest

That’s it! You’ve enabled guest access, so non-domain computers will at least have the ability to connect, though they’ll be restricted to the permissions granted to the local “Guest” account, so you’re not opening up much of a security risk.