ntwo

导航

Beginning ASP.NET Security1

Chapter 1: Why web secrutiy matters

PART I: THE ASP.NET SECURITY BASICS

Chapter 2: How the web works

Examing HTTP

Request  a Resource

Responding to a Request

Sniffing HTTP Requests and Responses

Use Fiddler to sniff:  http://www.fiddler2.com.

Understanding HTML FORMS

Examining How ASP.NET Works

Understanding How ASP.NET Events Work

Examing the ASP.NET Pipeline

Writing HTTP Modules

Chapter 3: Safely accepting user input

Defining Input

Dealing with Input Safely

XSS: Cross Site Scripting 跨站脚本攻击

The XSS Cheat Sheet:  http://ha.ckers.org/xss.html.

The Microsoft AntiXSS Library: http://www.codeplex.com/antixss.

Encodings Supported by the Microsoft Anti-XSS Library:

ENCODING

USAGE

HtmlEncode Use this when untrusted input is assigned to HTML output, unless it is assigned to an HTML attribute.
HtmlAttributeEncode Use this when untrusted input is assigned to an HTML attribute (such as id, name, width, or height).
JavaScriptEncode Use this when untrusted input is used within JavaScript.
UrlEncode Use this when untrusted input is used to produce (or is used within) a URL.
VisualBasicScriptEncode Use this when untrusted input is used to within VBScript.
XmlEncode Use this when untrusted input is assigned to XML output, unless it is assigned to an XML attribute.
XmlAttributeEncode Use this when untrusted input is assigned to an XML attribute.

Validating Form Input

A Checklist for Handling Input

  • Review all inputs to a system and decide if they are trustworthy — Remember that all inputs should be considered untrustworthy by default. If input must be trusted and comes from outside your application, it must be validated and sanitized. A good practice is to perform validation for all inputs, trusted or not.
  • Review code that generates output— Remember that XSS attacks are dependent on using untrusted input as direct ouput. Examine your code. Look for Response.Write, %= and setting Text of Web Controls as well as other properties on ASP.NET controls.
  • Examine output functions and determine if they use untrusted input parameters—Once all output parameters have been discovered, examine the values they are using to generate output. If they are using untrusted input, then it will require encoding. Typical input sources that generate output include database queries, the reading of files from the file system, and calls to Web services.
  • Determine what encoding the output expects—Different output types require different encoding methods. For example, HTML requires HTML encoding, URLs require URL encoding, and so on.
  • Encoding output—When assigning output, use the encoding you have determined to make the output safe.
  • Ensure cookies are marked as HttpOnly—As part of your layered defense, ensure that any cookies that you do not need to access on the Web client are marked with the HttpOnly attribute.
  • Do not disable request validation on a site wide basis—Request validation should be disabled on per-page basis. This ensures that any page where you forget that input is acceped will be proteced untill you add encoding to the page output and turn request validation off.
  • Use Microsoft Anti-XSS library and SRE—The Microsoft Anti-XSS library provices more robust and flexible encoding methods than the standard .NET framework. In addition, the SRE will automatically encode output for controls it knows about. However, this is not an execuse to avoid explicitly encoding output yourself.

Chapter 4: Using query strings, form fileds, events, and browser information

Using the Right Input Type

Query Strings

Form Fields

Request Forgery and how to Avoid It

Cross Site Request Forgery (CSRF) 跨站请求伪造

2011-02-10 17 21 22

Mitigating Against CSRF

For a CSRF attack to work, the following conditions must be met:

  • The attacker must have knowledge of sites on which the victim is currently authenticated. These sites may be Internet sites or intranet applications.
  • The target site must use ambient authority, where the browser sends authentication credentials with each request.
  • The target site must not have secondary authentication for actions, such as a requirement to re-enter a password.

The common mitigation technique against CSRF for ASP.NET sites is to use ViewState in combination with a ViewStateUserKey.

If the ViewStateUserKey does not meet your needs, another method of mitigation is to add a token to every form, which is verified when the form is submitted. You must generate a token for every session, store it in session state or in a cookie on the user’s machine, insert the token (or a value generated from it ) into each form, and check it with every form submission. And, you can automate the entire process by implementing an HTTP Module.

To add CSRF protection to every form submission, you should implement the following actions:

  1. If a request is from a new user (no cookie is sent), generate a unique token for that user.
  2. If the request is a GET request, store that token in a session cookie on the user’s browser. (A session cookie is one that is deleted when the user closes the browser.)
  3. If the request is a POST request (or PostBack) and the token is not present, reject the request as a potential CSRF attack.
  4. If the request is a POST request (or PostBack), read the token from the user’s browser and compare it to the token embedded in the ASP.NET Web form. If the tokens do not match, or the token is missing, reject the request as a potential CSRF attack.
  5. If the tokens match, allow the request to continue.
  6. When the request is completed, but before the request is set, examine the response to look for an ASP.NET Web forms. If one is present, automatically add the token (or a value generated from it) into the form as a hidden field.

HTTP Module to Protect Against CSRF Attacks:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Web;
using System.Web.UI;

namespace AntiCSRF
{
     public class AntiCSRF : IHttpModule
     {
          public void Dispose()
          {

          }

          public void Init(HttpApplication context)
          {
               context.PreSendRequestHeaders += new EventHandler(PreSendRequestHeaders);
               context.PreRequestHandlerExecute += new EventHandler(PreRequestHandlerExecute);
          }

          private static void PreSendRequestHeaders(object source, EventArgs args)
          {
               HttpApplication application = (HttpApplication)source;
               HttpContext context = application.Context;
               if (context.Handler != null)
               {
                    Page page = context.Handler as Page;
                    if (page != null)
                    {
                         HttpCookie csrfCookie = new HttpCookie("__CSRFCOOKIE")
                         {
                              Value = context.Items["CSRFContext"].ToString(),
                              HttpOnly = true
                         };
                         context.Response.Cookies.Add(csrfCookie);
                    }
               }
          }

          private static void PreRequestHandlerExecute(object source, EventArgs args)
          {
               HttpApplication application = (HttpApplication)source;
               HttpContext context = application.Context;
               if (context.Handler != null)
               {
                    Page page = context.Handler as Page;
                    if (page != null)
                    {
                         page.PreRender += PagePreRender;

                         if (context.Request.HttpMethod.Equals("POST",StringComparison.Ordinal))
                         {
                              if (context.Request != null)
                              {
                                   HttpCookie csrfCookie = context.Request.Cookies["__CSRFCOOKIE"];
                                   string csrfFormField = context.Request.Form["__CSRFTOKEN"];
                                   if (string.IsNullOrEmpty(csrfFormField) &&
                                        (csrfCookie == null || string.IsNullOrEmpty(csrfCookie.Value)))
                                        throw new Exception("Cookie and form field missing");
                                   if (csrfCookie == null || string.IsNullOrEmpty(csrfCookie.Value))
                                        throw new Exception("Cookie missing");
                                   if (string.IsNullOrEmpty(csrfFormField))
                                        throw new Exception("Form field missing");
                                   string tokenField = string.Empty;
                                   ObjectStateFormatter formatter = new ObjectStateFormatter();
                                   try
                                   {
                                        tokenField = formatter.Deserialize(context.Request.Form["__CSRFTOKEN"]) as string;
                                   }
                                   catch
                                   {
                                        throw new Exception("Form field format error");
                                   }
                                   if (csrfCookie.Value.Equals(tokenField))
                                        throw new Exception("Mismatched CSRF tokens");
                              }
                         }

                    }
               }
          }

          private static void PagePreRender(object source, EventArgs args)
          {
               Page page = source as Page;
               if (page != null && page.Form != null)
               {
                    string csrfToken;
                    HttpContext context = HttpContext.Current;
                    if (context.Request == null ||
                         context.Request.Cookies == null ||
                         context.Request.Cookies["__CSRFCOOKIE"] == null ||
                         string.IsNullOrEmpty(context.Request.Cookies["__CSRFCOOKIE"].Value))
                    {
                         csrfToken = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);
                         context.Items["CSRFContext"] = csrfToken;
                    }
                    else
                         csrfToken = context.Request.Cookies["__CSRFCOOKIE"].Value;

                    ObjectStateFormatter formatter = new ObjectStateFormatter();
                    page.ClientScript.RegisterHiddenField("__CSRFTOKEN", formatter.Serialize(csrfToken));
               }
          }
     }
}

Avoiding Mistakes with Browser Information

A Checklist for Query Strings, Forms, Events, and Browser Information

  • Never change state via GET request.—The HTTP specifications state that GET requests must not change state.
  • Do not use direct, sequential object referecnes.—Always use indirect object references (such as a GUID) to refer to resources on a Web server. Direct object references can be changed easily to allow attackers to access objects they should not be able to see. Check that the current user is authorized to see the object requested.
  • Do not use hidden form fields to hold sensitive information, unless they are properly protected.—Remember that form fields (and query strings) can be manipulated by attackers.
  • Add a CSRF token to your forms.—This will allow you to check that the request came from your own Web site.
  • Check the Request type when checking if a request is a postback.—This will protect you from ASP.NET considering query string-driven requests as potential postbacks.
  • Do not disable event validation, but do not rely on it.—Registering for event validation is optional for controls. Always check conditions within postback events.
  • Do not rely on Request headers.

Chapter 5: Controlling information

Controlling ViewState

Validating ViewState

ViewState validation ensures that no one can tamper with the contents.

Encrypting ViewState

ViewState encrytion ensures that no one can view the data.

To enforce ViewState encryption for entire application, set the viewStateEncrytionMode attribute on the pages element in web.config:

<pages ... viewStateEncryptionMode="Always" ... />

Prgrammatically request encryption on a per-page basis by calling Page.RegisterRequiresViewSateEncryption(), or by setting the ViewStateEncryptionMode attribute in the page directive:

<%@ Page Language="C#" ... ViewStateEncryptionMode="Always" %>

Encrypting ViewState will increase the time it takes for a page to render and respond, as well as affect the size of the hidden form field. Be sure to run tests to see if any increases are acceptable in terms of load time and bandwidth.

Protecting Against ViewState One-Click Attacks

A replay attack occurs when an attacker takes a valid ViewState from a previous erquest and sends it at a later point, or under the context of another user.

Often, a ViewState replay attack can be used in the flavor of Cross Site Request Forgery (CSRF) called a one-click attack, where a form is submitted via JavaScript to a vulnerable page.

In light of this attack method, ASP.NET provides the ViewStateUserKey property as a way to lock ViewState to a specific user or session. Generally, this value is set to either the username of a currently authenticated user, or, the session identifier for the current session.

Setting a ViewState User Key in global.asax:

<%@ Application Language="C#" %> 
   
<script runat="server"> 
   void Application_PreRequestHandlerExecute(object sender, EventArgs e)
   {
       HttpContext context = HttpContext.Current;
       // Check we are actually in a webforms page.
       Page page = context.Handler as Page;
       if (page != null)
       {
           // Use the authenticated user if one is available,
           // so as the user key does not expire over
           // application recycles.
           if (context.Request.IsAuthenticated)
           {
               page.ViewStateUserKey = context.User.Identity.Name;
           }
           else
           {
               page.ViewStateUserKey = context.Session.SessionID;
           }
       }
   }
</script>

Setting a ViewState User Key in a Base Class

using System;
using System.Web.UI;
   
public class ProtectedViewStatePage : Page
{
    protected override void OnInit(EventArgs e)
    {
        if (Request.IsAuthenticated)
        {
            ViewStateUserKey = User.Identity.Name;
        }
        else
        {
            ViewStateUserKey = Session.SessionID;
        }
        
        base.OnInit(e);
    }
}

You should then change the class your pages inherit from to the new base class you creaed. If you do not use code behind, then you can set the base class application-wide by using the <pages> element in web.config:

<system.web>
    <pages pageBaseType="ProtectedViewStatePage" /> 
</system.web>

Removing ViewState from the Client Page

Store ViewState in session:

protected override PageStatePersister PageStatePersister
{
    get
    {
        return new SessionPageStatePersister(this);
    }
}

Disabling Browser Caching

Using the OutputCache directive on a page to disable browser caching:

<%@ OutputCache Location="None" VaryByParam="None" %>

or by code, add following code to Page_Load event:

 Response.Cache.SetCacheability(HttpCacheability.NoCache);

Always disable caching for pages that contain sensitive data.

Error Handling and Logging

Improving Your Error Handling

ASP.NET provides error events you can respond to at both a page level (Page_Error) and application level (Application_Error).

Handle errors within in a page class:

public partial class MyPage : System.Web.UI.Page 
{
    protected void Page_Error(object sender, EventArgs e)
    {
        // Log Errors.
        Exception ex = Server.GetLastError();
        Error.Log(ex);
    }
}

Handle errors within global.asax:

<%@ Application Language="C#" %> 
<script runat="server"> 
   void Application_Error(object sender, EventArgs e) 
   {
       // Log Errors.
       Exception ex = Server.GetLastError();
       Error.Log(ex);
   }       
</script>   

Watching for Special Exceptions

Some examples of exceptions that indicate a potential threat:

EXCEPTION WHEN OCCURS
HttpRequestValidationException Occurs when request validation is on, and potentially threatening characters are sent with a request.
ArgumentException Occurs when event validation fails, indicating an attempt to fire an event that is not valid for a page.
ViewStateException Occurs when an invalid ViewState has been sent.

It is possible for unhandled exception to cause your entire application to crash if they occur outside of a page request. An example might be found in a background worker thread, or within the garbage colloctor. Micorsoft recommends that an HttpModule be used to catch these types of errors:

using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Web;
 
namespace WebMonitor {
    public class UnhandledExceptionModule: IHttpModule {

        static int _unhandledExceptionCount = 0;

        static string _sourceName = null;
        static object _initLock = new object();
        static bool _initialized = false;

        public void Init(HttpApplication app) {

            // Do this one time for each AppDomain.
            if (!_initialized) {
                lock (_initLock) {
                    if (!_initialized) { 

                        string webenginePath = Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "webengine.dll"); 

                        if (!File.Exists(webenginePath)) {
                            throw new Exception(String.Format(CultureInfo.InvariantCulture,
                                                              "Failed to locate webengine.dll at '{0}'.  This module requires .NET Framework 2.0.", 
                                                              webenginePath));
                        } 

                        FileVersionInfo ver = FileVersionInfo.GetVersionInfo(webenginePath);
                        _sourceName = string.Format(CultureInfo.InvariantCulture, "ASP.NET {0}.{1}.{2}.0",
                                                    ver.FileMajorPart, ver.FileMinorPart, ver.FileBuildPart);

                        if (!EventLog.SourceExists(_sourceName)) {
                            throw new Exception(String.Format(CultureInfo.InvariantCulture,
                                                              "There is no EventLog source named '{0}'. This module requires .NET Framework 2.0.", 
                                                              _sourceName));
                        }
 
                        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(OnUnhandledException);
 
                        _initialized = true;
                    }
                }
            }
        }

        public void Dispose() {
        }

        void OnUnhandledException(object o, UnhandledExceptionEventArgs e) {
            // Let this occur one time for each AppDomain.
            if (Interlocked.Exchange(ref _unhandledExceptionCount, 1) != 0)
                return;

            StringBuilder message = new StringBuilder("\r\n\r\nUnhandledException logged by UnhandledExceptionModule.dll:\r\n\r\nappId=");

            string appId = (string) AppDomain.CurrentDomain.GetData(".appId");
            if (appId != null) {
                message.Append(appId);
            }
            

            Exception currentException = null;
            for (currentException = (Exception)e.ExceptionObject; currentException != null; currentException = currentException.InnerException) {
                message.AppendFormat("\r\n\r\ntype={0}\r\n\r\nmessage={1}\r\n\r\nstack=\r\n{2}\r\n\r\n",
                                     currentException.GetType().FullName, 
                                     currentException.Message,
                                     currentException.StackTrace);
            }           

            EventLog Log = new EventLog();
            Log.Source = _sourceName;
            Log.WriteEntry(message.ToString(), EventLogEntryType.Error);
        }

    }
}

Logging Errors and Monitoring Your Application

Using the Windows Event Log

EventLog.CreateEventSource("MyWebApplication", "Application");
EventLog.WriteEntry("MyWebApplication", "Something bad happened", EventLogEntryType.Error, 101); 

Using Email to Log Events

Using asynchronmous sending email:

<%@ Page Async="true" ... %>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net.Mail;
using System.Web;
   
public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Error(object sender, EventArgs e)
    {
        MailMessage mail = new MailMessage();
        // Create the message
        mail.From = new MailAddress("webError@wrox.example");
        mail.To.Add("monitor@wrox.example");
        mail.Subject = "Unhanded exception in "+Context.Request.Path;
        mail.Body = Server.GetLastError().ToString();
        SmtpClient smtp = new SmtpClient();
        object userState = mail;
   
        //wire up the Asynce event for the send is completed
        smtp.SendCompleted += new SendCompletedEventHandler(smtp_SendCompleted);
        smtp.SendAsync(mail, userState);
    }
   
    void smtp_SendCompleted(object sender, AsyncCompletedEventArgs e)
    {
        //Get the Original MailMessage object
        MailMessage mail = (MailMessage)e.UserState;
   
        if (e.Error != null)
        {
            LogErrorElsewhere("Error {1} occurred when sending mail [{0}] ", mail.Subject, e.Error.ToString());
        }
    }
}   

Using ASP.NET Tracing

Using Performance Counters

string counterCategory = "SecuringASPNet";
   
if (!PerformanceCounterCategory.Exists(counterCategory))
{
    CounterCreationDataCollection counterCreationDataCollection = 
        new CounterCreationDataCollection();
   
    counterCreationDataCollection.Add(
        new CounterCreationData("BadGuysFound",
            "Total number of bad guys detected",
            PerformanceCounterType.NumberOfItems32)
        );
   
    counterCreationDataCollection.Add(
        new CounterCreationData("BadGuysFoundPerSecond",
            "How many bad guys have been detected",
            PerformanceCounterType.RateOfCountsPerSecond32)
        );
   
    PerformanceCounterCategory.Create(counterCategory,
        "My category description/Help",
        PerformanceCounterCategoryType.SingleInstance,
        counterCreationDataCollection);
} 

If you write an installer for your application, you can derive an installer component from PerformanceCounterInstaller, and then either use InstallUtil form the .NET framework, or use it as a custom action in your Microsoft Installer Package (MSI):

[RunInstaller(true)]
public class CountersInstaller : PerformanceCounterInstaller
{
    public CountersInstaller()
    {
        this.CategoryName = "SecuringASPNet";
        Counters.Add(
            new CounterCreationData("BadGuysFound",
                "Total number of bad guys detected",
                PerformanceCounterType.NumberOfItems32)
            );
   
        Counters.Add(
            new CounterCreationData("BadGuysFoundPerSecond",
                "How many bad guys have been detected",
                PerformanceCounterType.RateOfCountsPerSecond32)
            );
}

Use counters:

PerformanceCounter badGuysFound = 
    new PerformanceCounter("SecuringASPNet",
    "BadGuysFound",
    false);
   
PerformanceCounter badGuysFoundPerSecond =
    new PerformanceCounter("SecuringASPNet",
    "BadGuysFoundPerSecound",
    false);
   
badGuysFound.Increment();
badGuysFoundPerSecond.IncrementBy(1); 

Using WMI Events

Another Alternative: Logging Frameworks

Limiting Search Engines

Controlling Robots with a Metatag

<html> 
    <head> 
    <title> ... </title> 
    <meta name="ROBOTS" content="NOINDEX. NOFOLLOW" /> 
    </head> 
</html>   

Controlling Robots with robots.txt

A sample robots.txt file:

# This is a comment in a robots.txt file
   
User-Agent: r2d2
Disallow:
   
User-Agent: c3po
Disallow: /trashCompactor
   
User-Agent:  * 
Disallow: /deathStarPlans
Disallow: /logs

A sample robots.txt file that stops all crawling

User-agent:  * 
Disallow: /

Unfortunately, a robots.txt file can only contain disallowed areas. Attackers often check this file for directories where robots should not go, ant attemp to load those directoies to see what happens. One other thing to note is that the robots.txt file should be in a Unix text format — only LineFeed characters marking the end of a file, not the Carriage Return and Line Feed combination that Windows uses. You can use Visual Studio to save a file in the correct format by selecting File | Advanced Save and choosing Unix from the Line Endings drop-down menu.

Protecting Passwords in Config Files

encrypt a section in the web.config:

aspnet_regiis -pef SecretSection

The drawback to this apporach is that, if the web.config file moved to another server, decryption will fail. So, if you have multiple servers in a Web farm, the command must be run on every server. For this scenario, you must use a RSA key container that can be exported from one machine and imported onto another one. In an elevated command prompt, the following steps will create an RSA key container and then export it:

  1. Create a new key container for exporting:
    aspnet_regiis -pc MyConfigurationKey -size 2048 –exp
  2. Export the key to an XML file:
    aspnet_regiis -px MyConfigurationKey c:\myconfi gurationkey.xml   
  3. Import the key on every server:
    aspnet_regiis -pi MyConfigurationKey myconfi gurationkey.xml
  4. Set permissions on the key container for the account under which your application runs:
    aspnet_regiis -pa MyConfigurationKey "machineName\Account"

Once you move to a key container, you must add a section to your configuration file to tell ASP.NET that you want to use your new key container:

<configuration> 
   <configProtectedData> 
       <providers> 
           <remove name="RsaProtectedConfigurationProvider"/> 
           <add name="RsaProtectedConfigurationProvider"
               type="System.Configuration.RsaProtectedConfigurationProvider, 
                      System.Configuration, Version=2.0.0.0, Culture=neutral, 
                      PublicKeyToken=b03f5f7f11d50a3a"
               keyContainerName="MyConfigurationKey"
               cspProviderName=""
               useOAEP="false"
               useMachineContainer="true" /> 
    </providers> 
</configProtectedData> 
</configuration>

You can then encrypt sections by using your new provider:

aspnet_regiis -pe SectionName -prov RsaProtectedConfigurationProvider  

Use the –pd switch, to decrypt a configuration section, use the –pz switch to delete a key container.

A Checklist for Query Strings, Forms, Events, and Browser Information

  • Prevent reply attack. —If you use ViewState, implement a ViewStateUserKey to prevent reply attacks.
  • Protect sensitive information.—Never put sensitive information in ViewState.
  • Encrypt ViewState.— If you must use ViewState for sensitive infomation, then encrypt ViewState.
  • Avoid error pages.—Do not use the default error pages.
  • User error handlers.—Catch all errors in page level or application level error handlers, and user Server.Transfer to switch to an error page.
  • Use instrumentation.—Log errors using a suitable instrumentation type.
  • Do not use .bak extensions.—Never rename .config file with a .bak extension.
  • Be aware of sensitive configuration information.—Encrypt sensitive parts of a configuration file.

Chapter 6: Keeping secrets secret — hasing and encryption

Protecting Integrity with Hashing

Choosing a Hasing Algorithm

The most common algorithms in use are MD5 and SHA1. However, both of these algorithms have been shown to have weaknesses, and should be avoided whenever possible. The current recommended algorithms are SHA256 and SHA512.

Use the SHA256 algorithm to calculaet a hash for the provided string:

private string CalculateSHA256Hash(string input)
{
    // Encode the input string into a byte array.
    byte[] inputBytes = Encoding.UTF8.GetBytes(input);
   
    // Create an instance of the SHA256 algorithm class
    // and use it to calculate the hash.
    SHA256Managed sha256 = new SHA256Managed();
    byte[] outputBytes = sha256.ComputeHash(inputBytes);
   
    // Convert the outputed hash to a string and return it.
    return Convert.ToBase64String(outputBytes);
} 

The American National Security Agency recommended algorithms:

  http://www.nsa.gov/ia/programs/suiteb_cryptography/index.shtml

Protecting Passwords with Hasing

Salting Passwords

Because a hashing algorithm is deterministic, it is posible to produce pre-calculated lists of common hashes (for example, to produce a database of hashes for every word in a dictionary). The initial time taken to produce these lists is significant. However, once produced, lookup is simple.

Salting involves the addition of entropy to the password hash by storing the combined has of a salt (a random value) and the password. The salt does not need to be kept secret, and so it can be stored with the password hash. The combination of a salt plus a password means that a dictionary of hash lookups would have to be produced for every possible salt value, which would take both a significant amount of time and space. For each value you hash, you should use a new salt value. Using a new salt means that a new dictionary would have to be produced for every single password stored in your system.

Generating Secure Random Numbers

The .NET framework includes a random number class, Random, which can be used to generate random numbers and bytes. However, the Random class is not suitable for cryptography. It does not generate cryptographically secure random numbers.

The .NET Framework provides a cryptographically secure pseudo-random number generator (CSPRNG) class, System.Cryptography.RNGCryptoServiceProvider, which you should use whenever you want to generate generate random data for cryptographic purposes. For salting purposes:

byte[] saltBytes;
int minSaltSize = 4;
int maxSaltSize = 8;
   
// Generate a random number to determine the salt size.
Random  random = new Random();
int saltSize = random.Next(minSaltSize, maxSaltSize);
   
// Allocate a byte array, to hold the salt.
saltBytes = new byte[saltSize];
   
// Initialize the cryptographically secure random number generator.
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
   
// Fill the salt with cryptographically strong byte values.
rng.GetNonZeroBytes(saltBytes); 

Once you have the salt value, you must combine it with the clear text to produce a salted hash, using the salt value as a prefix to the clear text, or appending it to the clear text before calculating the hash.

Calculate a SHA256 hash:

// Convert the clear text into bytes.
byte[] clearTextBytes = Encoding.UTF8.GetBytes(clearText);
   
// Create a new array to hold clear text and salt.
byte[] clearTextWithSaltBytes = 
    new byte[clearTextBytes.Length + saltBytes.Length];
   
// Copy clear text bytes into the new array.
for (int i=0; i  <  clearTextBytes.Length; i++)
    clearTextWithSaltBytes[i] = clearTextBytes[i];
        
// Append salt bytes to the new array.
for (int i=0; i  <  saltBytes.Length; i++)
    clearTextWithSaltBytes[clearTextBytes.Length + i] = saltBytes[i];

// Calculate the hash
HashAlgorithm hash = new SHA256Managed();
byte[] hashBytes = hash.ComputeHash(clearTextWithSaltBytes);

Validate password:

private bool IsPasswordValid(string password, byte[] savedSalt, byte[] savedHash)
{
    Rfc2898DeriveBytes rfc2898DeriveBytes = 
        new Rfc2898DeriveBytes(password, savedSalt, NumberOfIterations);
    // Convert the provided password into bytes.
    byte[] clearTextBytes = Encoding.UTF8.GetBytes(clearText);
   
    // Create a new array to hold clear text and salt.
    byte[] clearTextWithSaltBytes = 
        new byte[clearTextBytes.Length + saltBytes.Length];
   
    // Copy clear text bytes into the new array.
    for (int i=0; i  <  clearTextBytes.Length; i++)
        clearTextWithSaltBytes[i] = clearTextBytes[i];
        
    // Append salt bytes to the new array.
    for (int i=0; i  <  saltBytes.Length; i++)
        clearTextWithSaltBytes[clearTextBytes.Length + i] = saltBytes[i];
   
    // Calculate the hash
    HashAlgorithm hash = new SHA256Managed();
    byte[] currentHash = hash.ComputeHash(clearTextWithSaltBytes);
   
    // Now check if the hash values match.
    bool matched = false;
    if (currentHash.Length == savedHash.Length)
    {
        int i = 0;
        while ((i  <  currentHash.Length)  &  &  (currentHash[i] == savedHash[i]))
        {
            i += 1;
        }
        if (i == currentHash.Length)
        {
            matched = true;
        }
    }
    return (matched); 
} 

Encrypting Data

Understanding Symmetric Entryption

Symmetic algorithms are suitable for scenarios where an application needs to both encrypt and decrypt the data.

Protecting Data with symmetric Encryption

Follow these steps to encrypt data symmetrically:

  1. Choose an algorithm.
  2. Create or retrieve a key.
  3. Generate the IV.
  4. Convert the clear text data to an array of bytes.
  5. Encrypt the clear text array.
  6. Store the encrypted data and the IV.
  7. If the key is new, store it.

Follow these steps to decrypt the data:

  1. Choose the same algorithm that was used to encrypt the data.
  2. Retrieve the key that was used.
  3. Retrieve the IV that was used.
  4. Retrieve the encrypted data.
  5. Decrypt the data.
  6. Convert the decrypted data back to its original format.

Remember that it is important to keep the key secret. Storage of encryption keys should be separated from the storage of the encrypted data, and locked down to only allow authorized use. An example would be separate databases on a SQL Server.

The Organization for the Advancement of Structured Information Standards (OASIS) has an entire Technical Committee dedicated to Key Management:    http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=ekmi.

Choosing a Symmetric Algorithm

The .NET framework provides the most common symmetric encryption algorithms:

  • Data Encryption Standard (DES)
  • Triple Data Encryption Algorithm (3DES/TDEA)
  • RC2
  • Rijndael/Advanced Encryption Standard (AES)

Generally, you should use RijndaelManaged or the AesManaged classes because they are the most commonly used symmetric algorithms in use today.

Generating Keys and Initialization Vectors

Generally, a key size of 128 bits (the standard size for SSL) is considered sufficient for most applications. A key size of 168 bits or 256 bits should be considered for highly secure systems (such as large financial transactions). The length of the initialization vector for an algorithm is equal to its block size, which you can access by using the BlockSize property on an instance of the algorithm.

Generate secure keys and IVs:

static byte[] GenerateRandomBytes(int length)
{
    byte[] key = new byte[length];
    RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
    provider.GetBytes(key);
    return key;
}

The Web site, http://www.keylength.com, contains the current, historical, and futrue requirements for recommended and mandated key sizes and algorithms by such organizaions.

You can also use the Rfc2898DriveBytes class to generate a key and initialization vector pair from a known value like a password combined with a random salt. Using a password to create a key is an easy way to provide secure data that only a user can unlock, or, if the password is not sourced from a user, it makes for easy storage in a configuration file as opposed to a binary key.

private void GetKeyAndIVFromPasswordAndSalt(
    string password, byte[] salt, 
    SymmetricAlgorithm symmetricAlgorithm, 
    ref byte[] key, ref byte[] iv)
{
    Rfc2898DeriveBytes rfc2898DeriveBytes = 
        new Rfc2898DeriveBytes(password, salt);
    key = 
        rfc2898DeriveBytes.GetBytes(symmetricAlgorithm.KeySize / 8);
    iv = 
        rfc2898DeriveBytes.GetBytes(symmetricAlgorithm.BlockSize / 8); 
}
Encrypting and Decrypting Your Data

Encrypt using the Rijndael algorithm:

static byte[] Encrypt(byte[] clearText, byte[] key, byte[] iv)
{
    // Create an instance of our encyrption algorithm.
    RijndaelManaged rijndael = new RijndaelManaged();
   
    // Create an encryptor using our key and IV
    ICryptoTransform transform = rijndael.CreateEncryptor(key, iv);
   
    // Create the streams for input and output
    MemoryStream outputStream = new MemoryStream();
    CryptoStream inputStream = new CryptoStream(
        outputStream,
        transform,
        CryptoStreamMode.Write);
   
    // Feed our data into the crypto stream.
    inputStream.Write(clearText, 0, clearText.Length);
   
    // Flush the crypto stream.
    inputStream.FlushFinalBlock();
   
    // And finally return our encrypted data.
    return outputStream.ToArray();
}

Decrypt using the Rijndael algorithm:

static byte[] Decrypt(byte[] cipherText, byte[] key, byte[] iv)
{
    // Create an instance of our encyrption algorithm.
    RijndaelManaged rijndael = new RijndaelManaged();
// Create an decryptor using our key and IV  ;
    ICryptoTransform transform = rijndael.CreateDecryptor(key, iv); 
    // Create the streams for input and output
    MemoryStream outputStream = new MemoryStream();
    CryptoStream inputStream = new CryptoStream(
        outputStream,
        transform,
        CryptoStreamMode.Write);
   
    // Feed our data into the crypto stream.
    inputStream.Write(cipherText, 0, cipher.Length);
       // Flush the crypto stream.
    inputStream.FlushFinalBlock();
   
    // And finally return our decrypted data.
    return outputStream.ToArray();
}
Using Session Keys

Some cryptanalytic attacks are made easier with more data encrypted with a spcific key. The mitigation against this is to use a session key. A session key is used to encrypt a single set of data—for example, a single record in the database, or all messages within a single communication session.

If it’s not feasible to store session keys separately from your data, then you can still use session keys by using master key. The master key is kept in a secure key store, completely separate from the data to be encrypted, or derived from a password entered by the application user. A session key is then used to encrypt the data to be protected. The session key is then encrypted with the master key and stored alongside the data that it applies to, while the master key remains in a separate secure key store.

Ensuring That Data Does not Change

The standard approach for generating an encrypted hash is to create a Message Authentication Code (MAC).

Storing a MAC for encrypted data provides two benefits:

  • You can verify data has not been changed (integrity).
  • You can verify that the data was created by someone who knows the secret key (authenticity).

.NET provides a number of common algorithms for generating a keyed hash, all of which use KeyedHashAlgorithm as their base class. The generally recommended algorithm is HMACSHA256, which a key size of 64 bytes.

Generates a MAC:

static byte[] GenerateMac(byte[] clearText, byte[] key)
{
    HMACSHA256 hmac = new HMACSHA256(key);
    return hmac.ComputeHash(clearText);
} 

Checking a MAC:

static bool IsMacValid(byte[] clearText, byte[] key, byte[] savedMac)
{
    byte[] recalculatedMac = GenerateMac(clearText, key);
   
    bool matched = false;
    if (recalculatedMac.Length == savedMac.Length)
    {
        int i = 0;
        while ((i  <  recalculatedMac.Length)  &  &  (recalculatedMac[i] == savedMac[i]))
        {
            i += 1;
        }
        if (i == recalculatedMac.Length)
        {
            matched = true;
        }
    return (matched); 
}
Putting it All Together

2011-02-14 10 32 04

We want to keep LicenseNumber secure.

2011-02-14 10 37 54

This record format needs to change to support encryption and integrity protection.

You need two master keys, kept safe away from your main database. These the master keys used to encrypt the session key for each record, and the validation key used to generate the MAC.

To add a new record:

  1. Retrieve the master encryption key and the validation key.
  2. Take the clear text values for the record and concatenate them together, converting them into a byte array.
  3. Compute the MAC by using the clear text values and validation key.
  4. Create a session key and initializatio vector from enyptographically secure random data.
  5. Encrypt the LicenseNumber using the session key and initialization vector.
  6. Encrypt the session key with the master encryption key.
  7. Store the PersonIdentifer, FirstName, Surname, the encrypted LicenseNumber, encrypted SessionKey, IV, and MAC in the data store.

To retrieve and validate the record:

  1. Retrieve the master encryption key and validation key.
  2. Load the record form the data store.
  3. Decrypt the session key for the record using the master encryption key.
  4. Decrypt the LicenseNumber using the decrypted session key and the IV from the retrieved record.
  5. Take the clear text values for the record and concatenate them together, converting them into a byte array.
  6. Compute the MAC using the clear text values and validation key, and compare it to the stored MAC. If the MACs do not match, then the data has been changed or corrupted.

Sharing Secrets with Asymmetric Encryption Plain

Using Asymmetric Entryption without Certificates

Encyrpting Data Using The RSA class:

// Create an UTF8 encoding class to parse strings from and to byte arrays
UTF8Encoding encoding = new UTF8Encoding();
   
// Setup the sample text to encrypt and convert it to a byte array.
string clearText = "example";
byte[] clearTextAsBytes = encoding.GetBytes(clearText);
   
// Create a new instance of the RSACryptoServiceProvider
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(1024);
// Export the keys as XML
string publicKeyAsXml = rsa.ToXmlString(false);
string privateKeyAsXml = rsa.ToXmlString(true);
   
// Create a new instance of the RSACryptoServiceProvider
// and load the public key parameters so it can be used 
// to encrypt.
RSACryptoServiceProvider publicKeyRSA = 
    new RSACryptoServiceProvider(1024);
publicKeyRSA.FromXmlString(publicKeyAsXml);
byte[] encryptedData = publicKeyRSA.Encrypt(clearTextAsBytes, true);
   
// Create a new instance of the RSACryptoServiceProvider
// and load the private key parameters so it can be used 
// to decrypt.
   
RSACryptoServiceProvider privateKeyRSA = 
    new RSACryptoServiceProvider();
privateKeyRSA.FromXmlString(privateKeyAsXml);
byte[] unencryptedBytes = privateKeyRSA.Decrypt(encryptedData, true);
// And finally convert it back to a string to prove it works!
string unecnryptedString = 
    Encoding.UTF8.GetString(unencryptedBytes, 0, unencryptedBytes.Length);    

Using Certificates for Asymmetric Entryption

Using asymmetric entryption without certificates has one drawback—you cannot tell from whom the encrypted data has come. This is where certificates, coupled with digital signatures, come into play.

Getting a Certificate

Load a certificate:

X509Store myStore = new X509Store(
    StoreName.My,
    StoreLocation.CurrentUser
    );
   
myStore.Open(OpenFlags.ReadOnly);
   
// Find my certificate
X509Certificate2Collection certificateCollection =
    myStore.Certificates.Find(
        X509FindType.FindBySubjectKeyIdentifier,
        "8a7ec2d153cbb0827ddaabedc729c618f15239c4",
        true);
   
// Retrieve the first certificate in the returned collection
// There will only be one, as the subject key identifier
// should be unique.
X509Certificate2 myCertificate = certificateCollection[0];
// Use this certificate to perform operations
   
myStore.Close(); 
Encrypting Data
static byte[] EncyrptWithCertificate(byte[] clearText, 
    X509Certificate2 certificate)
{
    // Load our clear text into the CMS/PKCS #7 data structure
    ContentInfo contentInfo = new ContentInfo(clearText);
   
    // Create an encrypted envelope for the encrypted data
    EnvelopedCms envelopedCms = new EnvelopedCms(contentInfo);
   
    // Set the certificate that we will encrypt for.
    // Remember we only need a cerificate with the public key
    CmsRecipient recipient = new CmsRecipient(certificate);
   
    // Encrypt it
    envelopedCms.Encrypt(recipient);
   
    // And return the encoded, encrypted data
    return envelopedCms.Encode();
} 
Decrypting Data
static byte[] DecryptWithCertificate(byte[] cipherText)
{
    EnvelopedCms envelopedCms = new EnvelopedCms();
    envelopedCms.Decode(cipherText);
    envelopedCms.Decrypt();
   
    return envelopedCms.ContentInfo.Content;
}
Ensure That Data Does Not Change
static byte[] SignWithCertificate(byte[] clearText, 
    X509Certificate2 certificate)
{
    // Load our clear text into the CMS/PKCS #7 data structure
    ContentInfo contentInfo = new ContentInfo(clearText);
   
    // Set who is signing the data
    CmsSigner signer = new CmsSigner(certificate);
   
    // Create a suitable signed message structure
    SignedCms signedCms = new SignedCms(contentInfo);
   
    // Sign the data
    signedCms.ComputeSignature(signer);
   
    // Return the signed data structure
    return signedCms.Encode();
} 

To check signature:

static bool IsSignatureValid(SignedCms signedMessage)
{
    bool result = false;
    try
    {
        // Set the parameter to true if you want to check
        // certificate revocations.
        signedMessage.CheckSignature(false);
   
        // Perform other checks on the signing certificates
        // as required
        foreach (SignerInfo signerInfo in signedMessage.SignerInfos)
        {
            X509Certificate2 signingCertificate = signerInfo.Certificate;
            // Validate we know the signing cerificate
        }
        result = true;
    }
    catch (CryptographicException)
    {
    }
   
    return result;
}
Allowing Access to a Certificates Private Key

Because a certificate’s private key is extremely secret,  certificates loaded into the machine store do not, by default, allow processes to access them, you must set the permission on the certificate to allow this if you want to programmatically encrypt using them.

Creating Test Certificates with MAKECERT

The Visual Studio SDK comes with a utility, MAKECERT, that enables you to create certificates for use during development and testing.

Putting it All Together

Folloing code waps a piece of clear text data into a CMS envelope, signs it using a local certificate with a subject name of Barry_Dorrans, and then encrypts it against a public key certificate loaded from a file, myserver.cer. The data is then decrypted, the CMS envelope is re-created, and the signatures check. A list of the signing certificate subject name is created, and finally, the clear text is returned for further processing.

Signing, Encrypting, Unencrypting, and Verifying Signatures:

// Create an UTF8 encoding class to parse strings
// from and to byte arrays
UTF8Encoding encoding = new UTF8Encoding();
   
// Setup the sample text to encrypt and 
// convert it to a byte array.
string clearText = "example";
byte[] clearTextAsBytes = encoding.GetBytes(clearText);
   
// Get the certificate we are going to encrypt for.
// As well as using the cerificate store you can also load 
// public key certificates from files, as demonstrated
// below.
X509Certificate2 serverPublicKeyCertificate = 
    LoadCertificateFromFile("myserver.cer");
// Load the certificate we will be signing the data with
// to provide data integrity and non-repudiation.
X509Certificate2 signingCertificate =
    GetCertificateBySubjectName("Barry_Dorrans");
   
// Create our signed data envelope
byte[] signedClearText = 
    SignData(clearTextAsBytes, signingCertificate);
   
// Now encrypt it
byte[] encryptedAndSignedData = 
    EncryptWithCertificate(
        signedClearText,
        serverPublicKeyCertificate);
   
// Then you would send the data to the receiving system.
   
// Now you're on the receiving system.
// As the envelope contains a reference to the certificate
// against which the data was encrypted it will get loaded
// automatically if it is available.
byte[] encodedUnencryptedCms = 
    DecryptWithCertificate(encryptedAndSignedData);
   
// Now you need to validate the signature
// Create a list suitable for holding the signing subjects
List < string >  signingSubjects = new List < string > ();
byte[] receivedClearText = 
    ValidateSignatureAndExtractContent(
        encodedUnencryptedCms, 
        signingSubjects);
   
// And finally convert it back to a string to prove it works!
string unecnryptedString =
    Encoding.UTF8.GetString(receivedClearText, 0, 
        receivedClearText.Length);
   
static byte[] SignData(byte[] clearText, 
    X509Certificate2 signingCertificate)
{
    // Load our clear text into the CMS/PKCS #7 data structure
    ContentInfo contentInfo = new ContentInfo(clearText);
   
    // Set who is signing the data
    CmsSigner signer = new CmsSigner(signingCertificate);
   
    // Create a suitable signed message structure
    SignedCms signedCms = new SignedCms(contentInfo);
       // Sign the data
    signedCms.ComputeSignature(signer);
   
    // Return the signed data structure
    return signedCms.Encode();            
}
   
static byte[] ValidateSignatureAndExtractContent(
    byte[] signedCmsAsBytes,
    ICollection < string >   signingSubjects)
{            
    SignedCms signedCms = new SignedCms();
    signedCms.Decode(signedCmsAsBytes);
   
    signedCms.CheckSignature(true);
    signingSubjects.Clear();
   
    foreach(SignerInfo signerInfo in signedCms.SignerInfos)
    {
        // Reconstruct the signing certificate public parts
        X509Certificate2 signingCertificate = 
            signerInfo.Certificate;
        // And extract the subject
        signingSubjects.Add(signingCertificate.Subject);
    }
   
    return signedCms.ContentInfo.Content;
}
   
static byte[] EncyrptWithCertificate(byte[] clearText,
    X509Certificate2 certificate)
{
    // Load our clear text into the CMS/PKCS #7 data structure
    ContentInfo contentInfo = new ContentInfo(clearText);
   
    // Create an encrypted envelope for the encrypted data
    EnvelopedCms envelopedCms = new EnvelopedCms(contentInfo);
   
    // Set the certificate that we will encrypt for.
    // Remember we only need a cerificate with the public key
    CmsRecipient recipient = new CmsRecipient(certificate);
   
    // Encrypt it
    envelopedCms.Encrypt(recipient);
   
    // And return the encoded, encrypted data
    return envelopedCms.Encode();
}

static byte[] DecryptWithCertificate(byte[] cipherText)
{
    EnvelopedCms envelopedCms = new EnvelopedCms();
    // Reconstruct the envelope and decrypt.
    envelopedCms.Decode(cipherText);
    envelopedCms.Decrypt();
   
    return envelopedCms.ContentInfo.Content;
}
   
static X509Certificate2 LoadCertificateFromFile(string fileName)
{
    X509Certificate2 certificate = new X509Certificate2();
    byte[] cerFileContents = ReadBinaryFile(fileName);
    certificate.Import(cerFileContents);
    return certificate;
}
static X509Certificate2 GetCertificateBySubjectName(
    string subjectName)
{
    X509Store store = null;
    try
    {
        store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        store.Open(OpenFlags.ReadOnly);
        X509Certificate2Collection certificates =
                store.Certificates.Find(X509FindType.FindBySubjectName,
                subjectName, true);
        return certificates[0];
    }
    // finally 
    {
        if (store != null)
            store.Close();
    }
}
   
static byte[] ReadBinaryFile(string fileName)
{
    FileStream f = new FileStream(fileName, FileMode.Open, FileAccess.Read);
    int size = (int)f.Length;
    byte[] data = new byte[size];
    size = f.Read(data, 0, size);
    f.Close();
    return data;
}   

Using the Windows DPAPI

The Windows Data Protection API (DPAPI) is a core system service proviced by Windows that is managed by the most secure process in the operating system: the Local Security Authority (LSA).

Encrypt and decrypt using DPAPI:

// This is an example of entropy. In a real application
// it should be a cryptographically secure array of random data.
private static byte[] entropy = {1, 3, 5, 7, 9, 11, 15, 17, 19};
   
static byte[] EncryptUsingDPAPI(byte[] clearText)
{
    return ProtectedData.Protect(
        clearText, 
        entropy, 
        DataProtectionScope.LocalMachine);
}
   
static byte[] DecryptUsingDPAPI(byte[] encryptedText)
{
    return ProtectedData.Unprotect(
        encryptedText,
        entropy,
        DataProtectionScope.LocalMachine);
} 

A Checklist for Encryption

  • Choose an appropriate method for your situation.—If your application must encrypt and decrypt the same data, choose a symmetric algorithm. If your application talks to an external system, choose asymmetric algorithm.
  • User the appropriate integrity checks for your data.—Encryption is not enough when to detect changes in data. Even unencrypted data may need a mechanism for detecting changes. Use hashing, MACs or certificate signing to give you the capability to check when data has changed.
  • Choose your algorithm carefully.—Some algorithms are now considered broken or easy to break. Use the recommended algorithms for your situation—SHA256 or SHA512 for hasing, and AES for symmetric encryption.
  • Protect your keys.—If your keys are compromised, so is your data. Store your key separately from your encrypted data, and tightly control access to them. Ensure that you have a secure, separate backup of your keys and certificates.
  • Plan ahead for algorithm change.—As time passes, algorithms are proven unsafe. Plan ahead, and consider how you could change your algorithm, and how you could support older data.

posted on 2011-02-12 16:16  9527  阅读(1138)  评论(0编辑  收藏  举报