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:
- Create a text file with a .udl extension.
- 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.
- 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.
- Create a SqlConnection object and open the connection.
- Start a Try block, and create a DataSet and a SqlDataAdapter.
- Use a select * from customers query and the Fill method to populate the DataSet.
- In the Try block, return the DataSet.
- 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
