Encrypt DataSets for Offline Storage

Data stored on a client machine represents a security hole in all of your carefully considered plans. This article borrows from the IssueVision smart client sample application to demonstrate how to protect offline data. IssueVision is a sample help desk management application that generates some choice aspects of Windows Forms programming in .NET (click here for the IssueVision source code). Included in IssueVision are examples of smart client updating, encryption techniques, Web Services use, a clever use of the Observer behavior pattern, and some cool GDI+ code.

This article contains original code except for a derivative of the Data Protection API (DPAPI) wrapper code presented in IssueVision. The motivation for using the DPAPI is that IssueVision is an application that works in a disconnected mode. When the application is disconnected, IssueVision serializes, encrypts, and stores changes to the DataSet on the local client. This article focuses on reading, serializing, encrypting, storing, retrieving, and deserializing a dataset. (You can use the DPAPI wrapper to encrypt and decrypt data—whatever your motivation demands.)

Reading Data into a DataSet

First, you need some data to encrypt. You can use any data, but to demonstrate an example in context, pretend that you want to support a client application for the Northwind database and that this data needs to be stored when the client is disconnected from the server.

Reading data to and from a dataset has been covered thoroughly during the couple of years .NET has been available, so I will summarize this part of the process and follow it with example code. You can use components to manage data (as demonstrated in IssueVision) or simply write the code manually as summarized here and shown in Listing 1:

  1. Create a text file with a .udl extension.
  2. Double-click on the .udl file to open the Data Link Properties editor. Use the editor to create the connection string. Close the editor, open the .udl file with Notepad, and copy the connection string.
  3. Add an imports state for system.data and system.data.sqlclient. This tutorial uses the SQL Server version of the Northwind database, but you are welcome to use the MS Access version.
  4. Create a SqlConnection object and open the connection.
  5. Start a Try block, and create a DataSet and a SqlDataAdapter.
  6. Use a select * from customers query and the Fill method to populate the DataSet.
  7. In the Try block, return the DataSet.
  8. In the Finally block, close the connection.

Listing 1: Filling a DataSet.

Public Function GetCustomers() As DataSet
  Const connectionString As String = _
    "Integrated Security=SSPI;Persist Security Info=False;" + _
    "Initial Catalog=Northwind;Data Source=localhost;"
  Dim connection As SqlConnection = New SqlConnection(connectionString)
  connection.Open()
  Try
    Dim adapter As SqlDataAdapter = New _
      SqlDataAdapter("SELECT * FROM CUSTOMERS", connection)
    Dim data As DataSet = New DataSet
    adapter.Fill(data)
    Dump(data)
    Return data

  Finally
    connection.Close()
  End Try
End Function

Serializing the DataSet to XML

ADO.NET stores datasets internally as XML, and the ability to serialize a dataset to an XML string is an inherent capability of datasets. The DataSet class implements a WriteXML method that writes the serialized XML to a stream. To utilize a string for encryption purposes, you need to convert the contiguous block of bytes that represents a stream to a string using the UTF8.GetString shared method. The three lines of code in Listing 2 implement all this.

Listing 2: Serializing a DataSet to a string.

Public Shared Function Serialize(ByVal data As DataSet) As String
  Dim stream As MemoryStream = New MemoryStream
  data.WriteXml(stream)
  Return Text.UTF8Encoding.UTF8.GetString(stream.ToArray())
End Function

Encrypting a Serialized DataSet Using DPAPI

The copy of IssueVision I happen to have as I write this is in C#, so I ported the code in IssueVision’s DataProtection.cs file to Visual Basic .NET. All this code really does is wrap Data Protection API methods to make them easier to use in .NET, and manage dynamically allocated memory. You don’t have to create a wrapper for DPAPI, but it is a consumer-friendly thing to do.

You will use three API methods: two from the DPAPI and one for managing dynamically allocated, unmanaged memory. The methods are CryptProtectData, CryptUnprotectData, and LocalFree. To import them, you can use the VB style of API declaration or the .NET style, using the DllImportAttribute. Because you probably know the VB style of declaration—which is similar between VB6 and VB.NET—I will demonstrate how to import API methods using the DllImportAttribute. The rest of the code converts between .NET data types and API data types.

DPAPI encryption is based on Triple-DES, which uses a strong key. DPAPI is designed for user protection and relies on user credentials implicitly for its key. For added protection, the DPAPI permits you to provide an additional entropy value, which is designed to make it more difficult for a second application running under the same user logon to read data encrypted by another application. The code in Listing 3 demonstrates the DPAPI wrapper code. (If you want to learn about DPAPI from a technical aspect, check out the MSDN help from VS.NET.)

Listing 3: A VB.NET implementation of a DPAPI wrapper, based on the DPAPI wrapper in IssueVision

Imports System.Text
Imports System.Runtime.InteropServices

Public Module DataProtection

    Public Enum Store
        Machine
        User
    End Enum

    Private Class Consts
        Public Shared ReadOnly entropyData As Byte() = _
            ASCIIEncoding.ASCII.GetBytes("1295C82E-6D6E-4a01-96DD-
                                          1BF76B7F4CB4")
    End Class

    Private Class Win32
        Public Const CRYPTPROTECT_UI_FORBIDDEN As Integer  = &H1
        Public Const CRYPTPROTECT_LOCAL_MACHINE As Integer = &H4

        <StructLayout(LayoutKind.Sequential)> _
        Public Structure DATA_BLOB
            Public cbData As Integer
            Public pbData As IntPtr
        End Structure

        <DllImport("crypt32", CharSet:=CharSet.Auto)> _
        Public Shared Function CryptProtectData(ByRef _
            pDataIn As DATA_BLOB, _
            ByVal szDataDescr As String, _
            ByRef pOptionalEntropy As DATA_BLOB, _
            ByVal pvReserved As IntPtr, _
            ByVal pPromptStruct As IntPtr, _
            ByVal dwFlags As Integer, _
            ByRef pDataOut As DATA_BLOB) As Boolean
        End Function


        <DllImport("crypt32", CharSet:=CharSet.Auto)> _
         Public Shared Function CryptUnprotectData(ByRef _
           pDataIn As DATA_BLOB, _
           ByVal szDataDescr As StringBuilder, _
           ByRef pOptionalEntropy As DATA_BLOB, _
           ByVal pvReserved As IntPtr, _
           ByVal pPromptStruct As IntPtr, _
           ByVal dwFlags As Integer, _
           ByRef pDataOut As DATA_BLOB) As Boolean
        End Function

        <DllImport("kernel32")> _
        Public Shared Function LocalFree(ByVal hMem As IntPtr) As IntPtr
        End Function
    End Class

    Public Function Encrypt(ByVal data As String, _
        ByVal store As Store) As String
        Dim inBlob As Win32.DATA_BLOB      = New Win32.DATA_BLOB
        Dim entropyBlob As Win32.DATA_BLOB = New Win32.DATA_BLOB
        Dim outBlob As Win32.DATA_BLOB     = New Win32.DATA_BLOB

        Dim result As String = ""

        Try
            Dim flags As Integer = Win32.CRYPTPROTECT_UI_FORBIDDEN
            If (store = store.Machine) Then
                flags = flags Or Win32.CRYPTPROTECT_LOCAL_MACHINE
            End If

            SetBlobData(inBlob, UTF8Encoding.UTF8.GetBytes(data))
            SetBlobData(entropyBlob, Consts.entropyData)

            If (Win32.CryptProtectData(inBlob, "", _
                entropyBlob, IntPtr.Zero, _
                IntPtr.Zero, flags, outBlob)) Then

                Dim resultBits() As Byte = GetBlobData(outBlob)
                If (resultBits.Length <> 0) Then
                    result = Convert.ToBase64String(resultBits)
                End If
            End If


        Catch ex As Exception
            Return String.Empty
        Finally
            If (inBlob.pbData.ToInt32() <> 0) Then
                Marshal.FreeHGlobal(inBlob.pbData)
            End If

            If (entropyBlob.pbData.ToInt32() <> 0) Then
                Marshal.FreeHGlobal(entropyBlob.pbData)
            End If
        End Try

        Return result
    End Function

    Public Function Decrypt(ByVal data As String, _
                            ByVal store As Store) As String

        Dim result As String = ""

        Dim inBlob As Win32.DATA_BLOB      = New Win32.DATA_BLOB
        Dim entropyBlob As Win32.DATA_BLOB = New Win32.DATA_BLOB
        Dim outBlob As Win32.DATA_BLOB     = New Win32.DATA_BLOB

        Try
            Dim flags As Integer = Win32.CRYPTPROTECT_UI_FORBIDDEN
            If (store = store.Machine) Then
                flags = flags Or Win32.CRYPTPROTECT_LOCAL_MACHINE
            End If

            Dim bits() As Byte = Convert.FromBase64String(data)

            SetBlobData(inBlob, bits)
            SetBlobData(entropyBlob, Consts.entropyData)

            If (Win32.CryptUnprotectData(inBlob, Nothing, entropyBlob, _
                IntPtr.Zero, IntPtr.Zero, flags, outBlob)) Then

                Dim resultBits() As Byte = GetBlobData(outBlob)
                If (resultBits.Length <> 0) Then
                    result = UTF8Encoding.UTF8.GetString(resultBits)
                End If
            End If
        Catch ex As Exception
            Return String.Empty
        Finally
            If (inBlob.pbData.ToInt32() <> 0) Then
                Marshal.FreeHGlobal(inBlob.pbData)
            End If

            If (entropyBlob.pbData.ToInt32() <> 0) Then
                Marshal.FreeHGlobal(entropyBlob.pbData)
            End If

        End Try

        Return result
    End Function

    Private Sub SetBlobData(ByRef blob As Win32.DATA_BLOB, _
                            ByVal bits() As Byte)
        blob.cbData = bits.Length
        blob.pbData = Marshal.AllocHGlobal(bits.Length)
        Marshal.Copy(bits, 0, blob.pbData, bits.Length)
    End Sub

    Private Function GetBlobData(ByRef blob As Win32.DATA_BLOB) As Byte()
        If (blob.pbData.ToInt32() = 0) Then Return Nothing
        Dim data(blob.cbData) As Byte
        Marshal.Copy(blob.pbData, data, 0, blob.cbData)
        Win32.LocalFree(blob.pbData)
        Return data
    End Function
End Module

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read