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