Microsoft Visual C#.NET 2003 Kick Start

Implementing Security in C#

One of the important aspects of programming assemblies is to understand and handle security. Security is an increasingly important issue to Microsoft so we'll take a look at this issue in the rest of this chapter, starting with using pointers in C#.

Using Pointers

You can use pointers in C++, but not in C# by default. If you're a C++ programmer, you know that a pointer holds the address and type of a programming entity, such as a variable or a method. Pointers are a problem if you're trying to be secure, because hackers can sometimes use them to point and modify various locations in your code far beyond your original intentions. (On the day of this writing, for example, Microsoft just issued a patch for all Windows versions back to Windows 98 for a "critical security issue"in Microsoft Outlook and Internet Explorer having to do with a security hole that can be exploited by hackers using pointers to access code past the data buffers they were intended for.) For that reason, Java doesn't even support pointers. C# doesn't support them by default (and it's best not to if security is an issue), but you can use them if you work with the unsafe and fixed keywords.

Here's an example, ch13_07.cs. In this case, we'll use pointers to copy five integers from one byte buffer to another in our code. We start by creating our byte buffers, Source and Target , and placing five integers into Source :

static void Main(string[] args) { byte[] Source = new byte[20]; byte[] Target = new byte[20]; for(int loopIndex = 0; loopIndex < 5; ++loopIndex) { Source[loopIndex] = (byte) loopIndex; } . . .

Each integer in Source is four bytes long, so we will copy the data in Source to Target in four-byte chunks . You can use pointers only in a context marked unsafe in C#. You can mark a type or class member unsafe using the keyword unsafe , so we'll use that keyword in the declaration of the Main method here, indicating that we're going to use pointers in that method.

You also need a fixed statement, because you can only work with pointers inside that statement in C#. In this example, we're going to create two pointers, one to point to the source buffer and one to the target. You use the fixed statement to fix pointers in memory so that the garbage collector doesn't automatically move them behind your back. You pass the pointers you want to fix to the fixed statement, which makes those pointers read-only. On the other hand, we need pointers we can write to, so inside the fix statement we'll copy those read-only pointers to read/write pointers and copy our data from Source to Target like this:

static unsafe void Main(string[] args) { byte[] Source = new byte[20]; byte[] Target = new byte[20]; for(int loopIndex = 0; loopIndex < 5; ++loopIndex) { Source[loopIndex] = (byte) loopIndex; } fixed (byte* pSourceFixed = Source, pTargetFixed = Target) { byte* pSource = pSourceFixed; byte* pTarget = pTargetFixed; for (int loopIndex = 0 ; loopIndex < 20 ; loopIndex++) { *((int*)pTarget) = *((int*)pSource); pSource += 4; pTarget += 4; } } . . .

All that's left is to display the integers copied into the Target buffer, and you can see how that works in Listing 13.7.

Listing 13.7 Using Pointers (ch13_07.cs)

class ch13_07 { static unsafe void Main(string[] args) { byte[] Source = new byte[20]; byte[] Target = new byte[20]; for(int loopIndex = 0; loopIndex < 5; ++loopIndex) { Source[loopIndex] = (byte) loopIndex; } fixed (byte* pSourceFixed = Source, pTargetFixed = Target) { byte* pSource = pSourceFixed; byte* pTarget = pTargetFixed; for (int loopIndex = 0 ; loopIndex < 20 ; loopIndex++) { *((int*)pTarget) = *((int*)pSource); pSource += 4; pTarget += 4; } } for(int loopIndex = 0; loopIndex < 5; ++loopIndex) { System.Console.Write(Target[loopIndex] + " "); } } }

When you compile this example, ch13_07.cs, you must use the /unsafe switch:

C:\>csc /unsafe ch13_07.cs

And that's it. You've used pointers in a C# program. When you run the program, you see that the integers were indeed copied over from Source to Target :

C:>ch13_07 0 1 2 3 4

One place where pointers are essential is when you have to call a Windows API function that uses them. To interact with Windows API functions directly in C#, you can use the [DllImport] attribute to indicate where the program can find those functions. For example, to import that Windows API function CreateFile , which is in the Windows system DLL kernel32.dll, you can use this code in a .CS file, but outside any method (make sure to include System.Runtime.InteropServices as well, as shown here):

using System.Runtime.InteropServices; [DllImport("kernel32", SetLastError=true)] static extern unsafe IntPtr CreateFile(string FileName, uint DesiredAccess, uint ShareMode, uint SecurityAttributes, uint CreationDisposition, uint FlagsAndAttributes, int hTemplateFile);

After importing CreateFile , you can call it to create a Windows file handle for a new file like this:

const uint GENERIC_READ = 0x80000000; const uint OPEN_EXISTING = 3; IntPtr handle; handle = CreateFile(FileName, GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0);

You can use a Windows file handle like this to read from a file, using the ReadFile API function. To call that function, you need to pass a pointer to the data buffer you want filled with data and a pointer to an integer which will hold the number of bytes read:

[DllImport("kernel32", SetLastError=true)] static extern unsafe bool ReadFile(IntPtr hFile, void* pBuffer, int NumberOfBytesToRead, int* pNumberOfBytesRead, int Overlapped);

That means you should call ReadFile in an unsafe context, as in this method:

public unsafe int Reader(byte[] buffer, int index, int count) { int number = 0; fixed (byte* p = buffer) { if (!ReadFile(handle, p + index, count, &number, 0)) return 0; } return number; }

Setting Security Permissions

You can grant or deny access to assemblies using permissions . There are three kinds of permissions, each with a specific purpose:

  • Code access permissions represent access to a protected resource or the capability to perform a protected operation.

  • Identity permissions indicate that code has credentials that support a particular kind of identity.

  • Role-based security permissions indicate whether a user has a particular identity or is a member of a specified role.

Code access permissions are used to protect resources and operations from unauthorized use. All code access permissions can be requested or demanded in code, and the runtime decides which permissions, if any, to grant the code. You can see the code access permissions in Table 13.5.

Table 13.5. Code Access Permissions

PERMISSION CLASS

PERMISSION

DirectoryServicesPermission

Allows access to the System.DirectoryServices classes.

DnsPermission

Allows access to Domain Name System (DNS).

EnvironmentPermission

Allows reading or writing of environment variables .

EventLogPermission

Allows read or write access to event log services.

FileDialogPermission

Allows access to files that have been selected by the user in an Open dialog box.

FileIOPermission

Allows code to read, append, or write files or directories.

IsolatedStorageFilePermission

Allows access to private virtual file systems.

IsolatedStoragePermission

Allows access to isolated storage.

MessageQueuePermission

Allows access to message queues through the managed Microsoft Message Queuing (MSMQ) interfaces.

OleDbPermission

Allows access to databases using OLE DB.

PerformanceCounterPermission

Allows access to performance counters.

PrintingPermission

Allows access to printers.

ReflectionPermission

Allows access to information about a type at runtime.

RegistryPermission

Allows the code to read, write, create, or delete Registry keys and values.

SecurityPermission

Allows the code to execute, assert permissions, call into unmanaged code, skip verification, and so on.

ServiceControllerPermission

Allows access to running or stopped services.

SocketPermission

Allows access to sockets.

SqlClientPermission

Allows access to SQL databases.

UIPermission

Allows access to user interface functionality.

WebPermission

Allows the code to use connections on a Web address.

Identity permissions hold characteristics that identify an assembly. The CLR grants identity permissions to an assembly based on the information it discovers about the assembly. For example, one identity permission represents the strong name an assembly must have, another represents the Web site where the code must have come from, and so on. You can see the identity permissions in Table 13.6.

Table 13.6. Identity Permissions

PERMISSION CLASS

IDENTITY

PublisherIdentityPermission

Permission based on the software publisher's digital signature.

SiteIdentityPermission

Permission based on the Web site where the code came from.

StrongNameIdentityPermission

Permission based on the strong name of the assembly.

URLIdentityPermission

Permission based on the URL where the code came from.

ZoneIdentityPermission

Permission based on the zone where the code originated.

There are also role-based permissions, and roles are groups of users. At this point, the PrincipalPermission class is the only role-based security permission supplied by the .NET Framework class library. PrincipalPermission is a security permission that you can use to determine whether a user has a given identity or is a member of a given group .

We'll take a look using code access permissions in the following few examples.

Asking for Minimum Permission

In our first example, ch13_08.cs, we'll simply request the minimum file I/O permissions that this application needs to run. To request code access permissions for the assembly, you use an assembly attribute, [assembly:FileIOPermissionAttribute] in your code. In this case, we'll request permission to read and write files in a directory named c:\ch13_08 , as you see in ch13_08.cs in Listing 13.8.

Listing 13.8 Requesting Minimum Code Access Permissions (ch13_08.cs)

using System; using System.IO; using System.Security.Permissions; [assembly:FileIOPermissionAttribute(SecurityAction.RequestMinimum, All="c:\ch13_08")] class ch13_08 { public static void Main() { FileStream filestream = File.Create("c:\ch13_08\ch13_08.txt"); StreamWriter streamwriter = new StreamWriter(filestream); streamwriter.WriteLine("No worries!"); streamwriter.Flush(); streamwriter.Close(); } }

When you run ch13_08.exe, it'll create a file named ch13_08.txt with the text "No worries!" in it. So far so goodnow how about restricting what permissions you can get for ch13_08.exe in code? To do that, you can use code groups .

Restricting Permission with Code Groups

When you create a code group, you act as an administrator, defining permissions for various assemblies. If an assembly is part of a code group, the code group grants the assembly a set of permissions that has been given to that code group. For example, we'll see how to add our ch13_08.exe application to a code group and restrict its permissions now.

How can you add assemblies to a code group? You do that by specifying the membership condition for the group; any code that meets the membership condition is included in the group (each code group has just one membership condition). You can see the possible membership conditions in Table 13.7.

Table 13.7. Membership Conditions

MEMBERSHIP CONDITION

MEANING

All code

Membership matches all code.

Application directory

Membership matches the application's installation directory.

Cryptographic hash

Membership matches an MD5, SHA1, or other cryptographic hash.

Software publisher

Membership matches the public key of a valid Authenticode signature.

Site membership

Membership matches the HTTP, HTTPS, and FTP site from which code came.

Strong name

Membership matches a cryptographically strong signature.

URL

Membership matches the URL from which the code came.

Zone

Membership matches the zone from where the code came.

After an assembly is identified as part of a code group, it is granted the permissions in the code groups' associated permission set . You can use any of the built-in permission sets with a code group Nothing , Execution , FullTrust , Internet , LocalIntranet , and SkipVerification or you can create your own permission set.

To see how all this works, we'll take a look at creating a code group now to see how to restrict the security example we just wrote, ch13_08.exe. We'll use the .NET Framework Configuration tool for this purpose. Select Start, Programs, Administrative Tools, Microsoft .NET Framework 1.1 Configuration to open the configuration manager, as shown in Figure 13.5.

Figure 13.5. The .NET Framework Configuration tool.

Expand the Runtime Security Policy node in the .NET Framework Configuration tool, and then the User node, and then the Permission Sets node to see the available pre-built permission sets for the current user: Nothing , Execution , FullTrust , Internet , LocalIntranet , and SkipVerification . We're going to create a new permission set here to deny assemblies file I/O access, so right-click the Everything permission set now and select Duplicate to create a copy of that permission set, which will be called Copy of Everything .

Select the Copy of Everything permission set and click the Rename Permission Set link in the right pane of the configuration tool. Change the name of this permission set to No Files and click OK, creating the new No Files permission set you see in Figure 13.6.

Figure 13.6. Creating a new permission set.

To set the permissions for this new permission set, click the Change Permissions link in the right pane of the configuration tool to open the Create Permission Set dialog box you see in Figure 13.7. Select the File IO permission and click the Remove button, as you see in the figure, and then click Finish. At this point, our new permission set has all the standard permissions in it except File I/O.

Figure 13.7. Changing a permission set's permissions.

To create a new code group with this permission set, expand the Code Groups folder and click the All Code group, and then click the Add a Child Group link to open the Create Code Group dialog box you see in Figure 13.8. Because we're only going to restrict ch13_08.exe here, name the new code group ch13_08 and click Next.

Figure 13.8. Creating a code group.

To identify the ch13_08.exe assembly, we'll use a hash membership condition in this code group. Select Hash in the Choose the Condition Type for this Code Group drop-down list, and then click SHA1 as the type of hash. Click the Import button, browse to ch13_08.exe, and click Open. This will display the hash used to identify ch13_08.exe in the Create Code Group dialog box, as you see in Figure 13.9. Now we've created a new code group that will contain only one assembly, ch13_08.exe.

Figure 13.9. Identifying an assembly.

Now click Next to specify the permission set for this new code group. In this case, select the No Files permission set as you see in Figure 13.10.

Figure 13.10. Specifying a permission set.

Click Next and then Finish to create the new code group, ch13_08. To make this code group active, right-click the code group now and select the Properties item to open the ch13_08 Properties dialog box you see in Figure 13.11.

Figure 13.11. The ch13_08 Properties dialog box.

Click the top check box in the ch13_08 Properties dialog box, labeled "This policy level will only have the permissions from the permission set associated with this code group" and click OK. This restricts the permissions available to ch13_08.exe so that it can't execute any file I/O operations. You can see how that works nowtry to run it, and you'll see this error:

C:\>ch13_08 Unhandled Exception: System.Security.Policy. PolicyException: Required permissions cannot be acquired.

In this way, we've created a code group with one assembly in it, assigned the code group a custom permission set, and restricted the actions the assembly can take. As you can see, security in .NET is real, and you can use it to control assemblies as you want.

Making Permission Levels Optional

Our permission example requested minimum permissions that it needed to run, but you can also request optional permissions that you would like but don't need. You can see how this works in Listing 13.9, where we've modified ch13_08.cs to ask for File I/O to the directory c:\ch13_08 if it can get it.

Listing 13.9 Requesting Optional Code Access Permissions (ch13_08.cs, Second Version)

using System; using System.IO; using System.Security.Permissions; [assembly:FileIOPermissionAttribute(SecurityAction.RequestOptional, All="c:\ch13_08")] class ch13_08 { public static void Main() { FileStream filestream = File.Create("c:\ch13_08\ch13_08.txt"); StreamWriter streamwriter = new StreamWriter(filestream); streamwriter.WriteLine("No worries!"); streamwriter.Flush(); streamwriter.Close(); } }

Unfortunately, we've denied file I/O permission to ch13_08.exe (although you'll have to update the code group's hash that it uses to identify ch13_08.exe after modifying the ch13_08 assembly as shown in Listing 13.9), so this new version of ch13_08.exe won't run. But in this case, the code won't automatically simply haltyou can handle this problem with a try/catch block in your code and recover from it, because you've indicated that this permission request is optional.

Requesting a Permission Set

In fact, evenif your assembly isn't part of a code group, you can request that it be assigned one of the pre-built permission sets, Nothing , Execution , FullTrust , Internet , LocalIntranet , or SkipVerification , using PermissionSetAttribute . For example, the code in Listing 13.10 is requesting Internet permission.

Listing 13.10 Requesting Permission Sets (ch13_08.cs, Third Version)

using System; using System.IO; using System.Security.Permissions; [assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, Name="Internet")] class ch13_08 { public static void Main() { FileStream filestream = File.Create("c:\ch13_08\ch13_08.txt"); StreamWriter streamwriter = new StreamWriter(filestream); streamwriter.WriteLine("No worries!"); streamwriter.Flush(); streamwriter.Close(); } }

Requiring Permission Levels

To maintain security, you can also require that code calling your assembly meets certain minimum permission levels. The code in Listing 13.11 shows how this works, where we're requiring that code linking to ours has unrestricted File I/O permission. Note that you require this type of permission on the class or method level, not on the assembly level (which would use an assembly attribute).

Listing 13.11 Requiring Permission Levels (ch13_08.cs, Fourth Version)

using System; using System.IO; using System.Security.Permissions; [FileIOPermissionAttribute(SecurityAction.LinkDemand, Unrestricted=true)] class ch13_08 { public static void Main() { FileStream filestream = File.Create(@"c:\ch13_08\ch13_08.txt"); StreamWriter streamwriter = new StreamWriter(filestream); streamwriter.WriteLine("No worries!"); streamwriter.Flush(); streamwriter.Close(); } }

Encrypting Files

Our last security topic in this chapter will cover how to encrypt and decrypt data files using the Triple Data Encryption Standard (DES) algorithm. This algorithm is supported in the FCL with the TripleDESCryptoServiceProvider class; when you encrypt data, you use an encryption key and an initialization vector, both of which can be handled as byte arrays, and you use them later as well to decrypt that data.

Here's how it works. You first create a CryptoStream stream and then configure it. Then you can pass that stream to a StreamReader or StreamWriter constructor and work with standard stream readers or stream writers.

Here's an example, ch13_09.cs in the code for this book. In this example, we'll encrypt the text "No worries!" and write it out to disk, and then read it back in with the decryption example coming up, ch13_10.cs. We first create a new CryptoStream object using the CreateEncryptor method of the TripleDESCryptoServiceProvider class. You can pass a specific encryption key and an initialization vector to the CreateEncryptor method if you want. However, if you don't want toand we won't here CreateEncryptor creates a random encryption key and an initialization vector for you. After creating our CryptoStream object, we'll pass it to a StreamWriter constructor. Then we'll use the stream writer to write the text "No worries!" to an encrypted file, secret.dat:

public static void Main() { TripleDESCryptoServiceProvider cyptoProvider = new TripleDESCryptoServiceProvider(); FileStream fileStream = File.Create("c:\c#\ch13\secret.dat"); CryptoStream cryptoStream = new CryptoStream(fileStream, cyptoProvider.CreateEncryptor(), CryptoStreamMode.Write); StreamWriter streamWriter = new StreamWriter(cryptoStream); streamWriter.WriteLine("No worries!"); streamWriter.Close(); . . .

The decrypting program needs the encryption key and an initialization vector, so we'll store those in a separate file, secret.key, as you see in ch13_09.cs, Listing 13.12.

Listing 13.12 Encrypting Data (ch13_09.cs)

using System.IO; using System.Security.Cryptography; class ch13_09 { public static void Main() { TripleDESCryptoServiceProvider cyptoProvider = new TripleDESCryptoServiceProvider(); FileStream fileStream = File.Create("c:\c#\ch13\secret.dat"); CryptoStream cryptoStream = new CryptoStream(fileStream, cyptoProvider.CreateEncryptor(), CryptoStreamMode.Write); StreamWriter streamWriter = new StreamWriter(cryptoStream); streamWriter.WriteLine("No worries!"); streamWriter.Close(); fileStream = File.Create("c:\c#\ch13\secret.key"); BinaryWriter binaryWriter = new BinaryWriter(fileStream); binaryWriter.Write(cyptoProvider.Key); binaryWriter.Write(cyptoProvider.IV); binaryWriter.Close(); System.Console.WriteLine("Data encrypted, key stored."); } }

When you run ch13_09.cs, you'll see the message "Data encrypted, key stored." and the files secret.dat and secret.key will be created. If you take a look at secret.dat, all you'll see is gibberish:

C:\>type secret.dat \u8734\'38_^bET&:_,<=

Now it's time to decrypt that gibberish.

Decrypting Files

At this point, we have an encrypted file, secret.dat, and a file holding our encryption key and initialization vector (only the people authorized to do the encrypting and decrypting should have access to the encryption key and an initialization vector). To decrypt secret.dat, we need the encryption key (24 bytes) and initialization vector (8 bytes) from secret.key; we can read them in using a BinaryReader and assign them to a TripleDESCryptoServiceProvider object's Key and IV properties like this:

public static void Main() { TripleDESCryptoServiceProvider cyptoProvider = new TripleDESCryptoServiceProvider(); FileStream fileStream = File.OpenRead("c:\c#\ch13\secret.key"); BinaryReader binaryReader = new BinaryReader(fileStream); cyptoProvider.Key = binaryReader.ReadBytes(24); cyptoProvider.IV = binaryReader.ReadBytes(8); . . .

Now we can create a new CryptoStream object and use that object to create a StreamReader object to read in the encrypted data:

public static void Main() { TripleDESCryptoServiceProvider cyptoProvider = new TripleDESCryptoServiceProvider(); FileStream fileStream = File.OpenRead("c:\c#\ch13\secret.key"); BinaryReader binaryReader = new BinaryReader(fileStream); cyptoProvider.Key = binaryReader.ReadBytes(24); cyptoProvider.IV = binaryReader.ReadBytes(8); fileStream = File.OpenRead("c:\c#\ch13\secret.dat"); CryptoStream cryptoStream = new CryptoStream(fileStream, cyptoProvider.CreateDecryptor(), CryptoStreamMode.Read); StreamReader streamReader = new StreamReader(cryptoStream); . . .

All that's left is to read in the encrypted data, which will be decrypted automatically, and to display the resulting text, as you see in ch13_10.cs, Listing 13.13.

Listing 13.13 Decrypting Data (ch13_10.cs)

using System.IO; using System.Security.Cryptography; class ch13_10 { public static void Main() { TripleDESCryptoServiceProvider cyptoProvider = new TripleDESCryptoServiceProvider(); FileStream fileStream = File.OpenRead("c:\c#\ch13\secret.key"); BinaryReader binaryReader = new BinaryReader(fileStream); cyptoProvider.Key = binaryReader.ReadBytes(24); cyptoProvider.IV = binaryReader.ReadBytes(8); fileStream = File.OpenRead("c:\c#\ch13\secret.dat"); CryptoStream cryptoStream = new CryptoStream(fileStream, cyptoProvider.CreateDecryptor(), CryptoStreamMode.Read); StreamReader streamReader = new StreamReader(cryptoStream); System.Console.WriteLine(streamReader.ReadLine()); streamReader.Close(); } }

Here's what you see when you run ch13_10.cs. As you can see, we've been able to recover our encrypted data:

C:\c#\ch13>ch13_10 No worries!

And that's itnow we're using encryption and decryption.

Категории