Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]

.pdf
Скачиваний:
104
Добавлен:
16.08.2013
Размер:
29.8 Mб
Скачать

858 C H A P T E R 2 5 C RY P TO G R A P H Y

Encrypting Sensitive Data in a Database

In this section, you will learn how to create a simple test page for encrypting information stored in a database table. This table will be connected to a user registered in the Membership Service. We suggest not creating a custom Membership provider with custom implementations of MembershipUser that support additional properties. As long as you stay loosely coupled with your own logic, you can use it with multiple Membership providers. In this sample, you will create a database table that stores additional information for a MembershipUser without creating a custom provider. It just connects to the MembershipUser through the ProviderUserKey—this means the actual primary key of the underlying data store. Therefore, you have to create a table on your SQL Server as follows:

CREATE DATABASE ExtendedUser GO

USE ExtendedUser GO

CREATE TABLE ShopInfo

(

UserId UNIQUEIDENTIFIER PRIMARY KEY,

CreditCard VARBINARY(60),

Street VARCHAR(80),

ZipCode VARCHAR(6), City VARCHAR(60)

)

The primary key, UserId, will contain the same key as the MembershipUser for which this information is created. That’s the only connection to the underlying Membership Service. As mentioned, the advantage of not creating a custom provider for just these additional fields is that you can use it for other providers. We suggest creating custom providers only for supporting additional types of data stores for the Membership Service. The sensitive information is the CreditCard field, which now is not stored as VARCHAR but as VARBINARY instead. Now you can create a page that looks like this:

<form id="form1" runat="server"> <div>

<asp:LoginView runat="server" ID="MainLoginView"> <AnonymousTemplate>

<asp:Login ID="MainLogin" runat="server" /> </AnonymousTemplate>

<LoggedInTemplate>

Credit Card: <asp:TextBox ID="CreditCardText" runat="server" /><br /> Street: <asp:TextBox ID="StreetText" runat="server" /><br />

Zip Code: <asp:TextBox ID="ZipCodeText" runat="server" /><br /> City: <asp:TextBox ID="CityText" runat="server" /><br /> <asp:Button runat="server" ID="LoadCommand" Text="Load"

OnClick="LoadCommand_Click" />  <asp:Button runat="server" ID="SaveCommand" Text="Save"

OnClick="SaveCommand_Click" /> </LoggedInTemplate>

</asp:LoginView>

</div>

</form>

The page includes a LoginView control to display the Login control for anonymous users and display some text fields for the information introduced with the CREATE TABLE statement. Within the Load button’s Click event handler, you will write code for retrieving and decrypting information

C H A P T E R 2 5 C RY P TO G R A P H Y

859

from the database, and within the Save button’s Click event handler, you will obviously do the opposite. Before doing that, though, don’t forget to configure the connection string appropriately.

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <connectionStrings>

<add name="DemoSql"

connectionString="data source=(local); Integrated Security=SSPI; initial catalog=ExtendedUser"/>

</connectionStrings>

<system.web>

<authentication mode="Forms" /> </system.web>

</configuration>

Now you should use the ASP.NET WAT to create a couple of users in your membership store. After you have done that, you can start writing the actual code for reading and writing data to the database. The code doesn’t include anything special. It just uses the previously created encryption utility class for encrypting the data before updating the database and decrypting the data stored on the database.

Let’s take a look at the update method first:

protected void SaveCommand_Click(object sender, EventArgs e)

{

DemoDb.Open();

try

{

string SqlText = "UPDATE ShopInfo " +

"SET Street=@street, ZipCode=@zip, " + "City=@city, CreditCard=@card " +

"WHERE UserId=@key";

SqlCommand Cmd = new SqlCommand(SqlText, DemoDb);

//Add simple values Cmd.Parameters.AddWithValue("@street", StreetText.Text); Cmd.Parameters.AddWithValue("@zip", ZipCodeText.Text); Cmd.Parameters.AddWithValue("@city", CityText.Text); Cmd.Parameters.AddWithValue("@key",

Membership.GetUser().ProviderUserKey);

//Now add the encrypted value

byte[] EncryptedData = SymmetricEncryptionUtility.EncryptData(

CreditCardText.Text, EncryptionKeyFile); Cmd.Parameters.AddWithValue("@card", EncryptedData);

// Execute the command

int results = Cmd.ExecuteNonQuery(); if (results == 0)

{

Cmd.CommandText = "INSERT INTO ShopInfo VALUES" + "(@key, @card, @street, @zip, @city)";

Cmd.ExecuteNonQuery();

}

860 C H A P T E R 2 5 C RY P TO G R A P H Y

}

finally

{

DemoDb.Close();

}

}

The two key parts of the previous code are the part that retrieves the ProviderUserKey from the currently logged-on MembershipUser for connecting the information to a membership user and the position where the credit card information is encrypted through the previously created encryption utility class. Only the encrypted byte array is passed as a parameter to the SQL command. Therefore, the data is stored encrypted in the database.

The opposite of this function, reading data, looks quite similar, as shown here:

protected void LoadCommand_Click(object sender, EventArgs e)

{

DemoDb.Open();

try

{

string SqlText = "SELECT * FROM ShopInfo WHERE UserId=@key"; SqlCommand Cmd = new SqlCommand(SqlText, DemoDb); Cmd.Parameters.AddWithValue("@key",

Membership.GetUser().ProviderUserKey); using (SqlDataReader Reader = Cmd.ExecuteReader())

{

if (Reader.Read())

{

// Cleartext Data

StreetText.Text = Reader["City"].ToString(); ZipCodeText.Text = Reader["ZipCode"].ToString(); CityText.Text = Reader["City"].ToString();

// Encrypted Data

byte[] SecretCard = (byte[])Reader["CreditCard"]; CreditCardText.Text =

SymmetricEncryptionUtility.DecryptData( SecretCard, EncryptionKeyFile);

}

}

}

finally

{

DemoDb.Close();

}

}

Again, the function uses the currently logged-on MembershipUser’s ProviderUserKey property for retrieving the information. If successfully retrieved, it reads the clear-text data and then retrieves the encrypted bytes from the database table. These bytes are then decrypted and displayed in the credit card text box. You can see the results in Figure 25-8.

C H A P T E R 2 5 C RY P TO G R A P H Y

861

Figure 25-8. Encrypting sensitive information on the database

Encrypting the Query String

In this book, you’ve seen several examples in which ASP.NET security works behind the scenes to protect your data. For example, in Chapter 20 you learned how ASP.NET uses encryption and hash codes to ensure that the data in the form cookie is always protected. You have also learned how you can use the same tools to protect view state. Unfortunately, ASP.NET doesn’t provide a similar way to enable automatic encryption for the query string (which is the extra bit of information you add to URLs to transmit information from one page to another). In many cases, the URL query information corresponds to user-supplied data, and it doesn’t matter whether the user can see or modify it. In other cases, however, the query string contains information that should remain hidden from the user. In this case, the only option is to switch to another form of state management (which may have other limitations) or devise a system to encrypt the query string.

In the next example, you’ll see a simple way to tighten security by scrambling data before you place it in the query string. Once again, you can rely on the cryptography classes provided with

.NET. In fact, you can leverage the DPAPI. (Of course, you can do this only if you are not in a server farm environment. In that case, you could use the previously created encryption classes and deploy the same key file to any machine in the server farm.)

862 C H A P T E R 2 5 C RY P TO G R A P H Y

Wrapping the Query String

The starting point is to build an EncryptedQueryString class. This class should accept a collection of string-based information (just like the query string) and allow you to retrieve it in another page. Behind the scenes, the EncryptedQueryString class needs to encrypt the data before it’s placed in the query string and decrypt it seamlessly on the way out.

Here’s the starting point for the EncryptedQueryString class you need:

public class EncryptedQueryString : System.Collections.Specialized.StringDictionary

{

public EncryptedQueryString()

{

// Nothing to do here

}

public EncryptedQueryString(string encryptedData)

{

//Decrypt information and add to

//the dictionary

}

public override string ToString()

{

//Encrypt information and return as

//HEX-encoded string

}

}

You should notice one detail immediately about the EncryptedQueryString class: it derives from the StringDictionary class, which represents a collection of strings indexed by strings. By deriving from StringDictionary, you gain the ability to use the EncryptedQueryString like an ordinary string collection. As a result, you can add information to the EncryptedQueryString in the same way you add information to the Request.QueryString collection. Here’s an example:

encryptedQueryString["value1"] = "Sample Value";

Best of all, you get this functionality for free, without needing to write any additional code. So, with just this rudimentary class, you have the ability to store a collection of name/value strings. But how do you actually place this information into the query string? The EncryptedQueryString class provides a ToString() method that examines all the collection data and combines it in a single encrypted string.

First, the EncryptedQueryString class needs to combine the separate collection values into a delimited string so that it’s easy to split the string back into a collection on the destination page. In this case, the ToString() method uses the conventions of the query string, separating each value from the name with an equal sign (=) and separating each subsequent name/value pair with the ampersand (&). However, for this to work, you need to make sure the names and values of the actual item in the collection don’t include these special characters. To solve this problem, the ToString() method uses the HttpServerUtility.UrlEncode() method to escape the strings before joining them.

C H A P T E R 2 5 C RY P TO G R A P H Y

863

Here’s the first portion of the ToString() method, which escapes and joins the collection settings into one string:

public override string ToString()

{

StringBuilder Content = new StringBuilder();

//Go through the contents and build a

//typical query string

foreach (string key in base.Keys)

{

Content.Append(HttpUtility.UrlEncode(key));

Content.Append("=");

Content.Append(HttpUtility.UrlEncode(base[key]));

Content.Append("&");

}

// Remove the last '&' Content.Remove(Content.Length-1, 1);

...

The next step is to use the ProtectedData class to encrypt the data. This class uses the DPAPI to encrypt the information and its Protect method to return a byte array, so you need to take additional steps to convert the byte array to a string form that’s suitable for the query string. One approach that seems reasonable is the static Convert.ToBase64String() method, which creates a Base64-encoded string. Unfortunately, Base64 strings can include symbols that aren’t allowed in the query string (namely, the equal sign). Although you could create a Base64 string and then URLencode it, this further complicates the decoding stage. The problem is that the ToBase64String() method may also introduce a series of characters that look like URL-encoded character sequences. These character sequences will then be incorrectly replaced when you decode the string.

A simpler approach is to use a different form of encoding. This example uses hex encoding, which replaces each character with an alphanumeric code. The methods for hex encoding aren’t shown in this example, but they are available with the downloadable code.

...

//Now encrypt the contents using DPAPI byte[] EncryptedData = ProtectedData.Protect(

Encoding.UTF8.GetBytes(Content.ToString()), null, DataProtectionScope.LocalMachine);

//Convert encrypted byte array to a URL-legal string

//This would also be a good place to check that data

//is not larger than typical 4 KB query string

return HexEncoding.GetString(EncryptedData);

}

You can place the string returned from EncryptedQueryString.ToString() directly into a query string using the Response.Redirect() method.

The destination page that receives the query data needs a way to deserialize and decrypt the string. The first step is to create a new EncryptedQueryString object and supply the encrypted data. To make this step easier, it makes sense to add a new constructor to the EncryptedQueryString class that accepts the encrypted string, as follows:

864C H A P T E R 2 5 C RY P TO G R A P H Y

public EncryptedQueryString(string encryptedData)

{

// Decrypt data passed in using DPAPI

byte[] RawData = HexEncoding.GetBytes(encryptedData); byte[] ClearRawData = ProtectedData.Unprotect(

RawData, null, DataProtectionScope.LocalMachine); string StringData = Encoding.UTF8.GetString(ClearRawData);

// Split the data and add the contents int Index;

string[] SplittedData = StringData.Split(new char[] { '&' }); foreach (string SingleData in SplittedData)

{

Index = SingleData.IndexOf('='); base.Add(

HttpUtility.UrlDecode(SingleData.Substring(0, Index)), HttpUtility.UrlDecode(SingleData.Substring(Index + 1))

);

}

}

This constructor first decodes the hexadecimal information from the string passed in and uses the DPAPI to decrypt information stored in the query string. It then splits the information back into its parts and adds the key/value pairs to the base StringCollection.

Now you have the entire infrastructure in place to create a simple test page and transmit information from one page to another in a secure fashion.

Creating a Test Page

To try the EncryptedQueryString class, you need two pages—one that sets the query string and redirects the user and another that retrieves the query string. The first one contains a text box for entering information, as follows:

<form id="form1" runat="server"> <div>

Enter some data here: <asp:TextBox runat="server" ID="MyData" /> <br />

<br />

<asp:Button ID="SendCommand" runat="server" Text="Send Info" OnClick="SendCommand_Click" />

</div>

</form>

When the user clicks the SendCommand button, the page sends the encrypted query string to the receiving page, as follows:

protected void SendCommand_Click(object sender, EventArgs e)

{

EncryptedQueryString QueryString = new EncryptedQueryString();

QueryString.Add("MyData", MyData.Text);

QueryString.Add("MyTime", DateTime.Now.ToLongTimeString());

QueryString.Add("MyDate", DateTime.Now.ToLongDateString());

Response.Redirect("QueryStringRecipient.aspx?data=" + QueryString.ToString());

}

C H A P T E R 2 5 C RY P TO G R A P H Y

865

Notice that the page enters the complete encrypted data string as one parameter called data into the query string for the destination page. Figure 25-9 shows the page in action.

Figure 25-9. The source page in action

The destination page deserializes the query string passed in through the data query string parameter with the previously created class, as follows:

protected void Page_Load(object sender, EventArgs e)

{

//Deserialize the encrypted query string EncryptedQueryString QueryString =

new EncryptedQueryString(Request.QueryString["data"]);

//Write information to the screen

StringBuilder Info = new StringBuilder(); foreach (String key in QueryString.Keys)

{

Info.AppendFormat("{0} = {1}<br>", key, QueryString[key]);

}

QueryStringLabel.Text = Info.ToString();

}

This code adds the information to a label on the page. You can see the result of the previously posted information in Figure 25-10.

Figure 25-10. The results of the received query string information

866 C H A P T E R 2 5 C RY P TO G R A P H Y

Summary

In this chapter, you learned how take control of the .NET security with advanced techniques. You saw how to use stream-based encryption to protect stored data and the query string. In the next chapter, you’ll learn how to use powerful techniques to extend the ASP.NET security model.

C H A P T E R 2 6

■ ■ ■

Custom Membership Providers

In the previous chapters, you learned all the necessary details for authenticating and authorizing users with ASP.NET through both forms authentication and Windows authentication. You learned that with forms authentication on its own, you are responsible for managing users (and roles if you want to implement role-based authorization in your application) in a custom store.

Fortunately, ASP.NET 2.0 ships with the Membership API and the Roles API, which provide you with a framework for user and roles management. You learned the details about the Membership API in Chapter 21, and you learned about the Roles API in Chapter 23. You can extend the framework through providers that implement the actual access to the underlying data store. In both of those chapters, you used the default provider for SQL Server that ships with ASP.NET 2.0.

Of course, you can exchange the default implementation that works with SQL Server by implementing custom Membership and Roles providers. This gives you the possibility of exchanging the underlying storage used for user and role information, without affecting your web application.

In this chapter, you will learn how you can extend the Membership API and the Roles API by implementing custom Membership and Roles providers. Furthermore, you will learn how you can configure and debug your custom provider for web applications. With the information in this chapter, you will also be equipped to create other custom providers—for example, providers for the Profiles API and the personalization engine of web parts—because the creation process is always the same.

Note Because the provider model was introduced in ASP.NET 2.0, most of the information in this chapter is new. Of course, for developing a custom Membership and Roles provider, you need in-depth know-how of

ADO.NET, System.Xml, and the basic ASP.NET infrastructure. If you are coming from ASP.NET 1.1, you should read Chapters 21 and 23 before digging into this chapter. If you are new to ASP.NET, you should read Chapters 19, 20, 21, 23, and 25 as well as Chapters 7, 8, 12, and 13 before you start reading this chapter.

Architecture of Custom Providers

In Chapters 21 and 23 you learned many details of the integrated Membership and Roles Services. These services provide you with an out-of-the-box solution for managing users and roles with forms authentication. As explained earlier, you can extend the model through providers, as shown in Figure 26-1. When implementing custom providers, you should always keep the architecture shown in Figure 26-1 in mind. A custom provider is always based on the lowest level in the layered model introduced by the ASP.NET 2.0 Membership and Roles framework. It’s important to know that every other provider-based API in ASP.NET 2.0 is structured in the same way. Therefore, implementing custom providers for the Profiles API or the personalization engine of ASP.NET 2.0 is similar.

867