Prior to version 4.0, the .NET Framework incorporated two complementary security models that addressed user and code security: role-based security (RBS) and code access security (CAS). With version 4.0, CAS has been deprecated. The previous edition of this book included a number of CAS recipes (11-1 through to 11-7), and we have included them in this new version because of the number of developers still using .NET 3.5 or earlier, where CAS still has a significant function. The C# compiler will show deprecation warnings if you use CAS in a project. You can prevent these errors by targeting your project at an earlier version of the .NET Framework on the Application tab. You can enable CAS in your .NET 4.0 projects with the NetFx40_LegacySecurityPolicy
configuration element in the app.config
file—for example:
<?xml version="1.0"?> <configuration> <runtime> <NetFx40_LegacySecurityPolicy enabled="true"/> </runtime> </configuration>
RBS remains current in .NET 4.0 and allows you to make runtime decisions based on the identity and roles of the user on whose behalf an application is running. On the Windows operating system, this equates to making decisions based on the Windows username and the Windows groups to which that user belongs. However, RBS provides a generic security mechanism that is independent of the underlying operating system, allowing you (with some development) to integrate with any user account system.
An important aspect of the security features provided by the .NET Framework is cryptography. Cryptography is one of the most complex aspects of software development that any developer will use. The theory of modern cryptographic techniques is extremely difficult to understand and requires a level of mathematical knowledge that relatively few people have or need. Fortunately, the Microsoft .NET Framework class library provides easy-to-use implementations of the most commonly used cryptographic techniques and support for the most popular and well-understood algorithms.
This chapter provides a wide variety of recipes that cover some of the more commonly used security capabilities provided by the .NET Framework. As you read the recipes in this chapter and think about how to apply the techniques to your own code, keep in mind that individual security features are rarely effective when implemented in isolation. In particular, cryptography does not equal security; the use of cryptography is merely one small element of creating a secure solution.
The recipes in this chapter describe how to do the following:
Develop strongly named assemblies that can still be called by partially trusted code (recipe 11-1)
Configure the .NET Framework security policy to turn off CAS completely or turn off only execution permission checks (recipes 11-2 and 11-3)
Request specific code access permissions for your assemblies, determine at runtime what permissions the current assembly has, and inspect third-party assemblies to determine what permissions they need in order to run correctly (recipes 11-4, 11-5, 11-6, and 11-7)
Control inheritance and member overrides using CAS (recipe 11-8)
Inspect the evidence presented by an assembly to the runtime when the assembly is loaded (recipe 11-9)
Integrate with Windows security to determine if a user is a member of a specific Windows group, restrict which users can execute your code, and impersonate other Windows users (recipes 11-10, 11-11, and 11-12)
Generate random numbers that are nondeterministic and are suitable for use in security-sensitive applications (recipe 11-13)
Use hash codes and keyed hash codes to store user passwords and determine if files have changed (recipes 11-14, 11-15, 11-16, and 11-17)
Use encryption to protect sensitive data both in memory and when it is stored to disk (recipes 11-18 and 11-19)
You need to write a shared assembly that is accessible to partially trusted code. (By default, the runtime does not allow partially trusted code to access the types and members contained in a strongly named assembly.)
Apply the assembly-level attribute System.Security.AllowPartiallyTrustedCallersAttribute
to your shared assembly.
CAS is deprecated in .NET 4.0.
To minimize the security risks posed by malicious code, the runtime does not allow assemblies granted only partial trust to access strongly named assemblies. This restriction dramatically reduces the opportunity for malicious code to attack your system, but the reasoning behind such a heavy-handed approach requires some explanation.
Assemblies that contain important functionality that is shared between multiple applications are usually strongly named and are often installed in the Global Assembly Cache (GAC). This is particularly true of the assemblies that constitute the .NET Framework class library. Other strongly named assemblies from well-known and widely distributed products are in the GAC and accessible to managed applications. The high chance that certain assemblies will be present in the GAC, their easy accessibility, and their importance to many different applications make strongly named assemblies the most likely target for any type of subversive activity by malicious managed code.
Generally, the code most likely to be malicious is that which is loaded from remote locations over which you have little or no control (such as over the Internet). Under the default security policy of the .NET Framework, all code run from the local machine has full trust, whereas code loaded from remote locations has only partial trust. Stopping partially trusted code from accessing strongly named assemblies means that partially trusted code has no opportunity to use the features of the assembly for malicious purposes, and cannot probe and explore the assembly to find exploitable holes. Of course, this theory hinges on the assumption that you correctly administer your security policy. If you simply assign all code full trust, not only will any assembly be able to access your strongly named assembly, but the code will also be able to access all of the functionality of the .NET Framework and even Win32 or any COM object through P/Invoke and COM Interop. That would be a security disaster!
If you design, implement, and test your shared assembly correctly using CAS to restrict access to important members, you do not need to impose a blanket restriction to prevent partially trusted code from using your assembly. However, for an assembly of any significance, it's impossible to prove there are no security holes that malicious code can exploit. Therefore, you should carefully consider the need to allow partially trusted code to access your strongly named assembly before applying AllowPartiallyTrustedCallersAttribute
. However, you might have no choice. If you are exposing public classes that provide events, you must apply this attribute. If you do not, an assembly that is not strongly named will be allowed to register a handler for one of your events, but when it is called, a security exception will be thrown. Code in an assembly that is not strongly named is not allowed to call code in a strongly named assembly.
The runtime stops partially trusted code from accessing strongly named assemblies by placing an implicit LinkDemand
for the FullTrust
permission set on every public and protected member of every publicly accessible type defined in the assembly. This means that only assemblies granted the permissions equivalent to the FullTrust
permission set are able to access the types and members from the strongly named assembly. Applying AllowPartiallyTrustedCallersAttribute
to your strongly named assembly signals the runtime to not enforce the LinkDemand
on the contained types and members.
The runtime is responsible for enforcing the implicit LinkDemand
security actions required to protect strongly named assemblies. The C# assembler does not generate declarative LinkDemand
statements at compile time.
The following code fragment shows the application of the attribute AllowPartiallyTrustedCallersAttribute
. Notice that you must prefix the attribute with assembly:
to signal to the compiler that the target of the attribute is the assembly (also called a global attribute). In addition, you do not need to include the Attribute
part of the attribute name, although you can if you want to add it. Because you target the assembly, the attribute must be positioned after any top-level using
statements, but before any namespace or type declarations.
using System.Security; [assembly:AllowPartiallyTrustedCallers] namespace Apress.VisualCSharpRecipes.Chapter11 { public class Recipe11-01 { // Implementation code . . . } }
It's common practice to contain all global attributes in a file separate from the rest of your application code. Microsoft Visual Studio uses this approach, creating a file named AssemblyInfo.cs
to contain all global attributes.
If, after applying AllowPartiallyTrustedCallersAttribute
to your assembly, you want to restrict partially trusted code from calling only specific members, you should implement a LinkDemand
for the FullTrust
permission set on the necessary members, as shown in the following code fragment:
[System.Security.Permissions.PermissionSetAttribute (System.Security.Permissions.SecurityAction.LinkDemand, Name="FullTrust")] public void SomeMethod() { // Method code . . . }
Use the Code Access Security Policy tool (Caspol.exe
) and execute the command caspol -s off
from the command line to temporarily disable code access security checks.
This recipe only applies to .NET version 3.5 and earlier.
Although CAS was implemented with performance in mind and has been used prudently throughout the .NET class library, some overhead is associated with each security demand and resulting stack walk that the runtime must execute to check every caller in the chain of execution.
You can temporarily disable CAS and remove the overhead and possible interference caused by code-level security checks. Turning off CAS has the effect of giving all code the ability to perform any action supported by the .NET Framework (equivalent to the FullTrust
permission set). This includes the ability to load other code, call native libraries, and use pointers to access memory directly.
Caspol.exe
is a utility provided with the .NET Framework that allows you to configure all aspects of your code access security policy from the command line. When you enter the command caspol -s off
from the command line, you will see the following message indicating that CAS has been temporarily disabled:
Microsoft (r) .NET Framework CasPol 2.0.50727.42 Copyright (c) Microsoft Corporation. Al rights reserved. CAS enforcement is being turned off temporarily. Press <enter> when you want to restore the setting back on.
As the message states, CAS enforcement is off until you press Enter, or until the console in which Caspol.exe
is running terminates.
In code, set the property CheckExecutionRights
of the class System.Security.SecurityManager
to false
and persist the change by calling SecurityManager.SavePolicy
. Alternatively, use the Code Access Security Policy tool (Caspol.exe
), and execute the command caspol -e off
from the command line.
This recipe only applies to .NET version 3.5 and earlier.
As the runtime loads each assembly, it ensures that the assembly's grant set (the permissions assigned to the assembly based on the security policy) includes the Execution
element of SecurityPermission
. The runtime implements a lazy policy resolution process, meaning that the grant set of an assembly is not calculated until the first time a security demand is made against the assembly. Not only does execution permission checking force the runtime to check that every assembly has the execution permission, but it also indirectly causes policy resolution for every assembly loaded, effectively negating the benefits of lazy policy resolution. These factors can introduce a noticeable delay as assemblies are loaded, especially when the runtime loads a number of assemblies together, as it does at application startup.
In many situations, simply allowing code to load and run is not a significant risk, as long as all other important operations and resources are correctly secured using CAS and operating system security. The SecurityManager
class contains a set of static methods that provide access to critical security functionality and data. This includes the CheckExecutionRights
property, which turns on and off execution permission checks.
To modify the value of CheckExecutionRights
, your code must have the ControlPolicy
element of SecurityPermission
. The change will affect the current process immediately, allowing you to load assemblies at runtime without the runtime checking them for execution permission. However, the change will not affect other existing processes. You must call the SavePolicy
method to persist the change to the Windows registry for it to affect new processes.
The following example contains two methods (ExecutionCheckOn
and ExecutionCheckOff
) that demonstrate the code required to turn execution permission checks on and off and persist the configuration change. You may need to run the example with administrator privileges.
using System; using System.Security; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_03 { // A method to turn on execution permission checking // and persist the change. public void ExecutionCheckOn() { // Turn on execution permission checks. SecurityManager.CheckExecutionRights = true; // Persist the configuration change. SecurityManager.SavePolicy(); } // A method to turn off execution permission checking // and persist the change. public void ExecutionCheckOff() { // Turn off execution permission checks. SecurityManager.CheckExecutionRights = false; // Persist the configuration change. SecurityManager.SavePolicy(); } } }
The .NET runtime allows you to turn off the automatic checks for execution permissions from within code or by using Caspol.exe
. When you enter the command caspol -e off
or its counterpart caspol -e on
from the command line, the Caspol.exe
utility actually sets the CheckExecutionRights
property of the SecurityManager
class before calling SecurityManager.SavePolicy
.
You need to ensure that the runtime grants your assembly those code access permissions that are critical to the successful operation of your application.
In your assembly, use permission requests to specify the code access permissions that your assembly must have. You declare permission requests using assembly-level code access permission attributes.
CAS is deprecated in .NET 4.0.
The name permission request is a little misleading given that the runtime will never grant permissions to an assembly unless security policy dictates that the assembly should have those permissions. However, naming aside, permission requests serve an essential purpose, and although the way the runtime handles permission requests might initially seem strange, the nature of CAS does not allow for any obvious alternative.
Permission requests identify permissions that your code must have to function. For example, if you wrote a movie player that your customers could use to download and view movies from your web server, it would be disastrous if the user's security policy did not allow your player to open a network connection to your media server. Your player would load and run, but as soon as the user tried to connect to your server to play a movie, the application would crash with the exception System.Security.SecurityException
. The solution is to include in your assembly a permission request for the code access permission required to open a network connection to your server (System.Net.WebPermission
or System.Net.SocketPermission
, depending on the type of connection you need to open).
The runtime honors permission requests using the premise that it's better that your code never load than to load and fail sometime later when it tries to perform an action that it does not have permission to perform. Therefore, if after security policy resolution the runtime determines that the grant set of your assembly does not satisfy the assembly's permission requests, the runtime will fail to load the assembly and will instead throw the exception System.Security.Policy.PolicyException
. Since your own code failed to load, the runtime will handle this security exception during the assembly loading and transform it into a System.IO.FileLoadException
exception that will terminate your program.
When you try to load an assembly from within code (either automatically or manually), and the loaded assembly contains permission requests that the security policy does not satisfy, the method you use to load the assembly will throw a PolicyException
exception, which you must handle appropriately.
To declare a permission request, you must use the attribute counterpart of the code access permission that you need to request. All code access permissions have an attribute counterpart that you use to construct declarative security statements, including permission requests. For example, the attribute counterpart of SocketPermission
is SocketPermissionAttribute
, and the attribute counterpart of WebPermission
is WebPermissionAttribute
. All permissions and their attribute counterparts follow the same naming convention and are members of the same namespace.
When making a permission request, it's important to remember the following:
You must declare the permission request after any top-level using
statements but before any namespace or type declarations.
The attribute must target the assembly, so you must prefix the attribute name with assembly
.
You do not need to include the Attribute
portion of an attribute's name, although you can.
You must specify SecurityAction.RequestMinimum
as the first positional argument of the attribute. This value identifies the statement as a permission request.
You must configure the attribute to represent the code access permission you want to request using the attribute's properties. Refer to the .NET Framework SDK documentation for details of the properties implemented by each code access security attribute.
The permission request statements do not end with a semicolon (;
).
To make more than one permission request, simply include multiple permission request statements.
The following example is a console application that includes two permission requests: one for SocketPermission
and the other for SecurityPermission
. If you try to execute the PermissionRequestExample
application and your security policy does not grant the assembly the requested permissions, you will get a PolicyException
, and the application will not execute. Using the default security policy, this will happen if you run the assembly from a network share, because assemblies loaded from the intranet zone are not granted SocketPermission
.
using System; using System.Net; using System.Security.Permissions; // Permission request for a SocketPermission that allows the code to open // a TCP connection to the specified host and port. [assembly:SocketPermission(SecurityAction.RequestMinimum, Access = "Connect", Host = "www.fabrikam.com", Port = "3538", Transport = "Tcp")] // Permission request for the UnmanagedCode element of SecurityPermission, // which controls the code's ability to execute unmanaged code. [assembly:SecurityPermission(SecurityAction.RequestMinimum, UnmanagedCode = true)] namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_04 { public static void Main() { // Do something . . .
// Wait to continue. Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
You need to restrict the code access permissions granted to your assembly, ensuring that people and other software can never use your code as a mechanism through which to perform undesirable or malicious actions.
Use declarative security statements to specify optional permission requests and permission refusal requests in your assembly. Optional permission requests define the maximum set of permissions that the runtime will grant to your assembly. Permission refusal requests specify particular permissions that the runtime should not grant to your assembly.
CAS is deprecated in .NET 4.0.
In the interest of security, it's ideal if your code has only those code access permissions required to perform its function. This minimizes the opportunities for people and other code to use your code to carry out malicious or undesirable actions. The problem is that the runtime resolves an assembly's permissions using security policy, which a user or an administrator configures. Security policy could be different in every location where your application is run, and you have no control over what permissions the security policy assigns to your code.
Although you cannot control security policy in all locations where your code runs, the .NET Framework provides two mechanisms through which you can reject permissions granted to your assembly:
Refuse request: This allows you to identify specific permissions that you do not want the runtime to grant to your assembly. After policy resolution, if the final grant set of an assembly contains any permission specified in a refuse request, the runtime removes that permission.
Optional permission request: This defines the maximum set of permissions that the runtime can grant to your assembly. If the final grant set of an assembly contains any permissions other than those specified in the optional permission request, the runtime removes those permissions. Unlike as with a minimum permission request (discussed in recipe 11-4), the runtime will not refuse to load your assembly if it cannot grant all of the permissions specified in the optional request.
You can think of a refuse request and an optional request as alternative ways to achieve the same result. The approach you use depends on how many permissions you want to reject. If you want to reject only a handful of permissions, a refuse request is easier to code. However, if you want to reject a large number of permissions, it's easier to code an optional request for the few permissions you want, which will automatically reject the rest.
You include optional and refuse requests in your code using declarative security statements with the same syntax as the minimum permission requests discussed in recipe 11-4. The only difference is the value of the System.Security.Permissions.SecurityAction
that you pass to the permission attribute's constructor. Use SecurityAction.RequestOptional
to declare an optional permission request and SecurityAction.RequestRefuse
to declare a refuse request. As with minimal permission requests, you must declare optional and refuse requests as global attributes by beginning the permission attribute name with the prefix assembly
. In addition, all requests must appear after any top-level using
statements but before any namespace or type declarations.
The code shown here demonstrates an optional permission request for the Internet permission set. The Internet permission set is a named permission set defined by the default security policy. When the runtime loads the example, it will not grant the assembly any permission that is not included within the Internet permission set. (Consult the .NET Framework SDK documentation for details of the permissions contained in the Internet permission set.)
using System.Security.Permissions; [assembly:PermissionSet(SecurityAction.RequestOptional, Name = "Internet")] namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_05_OptionalRequest { // Class implementation . . . } }
In contrast to the preceding example, the following example uses a refuse request to single out the permission System.Security.Permissions.FileIOPermission
—representing write access to the C:
drive—for refusal.
using System.Security.Permissions; [assembly:FileIOPermission(SecurityAction.RequestRefuse, Write = @"C:")] namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_05_RefuseRequest { // Class implementation . . . } }
Use the Permissions Calculator (Permcalc.exe
) supplied with the .NET Framework SDK version 3.5 or earlier.
CAS is deprecated in .NET 4.0.
To configure security policy correctly, you need to know the code access permission requirements of the assemblies you intend to run. This is true of both executable assemblies and libraries that you access from your own applications. With libraries, it's also important to know which permissions the assembly refuses so that you do not try to use the library to perform a restricted action, which would result in a System.Security.SecurityException
exception.
The Permissions Calculator (Permcalc.exe
) supplied with the .NET Framework SDK version overcomes this limitation. Permcalc.exe
walks through an assembly and provides an estimate of the permissions the assembly requires to run, regardless of whether they are declarative or imperative.
The following example shows a class that declares a minimum, optional, and refusal request, as well as a number of imperative security demands:
using System; using System.Net; using System.Security.Permissions; // Minimum permission request for SocketPermission. [assembly: SocketPermission(SecurityAction.RequestMinimum, Unrestricted = true)] // Optional permission request for IsolatedStorageFilePermission. [assembly: IsolatedStorageFilePermission(SecurityAction.RequestOptional, Unrestricted = true)] // Refuse request for ReflectionPermission. [assembly: ReflectionPermission(SecurityAction.RequestRefuse, Unrestricted = true)] namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_06 { public static void Main() { // Create and configure a FileIOPermission object that represents // write access to the C:Data folder. FileIOPermission fileIOPerm = new FileIOPermission(FileIOPermissionAccess.Write, @"C:Data"); // Make the demand. fileIOPerm.Demand(); // Do something . . . // Wait to continue. Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Executing the command permview Recipe11-06.exe
will generate the following output. Although this output is not particularly user-friendly, you can decipher it to determine the declarative permission requests made by an assembly. Each of the three types of permission requests—minimum, optional, and refused—is listed under a separate heading and is structured as the XML representation of a System.Security.PermissionSet
object.
Microsoft (R) .NET Framework Permission Request Viewer. Version 1.1.4322.573 Copyright (C) Microsoft Corporation 1998-2002. All rights reserved. minimal permission set: <PermissionSet class=System.Security.PermissionSet" version="1"> <IPermission class="System.Net.SocketPermission, System, Version=1. 0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version=" 1" Unrestricted="true"/> </PermissionSet> optional permission set: <PermissionSet class="System.Security.PermissionSet" version="1"> <IPermission class="System.Security.Permissions.IsolatedStorageFilePermission, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c5 61934e089" version="1" Unrestricted="true"/> </PermissionSet>
refused permission set: <PermissionSet class="System.Security.PermissionSet" version="1"> <IPermission class="System.Security.Permissions.ReflectionPermission, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c5 61934e089" version="1" Unrestricted="true"/> </PermissionSet>
Executing the command permcalc -sandbox Recipe11-06.exe
will generate a file named sandbox.PermCalc.xml
that contains XML representations of the permissions required by the assembly. Where the exact requirements of a permission cannot be determined (because it is based on runtime data), Permcalc.exe
reports that unrestricted permissions of that type are required. You can instead default to the Internet zone permissions using the -Internet
flag. Here are the contents of sandbox.PermCalc.xml
when run against the sample code:
<?xml version="1.0"?> <Sandbox> <PermissionSet version="1" class="System.Security.PermissionSet"> <IPermission Write="C:Data" version="1" class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> <IPermission version="1" class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" Flags="Execution" /> <IPermission version="1" class="System.Security.Permissions.UIPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" Unrestricted="true" />
<IPermission version="1" class="System.Net.SocketPermission, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" Unrestricted="true" /> </PermissionSet> </Sandbox>
Instantiate and configure the permission you want to test for, and then pass it as an argument to the static method IsGranted
of the class System.Security.SecurityManager
.
CAS is deprecated in .NET 4.0.
Using minimum permission requests, you can ensure that the runtime grants your assembly a specified set of permissions. As a result, when your code is running, you can safely assume that it has the requested minimum permissions. However, you might want to implement opportunistic functionality that your application offers only if the runtime grants your assembly appropriate permissions. This approach is partially formalized using optional permission requests, which allow you to define a set of permissions that your code could use if the security policy granted them, but are not essential for the successful operation of your code. (Recipe 11-5 provides more details on using optional permission requests.)
The problem with optional permission requests is that the runtime has no ability to communicate to your assembly which of the requested optional permissions it has granted. You can try to use a protected operation and fail gracefully if the call results in the exception System.Security.SecurityException
. However, it's more efficient to determine in advance whether you have the necessary permissions. You can then build logic into your code to avoid invoking secured members that will cause stack walks and raise security exceptions.
IsGranted
checks the grant set only of the calling assembly. It does not do a full stack walk to evaluate the grant set of other assemblies on the call stack.
The following example demonstrates how to use the IsGranted
method to determine if the assembly has write permission to the directory C:Data
. You could make such a call each time you needed to test for the permission, but it's more efficient to use the returned Boolean value to set a configuration flag indicating whether to allow users to save files.
using System.Security; using System.Security.Permissions; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_07 { // Define a variable to indicate whether the assembly has write // access to the C:Data folder. private bool canWrite = false; public Recipe11_07() { // Create and configure a FileIOPermission object that represents // write access to the C:Data folder. FileIOPermission fileIOPerm = new FileIOPermission(FileIOPermissionAccess.Write, @"C:Data"); // Test if the current assembly has the specified permission. canWrite = SecurityManager.IsGranted(fileIOPerm); } } }
You need to control what code can extend your classes through inheritance and which class members a derived class can override.
Use declarative security statements to apply SecurityAction.InheritanceDemand
to the declarations of the classes and members that you need to protect.
Language modifiers such as sealed, public, private
, and virtual
give you a level of control over the ability of classes to inherit from your class and override its members. However, these modifiers are inflexible, providing no selectivity in restricting what code can extend a class or override its members. For example, you might want to allow only code written by your company or department to extend business-critical classes. By applying an InheritanceDemand
attribute to your class or member declaration, you can specify runtime permissions that a class must have to extend your class or override particular members. Remember that the permissions of a class are the permissions of the assembly in which the class is declared.
Although you can demand any permission or permission set in your InheritanceDemand
, it's more common to demand identity permissions. Identity permissions represent evidence presented to the runtime by an assembly. If an assembly presents certain types of evidence at load time, the runtime will automatically assign the assembly the appropriate identity permission. Identity permissions allow you to use regular imperative and declarative security statements to base security decisions directly on code identity, without the need to evaluate evidence objects directly. Table 11-1 lists the type of identity permission generated for each type of evidence. (Evidence types are members of the System.Security.Policy
namespace, and identity permission types are members of the System.Security.Permissions
namespace.)
Table 11.1. Evidence Classes That Generate Identity Permissions
Evidence Class | Identity Permission |
---|---|
| None |
| None |
|
|
|
|
|
|
|
|
|
|
The runtime assigns identity permissions to an assembly based on the evidence presented by the assembly. You cannot assign additional identity permissions to an assembly through the configuration of security policy.
You must use declarative security syntax to implement an InheritanceDemand
, and so you must use the attribute counterpart of the permission class that you want to demand. All permission classes, including InheritanceDemand
, have an attribute counterpart that you use to construct declarative security statements. For example, the attribute counterpart of PublisherIdentityPermission
is PublisherIdentityPermissionAttribute
, and the attribute counterpart of StrongNameIdentityPermission
is StrongNameIdentityPermissionAttribute
. All permissions and their attribute counterparts follow the same naming convention and are members of the same namespace.
To control which code can extend your class, apply the InheritanceDemand
to the class declaration using one of the permissions listed in Table 11-1. To control which code can override specific members of a class, apply the InheritanceDemand
to the member declaration.
The following example demonstrates the use of an InheritanceDemand
attribute on both a class and a method. Applying a PublisherIdentityPermissionAttribute
to the Recipe11_08
class means that only classes in assemblies signed by the publisher certificate contained in the pubcert.cer
file (or assemblies granted FullTrust
) can extend the class. The contents of the pubcert.cer
file are read at compile time, and the necessary certificate information is built into the assembly metadata. To demonstrate that other permissions can also be used with an InheritanceDemand
, the PermissionSetAttribute
is used to allow only classes granted the FullTrust
permission set to override the method SomeProtectedMethod
.
using System.Security.Permissions; namespace Apress.VisualCSharpRecipes.Chapter11 { [PublisherIdentityPermission(SecurityAction.InheritanceDemand, CertFile = "pubcert.cer")] public class Recipe11_08 { [PermissionSet(SecurityAction.InheritanceDemand, Name="FullTrust")] public void SomeProtectedMethod () { // Method implementation . . . } } }
Obtain a System.Reflection.Assembly
object that represents the assembly in which you are interested. Get the System.Security.Policy.Evidence
collection from the Evidence
property of the Assembly
object, and access the contained evidence objects using the GetEnumerator, GetHostEnumerator
, or GetAssemblyEnumerator
method of the Evidence
class.
The Evidence
class represents a collection of evidence objects. The read-only Evidence
property of the Assembly
class returns an Evidence
collection object that contains all of the evidence objects that the runtime assigned to the assembly as the assembly was loaded.
The Evidence
class actually contains two collections, representing different types of evidence:
Host evidence includes those evidence objects assigned to the assembly by the runtime or the trusted code that loaded the assembly.
Assembly evidence represents custom evidence objects embedded into the assembly at build time.
The Evidence
class implements three methods for enumerating the evidence objects it contains: GetEnumerator, GetHostEnumerator
, and GetAssemblyEnumerator
. The GetHostEnumerator
and GetAssemblyEnumerator
methods return a System.Collections.IEnumerator
instance that enumerates only those evidence objects from the appropriate collection. The GetEnumerator
method returns an IEnumerator
instance that enumerates all of the evidence objects contained in the Evidence
collection.
Evidence classes do not extend a standard base class or implement a standard interface. Therefore, when working with evidence programmatically, you need to test the type of each object and know what particular types you are seeking. (See recipe 3-11 for details on how to test the type of an object at runtime.)
The following example demonstrates how to display the host and assembly evidence of an assembly to the console. The example relies on the fact that all standard evidence classes override the Object.ToString
method to display a useful representation of the evidence object's state. Although interesting, this example does not always show the evidence that an assembly would have when loaded from within your program. The runtime host (such as the Microsoft ASP.NET or Internet Explorer runtime host) is free to assign additional host evidence as it loads an assembly.
using System; using System.Reflection; using System.Collections; using System.Security.Policy; namespace Apress.VisualCSharpRecipes.Chapter11 { public class Recipe11_09 { public static void Main(string[] args) { // Load the specified assembly. Assembly a = Assembly.LoadFrom(args[0]); // Get the Evidence collection from the // loaded assembly. Evidence e = a.Evidence; // Display the host evidence. IEnumerator x = e.GetHostEnumerator(); Console.WriteLine("HOST EVIDENCE COLLECTION:"); while(x.MoveNext()) { Console.WriteLine(x.Current.ToString()); Console.WriteLine("Press Enter to see next evidence."); Console.ReadLine(); } // Display the assembly evidence. x = e.GetAssemblyEnumerator(); Console.WriteLine("ASSEMBLY EVIDENCE COLLECTION:"); while(x.MoveNext()) { Console.WriteLine(x.Current.ToString()); Console.WriteLine("Press Enter to see next evidence."); Console.ReadLine(); } // Wait to continue. Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
You need to determine if the current user of your application is a member of a specific Windows user group.
Obtain a System.Security.Principal.WindowsIdentity
object representing the current Windows user by calling the static method WindowsIdentity.GetCurrent
. Create a System.Security.Principal.WindowsPrincipal
class using the WindowsIdentity
class, and then call the method IsInRole
of the WindowsPrincipal
object.
The RBS mechanism of the .NET Framework abstracts the user-based security features of the underlying operating system through the following two key interfaces:
The System.Security.Principal.IIdentity
interface, which represents the entity on whose behalf code is running; for example, a user or service account.
The System.Security.Principal.IPrincipal
interface, which represents the entity's IIdentity
and the set of roles to which the entity belongs. A role is simply a categorization used to group entities with similar security capabilities, such as a Windows user group.
To integrate RBS with Windows user security, the .NET Framework provides the following two Windows-specific classes that implement the IIdentity
and IPrincipal
interfaces:
System.Security.Principal.WindowsIdentity
, which implements the IIdentity
interface and represents a Windows user.
System.Security.Principal.WindowsPrincipal
, which implements IPrincipal
and represents the set of Windows groups to which the user belongs.
Because .NET RBS is a generic solution designed to be platform-independent, you have no access to the features and capabilities of the Windows user account through the IIdentity
and IPrincipal
interfaces, and you must frequently use the WindowsIdentity
and WindowsPrincipal
objects directly.
To determine if the current user is a member of a specific Windows group, you must first call the static method WindowsIdentity.GetCurrent
. The GetCurrent
method returns a WindowsIdentity
object that represents the Windows user on whose behalf the current thread is running. An overload of the GetCurrent
method takes a bool
argument and allows you to control what is returned by GetCurrent
if the current thread is impersonating a user different from the one associated with the process. If the argument is true
, then GetCurrent
returns a WindowsIdentity
representing the impersonated user, and it returns null
if the thread is not impersonating a user. If the argument is false
, then GetCurrent
returns the WindowsIdentity
of the thread if it is not impersonating a user, and it returns the WindowsIdentity
of the process if the thread is currently impersonating a user.
The WindowsIdentity
class provides overloaded constructors that, when running on Microsoft Windows Server 2003 or later platforms, allow you to obtain a WindowsIdentity
object representing a named user. You can use this WindowsIdentity
object and the process described in this recipe to determine whether that user is a member of a specific Windows group. If you try to use one of these constructors when running on an earlier version of Windows, the WindowsIdentity
constructor will throw an exception. On Windows platforms preceding Windows Server 2003, you must use native code to obtain a Windows access token representing the desired user. You can then use this access token to instantiate a WindowsIdentity
object. Recipe 11-12 explains how to obtain Windows access tokens for specific users.
Once you have a WindowsIdentity
, instantiate a new WindowsPrincipal
object, passing the WindowsIdentity
object as an argument to the constructor. Finally, call the IsInRole
method of the WindowsPrincipal
object to test if the user is in a specific group (role). IsInRole
returns true
if the user is a member of the specified group; otherwise, it returns false
. The IsInRole
method provides four overloads:
The first overload takes a string
containing the name of the group for which you want to test. The group name must be of the form [
DomainName][
GroupName]
for domain-based groups and [
MachineName][
GroupName]
for locally defined groups. If you want to test for membership of a standard Windows group, use the form BUILTIN[
GroupName]
or the other overload that takes a value from the System.Security.Principal.WindowsBuiltInRole
enumeration. IsInRole
performs a case-insensitive test for the specified group name.
The second IsInRole
overload accepts an int
, which specifies a Windows role identifier (RID). RIDs provide a mechanism that is independent of language and localization to identify groups.
The third IsInRole
overload accepts a member of the System.Security.Principal.WindowsBuiltInRole
enumeration. The WindowsBuiltInRole
enumeration defines a set of members that represent each of the built-in Windows groups.
The fourth IsInRole
overload accepts a System.Security.Principal.SecurityIdentifier
object that represents the security identifier (SID) of the group for which you want to test.
Table 11-2 lists the name, RID, and WindowsBuiltInRole
value for each of the standard Windows groups.
Table 11.2. Windows Built-In Account Names and Identifiers
Account Name | RID (Hex) | WindowsBuiltInRole Value |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Membership of the BUILTINAdministrators
group under Windows 7 will depend on the whether your process is running with elevated privileges. If the current user is an administrator but your process is running without elevated privileges, checking membership of BUILTINAdministrators
will return false
. See Chapter 14 for recipes relating to elevated privileges.
The following example demonstrates how to test whether the current user is a member of a set of named "Windows groups." You specify the groups that you want to test for as command-line arguments. Remember to prefix the group name with the machine or domain name, or BUILTIN
for standard Windows groups.
using System; using System.Security.Principal; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_10 { public static void Main (string[] args) {
if (args.Length == 0) { Console.WriteLine( "Please provide groups to check as command line arguments"); } // Obtain a WindowsIdentity object representing the currently // logged-on Windows user. WindowsIdentity identity = WindowsIdentity.GetCurrent(); // Create a WindowsPrincipal object that represents the security // capabilities of the specified WindowsIdentity; in this case, // the Windows groups to which the current user belongs. WindowsPrincipal principal = new WindowsPrincipal(identity); // Iterate through the group names specified as command-line // arguments and test to see if the current user is a member of // each one. foreach (string role in args) { Console.WriteLine("Is {0} a member of {1}? = {2}", identity.Name, role, principal.IsInRole(role)); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } }
If you run this example as a user named Darryl on a computer named MACHINE using this command:
Recipe11-10 BUILTINAdministrators BUILTINUsers MACHINEAccountants
you will see console output similar to the following:
Is MACHINEDarryl a member of BUILTINAdministrators? = False Is MACHINEDarryl a member of BUILTINUsers? = True Is MACHINEDarryl a member of MACHINEAccountants? = True
You need to restrict which users can execute elements of your code based on the user's name or the roles of which the user is a member.
Use the permission class System.Security.Permissions.PrincipalPermission
and its attribute counterpart System.Security.Permissions.PrincipalPermissionAttribute
to protect your program elements with RBS demands.
The .NET Framework supports both imperative and declarative RBS demands. The class PrincipalPermission
provides support for imperative security statements, and its attribute counterpart PrincipalPermissionAttribute
provides support for declarative security statements. RBS demands use the same syntax as CAS demands, but RBS demands specify the name the current user must have, or more commonly, the roles of which the user must be a member. An RBS demand instructs the runtime to look at the name and roles of the current user, and if that user does not meet the requirements of the demand, the runtime throws a System.Security.SecurityException
exception.
To make an imperative security demand, you must first create a PrincipalPermission
object specifying the username and role name you want to demand, and then you must call its Demand
method. You can specify only a single username and role name per demand. If either the username or the role name is null
, any value will satisfy the demand. Unlike with code access permissions, an RBS demand does not result in a stack walk; the runtime evaluates only the username and roles of the current user.
To make a declarative security demand, you must annotate the class or member you want to protect with a correctly configured PrincipalPermissionAttribute
attribute. Class-level demands apply to all members of the class, unless a member-specific demand overrides the class demand.
Generally, you are free to choose whether to implement imperative or declarative demands. However, imperative security demands allow you to integrate RBS demands with code logic to achieve more sophisticated demand behavior. In addition, if you do not know the role or usernames to demand at compile time, you must use imperative demands. Declarative demands have the advantage that they are separate from code logic and easier to identify. In addition, you can view declarative demands using the Permview.exe
tool (discussed in recipe 11-6). Whether you implement imperative or declarative demands, you must ensure that the runtime has access to the name and roles for the current user to evaluate the demand correctly.
The System.Threading.Thread
class represents an operating system thread running managed code. The static property CurrentPrincipal
of the Thread
class contains an IPrincipal
instance representing the user on whose behalf the managed thread is running. At the operating system level, each thread also has an associated Windows access token, which represents the Windows account on whose behalf the thread is running. The IPrincipal
instance and the Windows access token are two separate entities. Windows uses its access token to enforce operating system security, whereas the .NET runtime uses its IPrincipal
instance to evaluate application-level RBS demands. Although they may, and often do, represent the same user, this is by no means always the case.
The benefit of this approach is that you can implement a user and an RBS model within your application using a proprietary user accounts database, without the need for all users to have Windows user accounts. This is a particularly useful approach in large-scale, publicly accessible Internet applications.
By default, the Thread.CurrentPrincipal
property is undefined. Because obtaining user-related information can be time-consuming, and only a minority of applications use this information, the .NET designers opted for lazy initialization of the CurrentPrincipal
property. The first time code gets the Thread.CurrentPrincipal
property, the runtime assigns an IPrincipal
instance to the property using the following logic:
If the application domain in which the current thread is executing has a default principal, the runtime assigns this principal to the Thread.CurrentPrincipal
property. By default, application domains do not have default principals. You can set the default principal of an application domain by calling the SetThreadPrincipal
method on a System.AppDomain
object that represents the application domain you want to configure. Code must have the ControlPrincipal
element of SecurityPermission
to call SetThreadPrincipal
. You can set the default principal only once for each application domain; a second call to SetThreadPrincipal
results in the exception System.Security.Policy.PolicyException
.
If the application domain does not have a default principal, the application domain's principal policy determines which IPrincipal
implementation to create and assign to Thread.CurrentPrincipal
. To configure principal policy for an application domain, obtain an AppDomain
object that represents the application domain and call the object's SetPrincipalPolicy
method. The SetPrincipalPolicy
method accepts a member of the enumeration System.Security.Principal.PrincipalPolicy
, which specifies the type of IPrincipal
object to assign to Thread.CurrentPrincipal
. Code must have the ControlPrincipal
element of SecurityPermission
to call SetPrincipalPolicy
. Table 11-3 lists the available PrincipalPolicy
values; the default value is UnauthenticatedPrincipal
.
If your code has the ControlPrincipal
element of SecurityPermission
, you can instantiate your own IPrincipal
object and assign it to the Thread.CurrentPrincipal
property directly. This will prevent the runtime from assigning default IPrincipal
objects or creating new ones based on principal policy.
Table 11.3. Members of the PrincipalPolicy Enumeration
Member Name | Description |
---|---|
| No |
| An empty |
| A |
Whatever method you use to establish the IPrincipal
for the current thread, you must do so before you use RBS demands, or the correct user (IPrincipal
) information will not be available for the runtime to process the demand. Normally, when running on the Windows platform, you would set the principal policy of an application domain to PrincipalPolicy.WindowsPrincipal
(as shown here) to obtain Windows user information.
// Obtain a reference to the current application domain. AppDomain appDomain = System.AppDomain.CurrentDomain; // Configure the current application domain to use Windows-based principals. appDomain.SetPrincipalPolicy( System.Security.Principal.PrincipalPolicy.WindowsPrincipal);
The following example demonstrates the use of imperative and declarative RBS demands. The example shows three methods protected using imperative RBS demands (Method1, Method2
, and Method3
), and then three other methods protected using the equivalent declarative RBS demands (Method4, Method5
, and Method6
).
using System; using System.Security.Permissions; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_11 { public static void Method1() { // An imperative role-based security demand for the current principal // to represent an identity with the name Anya. The roles of the // principal are irrelevant. PrincipalPermission perm = new PrincipalPermission(@"MACHINEAnya", null);
// Make the demand. perm.Demand(); } public static void Method2() { // An imperative role-based security demand for the current principal // to be a member of the roles Managers OR Developers. If the // principal is a member of either role, access is granted. Using the // PrincipalPermission, you can express only an OR-type relationship. // This is because the PrincipalPolicy.Intersect method always // returns an empty permission unless the two inputs are the same. // However, you can use code logic to implement more complex // conditions. In this case, the name of the identity is irrelevant. PrincipalPermission perm1 = new PrincipalPermission(null, @"MACHINEManagers"); PrincipalPermission perm2 = new PrincipalPermission(null, @"MACHINEDevelopers"); // Make the demand. perm1.Union(perm2).Demand(); } public static void Method3() { // An imperative role-based security demand for the current principal // to represent an identity with the name Anya AND be a member of the // Managers role. PrincipalPermission perm = new PrincipalPermission(@"MACHINEAnya", @"MACHINEManagers"); // Make the demand. perm.Demand(); } // A declarative role-based security demand for the current principal // to represent an identity with the name Anya. The roles of the // principal are irrelevant. [PrincipalPermission(SecurityAction.Demand, Name = @"MACHINEAnya")] public static void Method4() { // Method implementation . . . } // A declarative role-based security demand for the current principal // to be a member of the roles Managers OR Developers. If the // principal is a member of either role, access is granted. You // can express only an OR type relationship, not an AND relationship. // The name of the identity is irrelevant. [PrincipalPermission(SecurityAction.Demand, Role = @"MACHINEManagers")]
[PrincipalPermission(SecurityAction.Demand, Role = @"MACHINEDevelopers")] public static void Method5() { // Method implementation . . . } // A declarative role-based security demand for the current principal // to represent an identity with the name Anya AND be a member of the // Managers role. [PrincipalPermission(SecurityAction.Demand, Name = @"MACHINEAnya", Role = @"MACHINEManagers")] public static void Method6() { // Method implementation . . . } } }
You need your code to run in the context of a Windows user other than the currently active user account.
Obtain a System.
Security
.Principal.WindowsIdentity
object representing the Windows user you need to impersonate, and then call the Impersonate
method of the WindowsIdentity
object.
Every Windows thread has an associated access token, which represents the Windows account on whose behalf the thread is running. The Windows operating system uses the access token to determine whether a thread has the appropriate permissions to perform protected operations on behalf of the account, such as read and write files, reboot the system, and change the system time.
By default, a managed application runs in the context of the Windows account that executed the application. This is normally desirable behavior, but sometimes you will want to run an application in the context of a different Windows account. This is particularly true in the case of server-side applications that process transactions on behalf of the users remotely connected to the server.
It's common for a server application to run in the context of a Windows account created specifically for the application—a service account. This service account will have minimal permissions to access system resources. Enabling the application to operate as though it were the connected user permits the application to access the operations and resources appropriate to that user's security clearance. When an application assumes the identity of another user, it's known as impersonation. Correctly implemented, impersonation simplifies security administration and application design while maintaining user accountability.
As discussed in recipe 11-11, a thread's Windows access token and its .NET principal are separate entities and can represent different users. The impersonation technique described in this recipe changes only the Windows access token of the current thread; it does not change the thread's principal. To change the thread's principal, code must have the ControlPrincipal
element of SecurityPermission
and assign a new System.Security.Principal.IPrincipal
object to the CurrentPrincipal
property of the current System.Threading.Thread
.
The System.Security.Principal.WindowsIdentity
class provides the functionality through which you invoke impersonation. However, the exact process depends on which version of Windows your application is running. If it's running on Windows Server 2003 or later, the WindowsIdentity
class supports constructor overloads that create WindowsIdentity
objects based on the account name of the user you want to impersonate. On all previous versions of Windows, you must first obtain a System.IntPtr
containing a reference to a Windows access token that represents the user to impersonate. To obtain the access token reference, you must use a native method such as the LogonUser
function from the Win32 API.
Once you have a WindowsIdentity
object representing the user you want to impersonate, call its Impersonate
method. From that point on, all actions your code performs occur in the context of the impersonated Windows account. The Impersonate
method returns a System.Security.Principal.WindowsSecurityContext
object, which represents the active account prior to impersonation. To revert to the original account, call the Undo
method of this WindowsSecurityContext
object.
The following example demonstrates impersonation of a Windows user. The example uses the LogonUser
function of the Win32 API to obtain a Windows access token for the specified user, impersonates the user, and then reverts to the original user context.
using System; using System.IO; using System.Security.Principal; using System.Security.Permissions; using System.Runtime.InteropServices; // Ensure the assembly has permission to execute unmanaged code // and control the thread principal. [assembly:SecurityPermission(SecurityAction.RequestMinimum, UnmanagedCode=true, ControlPrincipal=true)] namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_12 {
// Define some constants for use with the LogonUser function. const int LOGON32_PROVIDER_DEFAULT = 0; const int LOGON32_LOGON_INTERACTIVE = 2; // Import the Win32 LogonUser function from advapi32.dll. Specify // "SetLastError = true" to correctly support access to Win32 error // codes. [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern bool LogonUser(string userName, string domain, string password, int logonType, int logonProvider, ref IntPtr accessToken); public static void Main(string[] args) { // Create a new IntPtr to hold the access token returned by the // LogonUser function. IntPtr accessToken = IntPtr.Zero; // Call LogonUser to obtain an access token for the specified user. // The accessToken variable is passed to LogonUser by reference and // will contain a reference to the Windows access token if // LogonUser is successful. bool success = LogonUser( args[0], // Username to log on. ".", // Use the local account database. args[1], // User's password. LOGON32_LOGON_INTERACTIVE, // Create an interactive login. LOGON32_PROVIDER_DEFAULT, // Use the default logon provider. ref accessToken // Receives access token handle. ); // If the LogonUser return code is zero, an error has occurred. // Display the error and exit. if (!success) { Console.WriteLine("LogonUser returned error {0}", Marshal.GetLastWin32Error()); } else { // Create a new WindowsIdentity from the Windows access token. WindowsIdentity identity = new WindowsIdentity(accessToken); // Display the active identity. Console.WriteLine("Identity before impersonation = {0}", WindowsIdentity.GetCurrent().Name); // Impersonate the specified user, saving a reference to the // returned WindowsImpersonationContext, which contains the // information necessary to revert to the original user // context.
WindowsImpersonationContext impContext = identity.Impersonate(); // Display the active identity. Console.WriteLine("Identity during impersonation = {0}", WindowsIdentity.GetCurrent().Name); // ***************************************** // Perform actions as the impersonated user. // ***************************************** // Revert to the original Windows user using the // WindowsImpersonationContext object. impContext.Undo(); // Display the active identity. Console.WriteLine("Identity after impersonation = {0}", WindowsIdentity.GetCurrent().Name); // Wait to continue. Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } } }
The example expects two command-line arguments: the account name of the user on the local machine to impersonate and the account's password. For example, the command Recipe11-12 Bob password
impersonates the user Bob, as long as that user exists in the local accounts database and his password is "password."
You need to create a random number that is suitable for use in cryptographic and security applications.
Use a cryptographic random number generator such as the System.Security.Cryptography.RNGCryptoServiceProvider
class.
The System.Random
class is a pseudorandom number generator that uses a mathematical algorithm to simulate the generation of random numbers. In fact, the algorithm it uses is deterministic, meaning that you can always calculate what the next number will be based on the previously generated number. This means that numbers generated by the Random
class are unsuitable for use in situations in which security is a priority, such as generating encryption keys and passwords.
When you need a nondeterministic random number for use in cryptographic or security-related applications, you must use a random number generator derived from the class System.Security.Cryptography.RandomNumberGenerator
. The RandomNumberGenerator
class is an abstract
class from which all concrete .NET random number generator classes should inherit. Currently, the RNGCryptoServiceProvider
class is the only concrete implementation provided. The RNGCryptoServiceProvider
class provides a managed wrapper around the CryptGenRandom
function of the Win32 CryptoAPI, and you can use it to fill byte
arrays with cryptographically random byte
values.
The numbers produced by the RNGCryptoServiceProvider
class are not truly random. However, they are sufficiently random to meet the requirements of cryptography and security applications in most commercial and government environments.
As is the case with many of the .NET cryptography classes, the RandomNumberGenerator
base class is a factory for the concrete implementation classes that derive from it. Calling RandomNumberGenerator.Create("System.Security.Cryptography.RNGCryptoServiceProvider")
will return an instance of RNGCryptoServiceProvider
that you can use to generate random numbers. In addition, because RNGCryptoServiceProvider
is the only concrete implementation provided, it's the default class created if you call the Create
method without arguments, as in RandomNumberGenerator.Create()
.
Once you have a RandomNumberGenerator
instance, the method GetBytes
fills a byte
array with random byte
values. As an alternative, you can use the GetNonZeroBytes
method if you need random data that contains no zero values.
The following example instantiates an RNGCryptoServiceProvider
object and uses it to generate random values.
using System; using System.Security.Cryptography; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_13 { public static void Main() { // Create a byte array to hold the random data. byte[] number = new byte[32];
// Instantiate the default random number generator. RandomNumberGenerator rng = RandomNumberGenerator.Create(); // Generate 32 bytes of random data. rng.GetBytes(number); // Display the random number. Console.WriteLine(BitConverter.ToString(number)); // Wait to continue. Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } }
The computational effort required to generate a random number with RNGCryptoServiceProvider
is significantly greater than that required by Random
. For everyday purposes, the use of RNGCryptoServiceProvider
is overkill. You should consider the quantity of random numbers you need to generate and the purpose of the numbers before deciding to use RNGCryptoServiceProvider
. Excessive and unnecessary use of the RNGCryptoServiceProvider
class could have a noticeable effect on application performance if many random numbers are generated.
You need to store a user's password securely so that you can use it to authenticate the user in the future.
Create and store a cryptographic hash code of the password using a hashing algorithm class derived from the System.Security.Cryptography.HashAlgorithm
class. On future authentication attempts, generate the hash of the password entered by the user and compare it to the stored hash code.
You should never store a user's plain text password, because it is a major security risk and one that most users would not appreciate, given that many of them will use the same password to access multiple systems.
Hashing algorithms are one-way cryptographic functions that take plain text of variable length and generate a fixed-size numeric value. They are one-way because it's nearly impossible to derive the original plain text from the hash code. Hashing algorithms are deterministic; applying the same hashing algorithm to a specific piece of plain text always generates the same hash code. This makes hash codes useful for determining if two blocks of plain text (passwords in this case) are the same. The design of hashing algorithms ensures that the chance of two different pieces of plain text generating the same hash code is extremely small (although not impossible). In addition, there is no correlation between the similarity of two pieces of plain text and their hash codes; minor differences in the plain text cause significant differences in the resulting hash codes.
When using passwords to authenticate a user, you are not concerned with the content of the password that the user enters. You need to know only that the entered password matches the password that you have recorded for that user in your accounts database.
The nature of hashing algorithms makes them ideal for storing passwords securely. When the user provides a new password, you must create the hash code of the password and store it, and then discard the plain text password. Each time the user tries to authenticate with your application, calculate the hash code of the password that user provides and compare it with the hash code you have stored.
People regularly ask how to obtain a password from a hash code. The simple answer is that you cannot. The whole purpose of a hash code is to act as a token that you can freely store without creating security holes. If a user forgets a password, you cannot derive it from the stored hash code. Rather, you must either reset the account to some default value or generate a new password for the user.
Generating hash codes is simple in the .NET Framework. The abstract class HashAlgorithm
provides a base from which all concrete hashing algorithm implementations derive. The .NET Framework class library includes the seven hashing algorithm implementations listed in Table 11-4; each implementation class is a member of the System.Security.Cryptography
namespace. The classes with names ending in CryptoServiceProvider
wrap functionality provided by the native Win32 CryptoAPI, whereas those with names ending in Managed
are fully implemented in managed code.
Table 11.4. Hashing Algorithm Implementations
Algorithm Name | Class Name | Hash Code Size (in Bits) |
---|---|---|
MD5 |
| 128 |
RIPEMD160 or RIPEMD-160 |
| 160 |
SHA or SHA1 |
| 160 |
SHA1Managed |
| 160 |
SHA256 or SHA-256 |
| 256 |
SHA384 or SHA-384 |
| 384 |
SHA512 or SHA-512 |
| 512 |
Although you can create instances of the hashing algorithm classes directly, the HashAlgorithm
base class is a factory for the concrete implementation classes that derive from it. Calling the static method HashAlgorithm.Create
will return an object of the specified type. Using the factory approach allows you to write generic code that can work with any hashing algorithm implementation. Note that unlike in recipe 11-13, you do not pass the class name as parameter to the factory; instead, you pass the algorithm name.
Once you have a HashAlgorithm
object, its ComputeHash
method accepts a byte
array argument containing plain text and returns a new byte
array containing the generated hash code. Table 11-4 shows the size of hash code (in bits) generated by each hashing algorithm class.
The SHA1Managed algorithm cannot be implemented using the factory approach. It must be instantiated directly.
The example shown here demonstrates the creation of a hash code from a string, such as a password. The application expects two command-line arguments: the name of the hashing algorithm to use and the string from which to generate the hash. Because the HashAlgorithm.ComputeHash
method requires a byte array, you must first byte-encode the input string using the class System.Text.Encoding
, which provides mechanisms for converting strings to and from various character-encoding formats.
using System; using System.Text; using System.Security.Cryptography;
namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_14 { public static void Main(string[] args) { // Create a HashAlgorithm of the type specified by the first // command-line argument. HashAlgorithm hashAlg = null; if (args[0].CompareTo("SHA1Managed") == 0) { hashAlg = new SHA1Managed(); } else { hashAlg = HashAlgorithm.Create(args[0]); } using (hashAlg) { // Convert the password string, provided as the second // command-line argument, to an array of bytes. byte[] pwordData = Encoding.Default.GetBytes(args[1]); // Generate the hash code of the password. byte[] hash = hashAlg.ComputeHash(pwordData); // Display the hash code of the password to the console. Console.WriteLine(BitConverter.ToString(hash)); // Wait to continue. Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } } }
Running the following command:
Recipe11-14 SHA1 ThisIsMyPassword
will display the following hash code to the console:
30-B8-BD-58-29-88-89-00-D1-5D-2B-BE-62-70-D9-BC-65-B0-70-2F
In contrast, executing this command:
Recipe11-14 RIPEMD-160 ThisIsMyPassword
will display the following hash code:
0C-39-3B-2E-8A-4E-D3-DD-FB-E3-C8-05-E4-62-6F-6B-76-7C-7A-49
Create a cryptographic hash code of the file's contents using the ComputeHash
method of the System.Security.Cryptography.HashAlgorithm
class. Store the hash code for future comparison against newly generated hash codes.
As well as allowing you to store passwords securely (discussed in recipe 11-14), hash codes provide an excellent means of determining if a file has changed. By calculating and storing the cryptographic hash of a file, you can later recalculate the hash of the file to determine if the file has changed in the interim. A hashing algorithm will produce a very different hash code even if the file has been changed only slightly, and the chances of two different files resulting in the same hash code are extremely small.
Standard hash codes are not suitable for sending with a file to ensure the integrity of the file's contents. If someone intercepts the file in transit, that person can easily change the file and recalculate the hash code, leaving the recipient none the wiser. Recipe 11-17 discusses a variant of the hash code—a keyed hash code—that is suitable for ensuring the integrity of a file in transit.
The HashAlgorithm
class makes it easy to generate the hash code of a file. First, instantiate one of the concrete hashing algorithm implementations derived from the HashAlgorithm
class. To instantiate the desired hashing algorithm class, pass the name of the hashing algorithm to the HashAlgorithm.Create
method, as described in recipe 11-14. See Table 11-4 for a list of valid hashing algorithm names. Then, instead of passing a byte array to the ComputeHash
method, you pass a System.IO.Stream
object representing the file from which you want to generate the hash code. The HashAlgorithm
object handles the process of reading data from the Stream
and returns a byte array containing the hash code for the file.
The example shown here demonstrates the generation of a hash code from a file. The application expects two command-line arguments: the name of the hashing algorithm to use and the name of the file from which the hash is calculated.
using System; using System.IO; using System.Security.Cryptography; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_15 { public static void Main(string[] args) { // Create a HashAlgorithm of the type specified by the first // command-line argument. using (HashAlgorithm hashAlg = HashAlgorithm.Create(args[0])) { // Open a FileStream to the file specified by the second // command-line argument. using (Stream file = new FileStream(args[1], FileMode.Open, FileAccess.Read)) { // Generate the hash code of the file's contents. byte[] hash = hashAlg.ComputeHash(file); // Display the hash code of the file to the console. Console.WriteLine(BitConverter.ToString(hash)); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } } }
Running this command:
Recipe11-15 SHA1 Recipe11-15.exe
will display the following hash code to the console:
CA-67-A5-2D-EC-E9-FC-45-AE-97-E9-E1-38-CB-17-86-BB-17-EE-30
In contrast, executing this command:
Recipe11-15 RIPEMD-160 Recipe11-15.exe
will display the following hash code:
E1-6E-FA-BB-89-BA-DA-83-20-D5-CA-EC-FC-3D-52-13-86-B9-41-7C
Convert both the old and the new hash codes to hexadecimal code strings, Base64 strings, or byte arrays, and compare them.
You can use hash codes to determine if two pieces of data (such as passwords or files) are the same, without the need to store or even maintain access to the original data. To determine if data changes over time, you must generate and store the original data's hash code. Later, you can generate another hash code for the data and compare the old and new hash codes, which will show whether any change has occurred. The format in which you store the original hash code will determine the most appropriate way to verify a newly generated hash code against the stored one.
The recipes in this chapter use the ToString
method of the class System.BitConverter
to convert byte arrays to hexadecimal string values for display. Although easy to use and appropriate for display purposes, you might find this approach inappropriate for use when storing hash codes, because it places a hyphen (-
) between each byte value (for example, 4D-79-3A-C9- . . .). In addition, the BitConverter
class does not provide a method to parse such a string representation back into a byte array.
Hash codes are often stored in text files, either as hexadecimal strings (for example, 89D22213170A9CFF09A392F00E2C6C4EDC1B0EF9
) or as Base64-encoded strings (for example, idIiExcKnP8Jo5LwDixsTtwbDvk=
). Alternatively, hash codes may be stored in databases as raw byte values. Regardless of how you store your hash code, the first step in comparing old and new hash codes is to get them both into a common form.
This following example contains three methods that use different approaches to compare hash codes:
VerifyHexHash
: This method converts a new hash code (a byte array) to a hexadecimal string for comparison to an old hash code. Other than the BitConverter.ToString
method, the .NET Framework class library does not provide an easy method to convert a byte array to a hexadecimal string. You must program a loop to step through the elements of the byte array, convert each individual byte to a string, and append the string to the hexadecimal string representation of the hash code. The use of System.Text.StringBuilder
avoids the unnecessary creation of new strings each time the loop appends the next byte value to the result string. (See recipe 2-1 for more details.)
VerifyB64Hash
: This method takes a new hash code as a byte array and the old hash code as a Base64-encoded string. The method encodes the new hash code as a Base64 string and performs a straightforward string comparison of the two values.
VerifyByteHash
: This method compares two hash codes represented as byte arrays. The .NET Framework class library does not include a method that performs this type of comparison, and so you must program a loop to compare the elements of the two arrays. This code uses a few time-saving techniques, namely ensuring that the byte arrays are the same length before starting to compare them and returning false
on the first difference found.
using System; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_16 { // A method to compare a newly generated hash code with an // existing hash code that's represented by a hex code string. public static bool VerifyHexHash(byte[] hash, string oldHashString) { // Create a string representation of the hash code bytes. StringBuilder newHashString = new StringBuilder(hash.Length);
// Append each byte as a two-character uppercase hex string. foreach (byte b in hash) { newHashString.AppendFormat("{0:X2}", b); } // Compare the string representations of the old and new hash // codes and return the result. return (oldHashString == newHashString.ToString()); } // A method to compare a newly generated hash code with an // existing hash code that's represented by a Base64-encoded string. private static bool VerifyB64Hash(byte[] hash, string oldHashString) { // Create a Base64 representation of the hash code bytes. string newHashString = Convert.ToBase64String(hash); // Compare the string representations of the old and new hash // codes and return the result. return (oldHashString == newHashString); } // A method to compare a newly generated hash code with an // existing hash code represented by a byte array. private static bool VerifyByteHash(byte[] hash, byte[] oldHash) { // If either array is null or the arrays are different lengths, // then they are not equal. if (hash == null || oldHash == null || hash.Length != oldHash.Length) return false; // Step through the byte arrays and compare each byte value. for (int count = 0; count < hash.Length; count++) { if (hash[count] != oldHash[count]) return false; } // Hash codes are equal. return true; } } }
You need to transmit a file to someone and provide the recipient with a means to verify the integrity of the file and its source.
Share a secret key with the intended recipient. This key would ideally be a randomly generated number, but it could also be a phrase that you and the recipient agree to use. Use the key with one of the keyed hashing algorithm classes derived from the System.Security.Cryptography.KeyedHashAlgorithm
class to create a keyed hash code. Send the hash code with the file. On receipt of the file, the recipient will generate the keyed hash code of the file using the shared secret key. If the hash codes are equal, the recipient knows that the file is from you and that it has not changed in transit.
Hash codes are useful for comparing two pieces of data to determine if they are the same, even if you no longer have access to the original data. However, you cannot use a hash code to reassure the recipient of data as to the data's integrity. If someone could intercept the data, that person could replace the data and generate a new hash code. When the recipient verifies the hash code, it will seem correct, even though the data is actually nothing like what you sent originally.
A simple and efficient solution to the problem of data integrity is a keyed hash code. A keyed hash code is similar to a normal hash code (discussed in recipes 11-14 and 11-15); however, the keyed hash code incorporates an element of secret data—a key—known only to the sender and the receiver. Without the key, a person cannot generate the correct hash code from a given set of data. When you successfully verify a keyed hash code, you can be certain that only someone who knows the secret key could generate the hash code.
The secret key must remain secret. Anyone who knows the secret key can generate valid keyed hash codes, meaning that you would be unable to determine whether someone else who knew the key had changed the content of a document. For this reason, you should not transmit or store the secret key with the document whose integrity you are trying to protect.
Generating keyed hash codes is similar to generating normal hash codes. The abstract class System.Security.Cryptography.KeyedHashAlgorithm
extends the class System.Security.Cryptography.HashAlgorithm
and provides a base class from which all concrete keyed hashing algorithm implementations must derive. The .NET Framework class library includes the seven keyed hashing algorithm implementations listed in Table 11-5. Each implementation is a member of the namespace System.Security.Cryptography
.
Table 11.5. Keyed Hashing Algorithm Implementations
Algorithm/Class Name | Key Size (in Bits) | Hash Code Size (in Bits) |
---|---|---|
| Any | 128 |
| Any | 160 |
| Any | 160 |
| Any | 256 |
| Any | 384 |
| Any | 512 |
| 128, 192 | 64 |
As with the standard hashing algorithms, you can either create keyed hashing algorithm objects directly or use the static factory method KeyedHashAlgorithm.Create
and pass the algorithm name as an argument. Using the factory approach allows you to write generic code that can work with any keyed hashing algorithm implementation, but as shown in Table 11-5, MACTripleDES
supports fixed key lengths that you must accommodate in generic code.
If you use constructors to instantiate a keyed hashing object, you can pass the secret key to the constructor. Using the factory approach, you must set the key using the Key
property inherited from the KeyedHashAlgorithm
class. Then call the ComputeHash
method and pass either a byte array or a System.IO.Stream
object. The keyed hashing algorithm will process the input data and return a byte array containing the keyed hash code. Table 11-5 shows the size of the hash code generated by each keyed hashing algorithm.
The following example demonstrates the generation of a keyed hash code from a file. The example uses the given class to generate the keyed hash code, and then displays it to the console. The example requires three command-line arguments: the name of the file from which the hash is calculated, the name of the class to instantiate, and the key to use when calculating the hash.
using System; using System.IO; using System.Text; using System.Security.Cryptography; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_17 {
public static void Main(string[] args) { // Create a byte array from the key string, which is the // second command-line argument. byte[] key = Encoding.Unicode.GetBytes(args[2]); // Create a KeyedHashAlgorithm-derived object to generate the keyed // hash code for the input file. Pass the byte array representing the // key to the constructor. using (KeyedHashAlgorithm hashAlg = KeyedHashAlgorithm.Create(args[1])) { // Assign the key. hashAlg.Key = key; // Open a FileStream to read the input file. The file name is // specified by the first command-line argument. using (Stream file = new FileStream(args[0], FileMode.Open, FileAccess.Read)) { // Generate the keyed hash code of the file's contents. byte[] hash = hashAlg.ComputeHash(file); // Display the keyed hash code to the console. Console.WriteLine(BitConverter.ToString(hash)); } } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } }
Executing the following command:
Recipe11-17 Recipe11-17.exe HMACSHA1 secretKey
will display the following hash code to the console:
2E-5B-9B-2C-91-42-BA-4E-98-DF-39-F6-AE-89-B6-44-61-FB-32-E7
In contrast, executing this command:
Recipe11-17 Recipe11-17.exe HMACSHA1 anotherKey
will display the following hash code to the console:
EF-64-79-3A-3C-A4-44-01-AD-9E-94-2A-B4-58-CF-42-84-3E-27-91
You need to work with sensitive string data, such as passwords or credit card numbers, in memory, and you need to minimize the risk of other people or processes accessing that data.
Storing sensitive data such as passwords, personal details, and banking information in memory as String
objects is insecure for many reasons, including the following:
String
objects are not encrypted.
The immutability of String
objects means that whenever you change the String
, the old String
value is left in memory until it is garbage-collected and later overwritten.
Because the garbage collector is free to reorganize the contents of the managed heap, multiple copies of your sensitive data may be present on the heap.
If part of your process address space is swapped to disk or a memory dump is written to disk, a copy of your data may be stored on the disk.
Each of these factors increases the opportunities for others to access your sensitive data. The .NET Framework includes the SecureString
class to simplify the task of working with sensitive string
data in memory.
You create a SecureString
as either initially empty or from a pointer to a character (char) array. Then you manipulate the contents of the SecureString
one character at a time using the methods AppendChar, InsertAt, RemoveAt
, and SetAt
. As you add characters to the SecureString
, they are encrypted using the capabilities of the Data Protection API.
The SecureString
class uses features of Data Protection API (DPAPI) and is available only on Windows 2000 SP3 and later operating system versions.
The SecureString
class also provides a method named MakeReadOnly
. As the name suggests, calling MakeReadOnly
configures the SecureString
to no longer allow its value to be changed. Attempting to modify a SecureString
marked as read-only results in the exception System.InvalidOperationException
being thrown. Once you have set the SecureString
to read-only, it cannot be undone.
The SecureString
class has a ToString
method, but this does not retrieve a string representation of the contained data. Instead, the class System.Runtime.InteropServices.Marshal
implements a number of static methods that take a SecureString
object; decrypts it; converts it to a binary string, a block of ANSI, or a block of Unicode data; and returns a System.IntPtr
object that points to the converted data.
At any time, you can call the SecureString.Clear
method to clear the sensitive data, and when you have finished with the SecureString
object, call its Dispose
method to clear the data and free the memory. SecureString
implements System.IDisposable
.
Although it might seem that the benefits of the SecureString
class are limited, because there is no way in Windows Forms applications to get such a secured string from the GUI without first retrieving an unsecured String
through a TextBox
or another control, it is likely that third parties and future additions to the .NET Framework will use the SecureString
class to handle sensitive data. This is already the case in System.Diagnostics.ProcessStartInfo
, where using a SecureString
, you can set the Password
property to the password of the user context in which the new process should be run.
The following example reads a username and password from the console and starts Notepad.exe
as the specified user. The password is masked on input and stored in a SecureString
in memory, maximizing the chances of the password remaining secret.
using System; using System.Security; using System.Diagnostics; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_18 { public static SecureString ReadString() { // Create a new emtpty SecureString. SecureString str = new SecureString(); // Read the string from the console one // character at a time without displaying it. ConsoleKeyInfo nextChar = Console.ReadKey(true);
// Read characters until Enter is pressed. while (nextChar.Key != ConsoleKey.Enter) { if (nextChar.Key == ConsoleKey.Backspace) { if (str.Length > 0) { // Backspace pressed, remove the last character. str.RemoveAt(str.Length - 1); Console.Write(nextChar.KeyChar); Console.Write(" "); Console.Write(nextChar.KeyChar); } else { Console.Beep(); } } else { // Append the character to the SecureString and // display a masked character. str.AppendChar(nextChar.KeyChar); Console.Write("*"); } // Read the next character. nextChar = Console.ReadKey(true); } // String entry finished. Make it read-only. str.MakeReadOnly(); return str; } public static void Main() { string user = ""; // Get the username under which Notepad.exe will be run. Console.Write("Enter the user name: "); user = Console.ReadLine(); // Get the user's password as a SecureString. Console.Write("Enter the user's password: "); using (SecureString pword = ReadString()) {
// Start Notepad as the specified user. ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.FileName = "notepad.exe"; startInfo.UserName = user; startInfo.Password = pword; startInfo.UseShellExecute = false; // Create a new Process object. using (Process process = new Process()) { // Assign the ProcessStartInfo to the Process object. process.StartInfo = startInfo; try { // Start the new process. process.Start(); } catch (Exception ex) { Console.WriteLine(" Could not start Notepad process."); Console.WriteLine(ex); } } } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } }
You need a convenient way to securely encrypt data without the headache associated with key management.
Use the ProtectedData
and ProtectedMemory
classes of the System.Security.Cryptography
namespace to access the encryption and key management capabilities provided by the Data Protection API (DPAPI).
Given that the .NET Framework provides you with well-tested implementations of the most widely used and trusted encryption algorithms, the biggest challenge you face when using cryptography is key management, namely the effective generation, storage, and sharing of keys to facilitate the use of cryptography. In fact, key management is the biggest problem facing most people when they want to securely store or transmit data using cryptographic techniques. If implemented incorrectly, key management can easily render useless all of your efforts to encrypt your data.
DPAPI provides encryption and decryption services without the need for you to worry about key management. DPAPI automatically generates keys based on Windows user credentials, stores keys securely as part of your profile, and even provides automated key expiry without losing access to previously encrypted data.
DPAPI is suitable for many common uses of cryptography in Windows applications, but will not help you in situations that require you to distribute or share secret or public keys with other users.
The .NET Framework contains two classes in System.Security.dll
that provide easy access to the encryption and decryption capabilities of DPAPI: ProtectedData
and ProtectedMemory
. Both classes allow you to encrypt a byte array by passing it to the static method Protect
, and decrypt a byte array of encrypted data by passing it the static method Unprotect
. The difference in the classes is in the scope that they allow you to specify when you encrypt and decrypt data.
You must use ProtectedData
if you intend to store encrypted data and reboot your machine before decrypting it. ProtectedMemory
will be unable to decrypt data that was encrypted before a reboot.
When you call ProtectedData.Protect
, you specify a value from the enumeration System.Security.Cryptography.DataProtectionScope
. The following are the possible values:
CurrentUser
, which means that only code running in the context of the current user can decrypt the data
LocalMachine
, which means that any code running on the same computer can decrypt the data
When you call ProtectedMemory.Protect
, you specify a value from the enumeration System.Security.Cryptography.MemoryProtectionScope
. The possible values are as follows:
CrossProcess
, which means that any code in any process can decrypt the encrypted data
SameLogon
, which means that only code running in the same user context can decrypt the data
SameProcess
, which means that only code running in the same process can decrypt the data
Both classes allow you to specify additional data (entropy) when you encrypt your data. Entropy makes certain types of cryptographic attacks less likely to succeed. If you choose to use entropy when you protect data, you must use the same entropy value when you unprotect the data. It is not essential that you keep the entropy data secret, so it can be stored freely without encryption.
The following example demonstrates the use of the ProtectedData
class to encrypt a string entered at the console by the user. Note that you need to reference the System.Security.dll
assembly.
using System; using System.Text; using System.Security.Cryptography; namespace Apress.VisualCSharpRecipes.Chapter11 { class Recipe11_19 { public static void Main() { // Read the string from the console. Console.Write("Enter the string to encrypt: "); string str = Console.ReadLine(); // Create a byte array of entropy to use in the encryption process. byte[] entropy = { 0, 1, 2, 3, 4, 5, 6, 7, 8 }; // Encrypt the entered string after converting it to // a byte array. Use LocalMachine scope so that only // the current user can decrypt the data. byte[] enc = ProtectedData.Protect(Encoding.Unicode.GetBytes(str), entropy, DataProtectionScope.LocalMachine); // Display the encrypted data to the console. Console.WriteLine(" Encrypted string = {0}", BitConverter.ToString(enc)); // Attempt to decrypt the data using CurrentUser scope. byte[] dec = ProtectedData.Unprotect(enc, entropy, DataProtectionScope.CurrentUser); // Display the data decrypted using CurrentUser scope. Console.WriteLine(" Decrypted data using CurrentUser scope = {0}", Encoding.Unicode.GetString(dec));
// Wait to continue. Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } }