The .NET Encryption Classes

SSL is an ideal solution for protecting mission-critical applications. But there are situations in which you might want the flexibility to encrypt only small details in large data messages or use encryption outside of IIS. The .NET Framework includes a full complement of cryptography classes that can help you with this task in the System.Security.Cryptography namespace.

Before you can harness encryption for use in a distributed system, you need to know a little about cryptography and the two forms of encryption in common use today. These standards form the basis of SSL and the .NET Framework cryptography classes.

Cryptographic algorithms use keys to scramble information. The two kinds of encryption are secret key (symmetric) encryption and public key (asymmetric) encryption. Both schemes rely on the strength of the keys, not on the secrecy of the algorithms used to perform the encryption. The algorithms aren't secret. In fact, they've been subject to public scrutiny by the cryptographic community, and their strengths and limitations are well known.

Symmetric encryption is the form of encryption that most individuals are familiar with. It uses the same key to both encrypt and decrypt information. Symmetric encryption is extremely fast, but it's easily compromised if another user knows the secret key value. Unfortunately, because this key has to be known by both the server and the client, it usually has to be transmitted in some way, making it very vulnerable.

The .NET Framework provides the following classes that implement private-key encryption algorithms:

  • DESCryptoServiceProvider

  • RC2CryptoServiceProvider

  • RijndaelManaged

  • TripleDESCryptoServiceProvider

Asymmetric encryption uses a key pair that consists of a public key and a corresponding private key. This is the type of encryption used natively by .NET to create strong-named assemblies, and we considered it in Chapter 2. With asymmetric encryption, information encrypted using the public key can be decrypted only using the matching private key. The reverse also applies: The public key is the only key that can decrypt data encrypted with the private key. Asymmetric encryption is mathematically complex. (For example, some algorithms depend on the fact that although multiplying two prime numbers to generate a new number is computationally easy, factoring the result to rediscover the original pair of prime numbers is quite difficult, especially for large numbers.) With asymmetric encryption, the private key is guarded carefully, whereas the public key value is published. If client A wants to send information to client B, it uses client B's public key to encrypt the data. This information can then be decrypted only using client B's private key, meaning that even the client that creates the message can't read it.

Asymmetric encryption is an elegant solution to the problems faced by symmetric encryption, but its added complexity comes with a significant cost: Using it is hundreds of times slower. Often, symmetric and asymmetric encryption are combined, such that asymmetric encryption is used to distribute a random key. This random key is then used to encrypt subsequent messages using symmetric encryption. This technique is used natively in SSL. If you attempt to encode all communication using asymmetric encryption, your application will probably perform terribly.

The .NET Framework provides the following classes that implement public-key encryption algorithms:

  • DSACryptoServiceProvider

  • RSACryptoServiceProvider

.NET also provides additional classes in the System.Security.Cryptography namespace that generate random numbers and create hash values and digital signatures, which can be used to verify data and ensure that it hasn't been altered in transmission. Some of these classes actually perform the appropriate cryptographic tasks in managed .NET code, while others are just thin .NET wrappers over the unmanaged CryptoAPI library.

Selective Asymmetric Encryption

Selective encryption is an attractive option if you need to create a remote component that isn't hosted in IIS, doesn't use SOAP, or just needs improved performance over SSL. The key disadvantage with selective encryption is that both the client and server need to participate actively. If the client accidentally submits unencrypted information, for example, an error might occur or at worst, invalid data could be committed to the database. For that reason, selective encryption is best when you have complete control over the client code, and it is less attractive for cross-platform or third-party integration projects. This section considers how you can add selective encryption to an XML Web service.

In most cases, a secure interaction begins when a client sends a request attempting to log in. Typically, this request contains two pieces of information: a username and a password. The password must be kept secret; otherwise the security of the entire application is compromised. Therefore, in a secure system, the client must begin by encrypting the password before sending the request. In this case, asymmetric encryption is the only option unless you have already distributed some sort of secure password to each and every client.

Fortunately, asymmetric encryption is quite easy to use, provided that the XML Web service exposes a public key. The client can encode its password using the XML Web service's public key, ensuring that only the XML Web service can decode this information. Figure 13-8 diagrams the process.

Figure 13-8. Encrypting information at the client

graphics/f13dp08.jpg

Listing 13-21 shows an XML Web service that creates a key and stores it in application state so that it can be reused for all requests. In this case, the key is used for RSA encryption (a standard developed by Ronald L. Rivest, Adi Shamir, and Leonard M. Adleman in 1977), and the default key size is used. Note that to use 128-bit encryption with Windows 2000, both the client and server require the Windows 2000 high-encryption pack (downloadable from Microsoft at http://www.microsoft.com/windows2000/downloads/recommended/encryption) or the Windows 2000 Service Pack 2.

Listing 13-21 An XML Web service that exposes a public key
 Imports System.Web.Services Imports System.Security.Cryptography Imports System.Text Public Class CryptographyTest     Inherits System.Web.Services.WebService     <WebMethod()> _     Public Function GetPublicKey() As String         ' Retrieve the key object.         Dim Key As RSACryptoServiceProvider = GetKeyFromState()         ' Return the private portion of the key only.         Return Key.ToXmlString(False)     End Function     Private Function GetKeyFromState() As RSACryptoServiceProvider         Dim Key As RSACryptoServiceProvider         ' Check if the key has been created yet.         ' This ensures that the key is only created once.         If Application("Key") Is Nothing Then             ' Create a key for RSA encryption.             Dim Parameters As New CspParameters()             Parameters.Flags = CspProviderFlags.UseMachineKeyStore             Key = New RSACryptoServiceProvider(Parameters)             ' Store the key in the server memory.             Application("Key") = Key         Else             Key = CType(Application("Key"), RSACryptoServiceProvider)         End If         Return Key     End Function End Class 

The XML Web service client can retrieve the key and use it to encrypt data. Keep in mind that encrypted data is always translated into a byte array. You could attempt to convert it back to a string using an Encoding object, but the resulting special characters might interfere with the XML serialization process required to send a SOAP message. Therefore, this approach is not recommended.

Incidentally, the XML version of the key looks something like this:

[View full width]

<RSAKeyValue> <Modulus>xMOD47YH9sadjqccA3mZZAuvFcUqfQ4pc9KzU6A6/BPTEKtrK3GY3jRkamwZ21JCht7/ graphics/ccc.gif12AQqatfSfmIav8Lqi3Jb8RwsG471XdHJBG80Aa7l7q3Mc8/QNiESNo7cWC/5scCa/ graphics/ccc.gif38r+57fqVtH4zChXxmxFL2fwm+dLS0dTyiMmE=</Modulus> <Exponent>AQAB</Exponent> </RSAKeyValue>

To test the encryption, you need to add a Web method that actually requires encrypted information. In this case, we'll use the Login method shown in Listing 13-22.

Listing 13-22 A Login method that requires an encrypted password
 <WebMethod()> _ Public Function Login(ByVal userName As String, _   ByVal password As Byte()) As Boolean     ' Retrieve the key object.     Dim Key As RSACryptoServiceProvider = GetKeyFromState()     ' Decrypt the password.     Dim DecryptedPassword As String     Dim Enc As New UnicodeEncoding()     DecryptedPassword = Enc.GetString(Key.Decrypt(password, False))     ' This code simply uses a hard-coded password.     ' Typically, you would look this value up in a database.     ' You would then create and return a ticket to indicate the     ' user is allowed, as demonstrated earlier.     If DecryptedPassword = "secret" Then         Return True     Else         Return False     End If End Function 

Note that the password parameter is a byte array, not a string. This hints at one unpleasant consequence of using manual encryption: You need to change the interface of the remote class to accommodate it. Of course, this change has at least one benefit. Namely, the client is unlikely to accidentally submit a clear-text password because the proxy class will throw an error if it doesn't receive the expected byte array data type.

To test this implementation, you can create a client-side method that reads the Web server's key, encrypts a password, and verifies that authentication succeeds (as shown in Listing 13-23).

Listing 13-23 Encrypting information at the client end
 Dim Proxy As New localhost.CryptographyTest() Dim Password As String = "secret" ' Construct a key object using the public portion of the key. ' The client will be able to encode, but not decode information. Dim Key As New RSACryptoServiceProvider() Key.FromXmlString(Proxy.GetPublicKey()) ' Encrypt the password. Dim Enc As New UnicodeEncoding() Dim EncryptedPassword As Byte() EncryptedPassword = Key.Encrypt(Enc.GetBytes(Password), False) ' Attempt to call Login() with the encrypted password. If Proxy.Login("test", EncryptedPassword) Then     MessageBox.Show("Authentication succeeded.") End If 

Note

For this encryption scheme to work properly, both the XML Web service and the client must use the same encoder (in this case, the System.Text.UnicodeEncoding class). If the client encodes information using the ASCIIEncoding class while the server uses UnicodeEncoding, the correct value won't be retrieved.


If you use a SOAP tracing tool (such as the one included with the Microsoft SOAP Toolkit), you can examine the request message as it is sent over the network. In an unencrypted XML Web service, the message appears as shown in Listing 13-24. With encryption, the message is more like that shown in Listing 13-25. (In this case, each number represents a byte value that can't be representable as text.)

Listing 13-24 An unencrypted SOAP message
 <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:xsd="http://www.w3.org/2001/XMLSchema"       xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">   <soap:Body>     <Login xmlns="http://tempuri.org/">       <userName>testuser</userName>       <password>secret</password>     </Login>   </soap:Body> </soap:Envelope> 
Listing 13-25 An encrypted SOAP message
 <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:xsd="http://www.w3.org/2001/XMLSchema"       xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">   <soap:Body>     <Login xmlns="http://tempuri.org/">       <userName>testuser</userName>       <password>56 67 16 180 176 188 160 39 187 29 85 204 163 243 149        16 222 138 75 56 117 61 128 87 218 83 223 121 22 206 129 33 117        7 197 181 89 230 216 17 174 0 34 96 111 177 60 163 107 99 6 12         223 176 143 251 85 76 50 143 19 202 50 20 14 153 59 158 116 156         187 28 183 194 0 198 61 115 10 214 211 146 240 80 130 55 199 84         50 37 33 172 136 111 11 62 117 106 13 178 96 214 31 157 19 235         170 240 222 44 51 253 176 44 116 241 15 245 218 129 34 182 213         32 149 86 54 67</password>     </Login>   </soap:Body> </soap:Envelope> 

Note that it's still possible to retrieve information such as which method the user is calling. If this information must also be protected, you're better off encrypting the entire message with SSL.

Note

You also can implement encryption by creating a custom SOAP extension for an XML Web service or a custom channel sink for .NET Remoting. Both of these approaches require more work and tightly couple your solution to a specific technology. They also encrypt the entire message, which means they perform more slowly than the selective encryption techniques shown in this example. However, these specialized solutions might be appealing if you need to reuse a specific encryption scheme that's independent of the actual business code and the type of data. You can find more information about both of these approaches in the MSDN reference.


Selective Symmetric Encryption

After a session has been started, it's imperative to switch to secret key encryption as soon as possible. Asymmetric encryption and decryption are computationally expensive, and encoded messages are significantly larger than they would be with symmetric encryption. To make the switch, the client and the XML Web service need to agree on a common secret key. You can use the user's existing password for this purpose, if it is sufficiently long, or the server can randomly generate a new key, in which case it needs to use asymmetric encryption to pass the key to the client. In both cases, the remote object will probably need to store the client's key in some type of state.

Whereas asymmetric encryption uses a public and a private key, symmetric encryption uses both a secret key and an initialization vector, which is a sequence of random bytes added to the message. Typically, a client will always use the same secret key (perhaps one derived from the user password) while selecting a different initialization vector for each session. However, this book doesn't consider these details at length. If you want to implement industrial-strength encryption, you might want to refer to a dedicated book about encryption theory. (In fact, entire technical works are devoted to explaining single-encryption algorithms, including Rijndael, the symmetric algorithm used in the next example.)

The first task is to define a structure in the XML Web service that represents the client key information:

 Public Class ClientKey     Public Key As Byte()     Public IV As Byte() End Class 

Next, you modify the Login Web method to accept client key information and store it in the Application collection, indexed by the client's username (as shown in Listing 13-26). As you might expect, the Key and IV values are actually encrypted before transmission using the XML Web service's public key. To ensure optimal performance, the Login method decrypts these values before storing the key. Otherwise, they would need to be decrypted each time the ClientKey object is used, which would impose a noticeable performance burden.

Listing 13-26 A Login method that stores client key information
 <WebMethod()> _ Public Function Login(ByVal userName As String, _   ByVal password As Byte(), ByVal clientKey As ClientKey) As Boolean     ' Retrieve the key object.     Dim Key As RSACryptoServiceProvider = GetKeyFromState()     ' Decrypt the password.     Dim DecryptedPassword As String     Dim Enc As New UnicodeEncoding()     DecryptedPassword = Enc.GetString(Key.Decrypt(password, False))     If DecryptedPassword = "secret" Then         ' Store the client's key in Application state.         ' To optimize performance, it must be decrypted first.         clientKey.IV = Key.Decrypt(clientKey.IV, False)         clientKey.Key = Key.Decrypt(clientKey.Key, False)         Application(userName) = clientKey         Return True     Else         Return False     End If End Function 

A new method is added, which returns a string of secret data, encrypted using the client's key (as shown in Listing 13-27). Note that once again both the client and server must agree on the same encryption and encoding standard; otherwise, this system will break down.

Listing 13-27 Encrypting data at the server
 <WebMethod()> _ Public Function GetSecretData(ByVal userName As String) As Byte()     ' Create a key object that is identical to the client's key.     Dim Key As ClientKey = CType(Application(userName), ClientKey)     Dim RM As New RijndaelManaged()     ' Create a memory stream.     Dim s As New MemoryStream()     ' Encrypt information into the memory stream.     Dim CryptStream As New CryptoStream(s, _       RM.CreateEncryptor(Key.Key, Key.IV), CryptoStreamMode.Write)     Dim w As New StreamWriter(s)     w.WriteLine("Secret data!")     w.Flush()     ' Now move the information out of the stream,     ' and into an array of bytes.     Dim r As StreamReader     Dim Bytes As Byte()     ReDim Bytes(s.Length)     s.Position = 0     s.Read(Bytes, 0, s.Length)     ' Clean up.     s.Close()     ' Return the final byte array of encrypted data.     Return Bytes End Function 

The code required for this task is somewhat complicated by the fact that symmetric encryption is stream-based. The string of information must be written to an in-memory cryptographic stream, which is automatically scrambled. Then the information must be read from the stream and converted to an array of bytes so that it can be safely returned from the Web method. (The Stream object is not natively supported as an XML Web service data type.)

The client code to retrieve the encrypted information is shown in Listing 13-28.

Listing 13-28 Decrypting data at the client end
 Dim Proxy As New localhost.CryptographyTest() Dim Password As String = "secret" ' Construct a key object using the public portion of the key. Dim Key As New RSACryptoServiceProvider() Key.FromXmlString(Proxy.GetPublicKey()) ' Encrypt the password. Dim Enc As New UnicodeEncoding() Dim EncryptedPassword As Byte() EncryptedPassword = Key.Encrypt(Enc.GetBytes(Password), False) ' Create a secret key for the server to send back data. ' This will be a symmetric key to ensure optimum performance. Dim RM As New RijndaelManaged() Dim ClientKey As New localhost.ClientKey() ' The values for this key are encrypted with the XML Web service's ' public key. ClientKey.Key = Key.Encrypt(RM.Key, False) ClientKey.IV = Key.Encrypt(RM.IV, False) If Proxy.Login("test", EncryptedPassword, ClientKey) Then    MessageBox.Show("Authentication succeeded.")    ' Try to retrieve some information.    Dim Data As Byte() = Proxy.GetSecretData("test")    ' Place the array of bytes into a memory stream.    Dim s As New MemoryStream()    s.Write(Data, 0, Data.Length)    ' Decrypt and display the data.    Dim cs As New CryptoStream(s, RM.CreateDecryptor(RM.Key, RM.IV), _      CryptoStreamMode.Read)    Dim r As New StreamReader(s)    s.Position = 0    MessageBox.Show("Retrieved: " & r.ReadToEnd())    s.Close() End If 

Now the encrypted string "Secret data!" will be successfully retrieved, decrypted, and displayed.

Note

Future versions of the .NET Framework will likely abstract away parts of this process with a declarative model. For example, you might be able to add a specific attribute to a class member or parameter to specify that it must be encrypted automatically by the runtime before transmission. Microsoft recently released the Web Services Enhancements kit (WSE) as a separate add-on, which can be downloaded from http://msdn.microsoft.com/webservices/building/wse. It includes support for using the emerging SOAP security standard WS-Security in an XML Web service. These extensions will likely be integrated into the core .NET Framework in future releases.


Why Custom Encryption is Not SSL

It's easy to be lured into the belief that this custom encryption scheme provides the equivalent of an SSL-encrypted session. This "do-it-yourself" frame of mind is often encouraged in programming books, although it's quite dangerous.

SSL is a mature, sophisticated protocol. As such, it provides some features that are not implemented in the above example. The problems in the custom implementation include the following:

  • No message authentication codes

    In other words, even though a malicious user can't read an encrypted message, the attacker could alter it, without the client or server realizing what had happened. This would probably lead to an application error, but it could be used as part of a more sophisticated exploit. To overcome this problem, you need to add keyed hash codes to every encrypted message. This is possible with the .NET cryptography classes, but it's more work.

  • No identity verification

    Because the custom authentication approach doesn't use certificates, there's no way for a client to be sure that the server it is communicating with isn't really a crafty hacker. Currently, the .NET Framework doesn't include any classes for validating certificates.

The lack of these features might be only a minor drawback if you're designing an internal application and the threat risk is small. However, if your application is dealing with highly sensitive information, these limitations might be unacceptable. The best approach is to carefully model the security risks and the types and motivations of potential attackers. If you could be subject to a man-in-the-middle attack (where an attacker uses privileged network access to intercept communication or even steal another computer's IP address), SSL might be the only viable choice. If you do decide to use the .NET cryptography classes, write as little code as possible, and always have it reviewed by a security expert.

Selective Encryption with .NET Remoting

You can use selective encryption in a .NET Remoting scenario in much the same way that you use it with a Web service, and with the same classes from the System.Security.Cryptography namespace. The process becomes quite a bit more complicated if you use SingleCall objects, however, because SingleCall objects are destroyed at the end of each method invocation. Unlike an XML Web service, .NET Remoting doesn't provide a global state store (such as the Application collection) to store keys, so a SingleCall .NET Remoting object would need to manually serialize its information to some data source. This process would not only be difficult to code, but it would also be subject to other security breaches (for example, if a malicious user accesses the database and reads the remote object's private key).

A much better approach is to use client-activated objects. Using encryption with a client-activated object is actually quite a bit easier than using it with an XML Web service because the remote object will need to serve only a single user the user who created the object. The remote object will also remain running so that all the required information can be retained in memory. This means you don't need to worry about using the Application collection or a database. Instead, the remote object uses a private member variable to store the automatically generated asymmetric server key and the client-submitted symmetric key. You can allow the client to retrieve the former and submit the latter using property procedures or methods.

Using Custom Classes to Wrap Encryption

If you're designing a secure service with .NET Remoting, you might be able to abstract away some of the encryption details by using custom objects. The basic strategy is to wrap the information that needs to be transmitted between the client and server in a custom object. This object can use property procedure logic to perform the encryption automatically. (This solution isn't possible with XML Web services because the property procedure logic for custom classes won't appear in the automatically generated proxy file.)

For example, you can use a UserInfo class to wrap the account information that must be submitted to the Login method, as shown in Listing 13-29. The constructor requires that you provide an RSACryptoServiceProvider instance when you create the object.

Listing 13-29 Automatic encryption with a custom class
 <Serializable()> _ Public Class UserInfo     <NonSerialized()> Private _RSA As RSACryptoServiceProvider     Private Encoding As New UnicodeEncoding()     Private _UserName As String     Private _Password As Byte()     Public Property RSA() As RSACryptoServiceProvider         Get             Return _RSA         End Get         Set(ByVal Value As RSACryptoServiceProvider)             _RSA = Value         End Set     End Property     Public Property UserName() As String         Get             Return _UserName         End Get         Set(ByVal Value As String)             _UserName = Value         End Set     End Property     Public Property Password() As String         Get             Return Encoding.GetString(_RSA.Decrypt(_Password, False))         End Get         Set(ByVal Value As String)             _Password = _RSA.Encrypt(Encoding.GetBytes(Value), False)         End Set     End Property     Public Sub New(ByVal encryptor As RSACryptoServiceProvider)         RSA = encryptor     End Sub End Class 

The Login method will then be slightly modified:

 Public Sub Login(userInfo As UserInfo)     ' (Code omitted.) End Sub 

The RSACryptoServiceProvider used for the encryption and decryption is specifically marked with a <NonSerialized> attribute. This ensures that the sensitive key information can't be sent over the wire. However, when receiving the UserInfo object, you'll need to set the RSA property before you can access any properties. If your RSACryptoServiceProvider encapsulates the full key pair, you'll be able to set and retrieve the password information. If the RSACrypto­ServiceProvider includes only the public key information, you'll be able to set the password information (encrypt) but not read it (decrypt).

Be warned, however, that although this approach simplifies the client code, it can make it difficult for you to extend the system or modify the encryption techniques. You might also end up with an unmanageable number of new classes. To counteract this tendency, you can create a generic encryption package that will work with any string, as shown in Listing 13-30.

Listing 13-30 A generic encrypted information package
 <Serializable()> _ Public Class EncryptionPackage     <NonSerialized()> Private _RSA As RSACryptoServiceProvider     Private Encoding As New UnicodeEncoding()     Private _Encoded As Byte()     Public Property RSA() As RSACryptoServiceProvider         Get             Return _RSA         End Get         Set(ByVal Value As RSACryptoServiceProvider)             _RSA = Value         End Set     End Property     Public Property Text() As String         Get             Return Encoding.GetString(_RSA.Decrypt(_Text, False))         End Get         Set(ByVal Value As String)             __Encoded = _RSA.Encrypt(Encoding.GetBytes(Value), False)         End Set     End Property     Public Sub New(ByVal encryptor As RSACryptoServiceProvider)         RSA = encryptor     End Sub End Class 

The client will then immediately recognize a method that requires encrypted information because it will use the EncryptionPackage data type. As written, this approach has two minor drawbacks. First, it supports only asymmetric encryption, which is slower and produces a larger encrypted message than symmetric encryption. Second, it doesn't support extremely large strings. The Encrypt method can work only with data up to one block size large. If the data is larger than one block, you must break it down into separate blocks, encrypt them separately, and then reassemble the encrypted blocks. Because it is stream based, symmetric encryption doesn't have this limitation. The example in the next section takes this limitation into account with a lengthier example.

Using .NET Serialization to Encrypt Classes

Another interesting approach is to serialize objects to byte arrays using .NET object serialization. This is particularly useful when you want to encrypt a group of related information. The basic approach is as follows:

  • Create a class that contains the information you want to encrypt. Make sure that it is serializable (as explained in Chapter 4).

  • Create an EncryptionPackage class that wraps this class. The EncryptionPackage class should provide methods that allow you to encrypt and decrypt the contained object.

  • Make the EncryptionPackage class serializable so that it can be sent to a remote component using .NET Remoting.

Listing 13-31 shows a class that follows this design and can be used to encrypt the contents of any serializable object. A full discussion of this code is beyond the scope of this chapter. However, the basics are easy to understand. The object is first converted to a stream of bytes using binary serialization. It is then asymmetrically encrypted and stored in a private memory stream named SerializedObject. The Deserialize method decrypts this data to a second memory stream and then deserializes and returns the original object.

The code is much longer than the previous example because it breaks down the data into the appropriate block size before it is encrypted, allowing you to asymmetrically encrypt data no matter what its size. You could develop a simpler class using symmetric encryption.

Listing 13-31 Encrypting a serializable object with a wrapper
 <Serializable()> _ Public Class EncryptionPackage     Private SerializedObject As New MemoryStream()     ' Encrypt the object and store it internally.     Public Sub New(ByVal objectToEncrypt As Object, _       ByVal rsa As RSACryptoServiceProvider)         ' Serialize a copy of objectToEncrypt in memory.         Dim f As New BinaryFormatter()         Dim ObjectStream As New MemoryStream()         f.Serialize(ObjectStream, objectToEncrypt)         ObjectStream.Position = 0         ' The block size depends on the key size.         Dim BlockSize As Integer         If rsa.KeySize = 1024 Then             BlockSize = 16         Else             BlockSize = 5         End If         ' Move through the data one block at a time.         Dim RawBlock(), EncryptedBlock() As Byte         Dim i As Integer         Dim Bytes As Integer = ObjectStream.Length         For i = 0 To Bytes Step BlockSize             If Bytes - i > BlockSize Then                 ReDim RawBlock(BlockSize - 1)             Else                 ReDim RawBlock(Bytes - i - 1)             End If             ' Copy a block of data.             ObjectStream.Read(RawBlock, 0, RawBlock.Length)             ' Encrypt the block of data.             EncryptedBlock = rsa.Encrypt(RawBlock, False)             ' Write the block of data.             Me.SerializedObject.Write(EncryptedBlock, _               0, EncryptedBlock.Length)         Next 
     End Sub     ' Decrypt the stored data, deserialize it, and return it as     ' a generic Object (because the code does not know the original type).     Public Function Decrypt(rsa As RsaCryptoServiceProvider) As Object         ' Create the memory stream where the decrypted data         ' will be stored.         Dim ObjectStream As New MemoryStream()         ' Determine the block size for decrypting.         Dim keySize As Integer = Rsa.KeySize / 8         ' Move through the data one block at a time.         Me.SerializedObject.Position = 0         Dim DecryptedBlock(), RawBlock() As Byte         Dim i As Integer         Dim Bytes As Integer = Me.SerializedObject.Length         For i = 0 To bytes - 1 Step keySize             If ((Bytes - i) > keySize) Then                 ReDim RawBlock(keySize - 1)             Else                 ReDim RawBlock(Bytes - i - 1)             End If             ' Copy a block of data.             Me.SerializedObject.Read(RawBlock, 0, RawBlock.Length)             ' Decrypt a block of data.             DecryptedBlock = rsa.Decrypt(RawBlock, False)             ' Write the decrypted data to the in-memory stream.             ObjectStream.Write(DecryptedBlock, 0, DecryptedBlock.Length)         Next         ObjectStream.Position = 0         Dim f As New BinaryFormatter()         Return f.Deserialize(ObjectStream)     End Function End Class 

To encrypt an object, you supply it in the EncryptionPackage constructor:

 ' MyObject is the object you want to encrypt. Dim MyObject As New MyClass() Dim Package As New EncryptionPackage(MyObject, publicRsa) ' Package now contains an encrypted copy of the object data. 

To decrypt the object, you use the Decrypt() method, and cast the returned object to the expected type:

 MyObject = CType(Package.Decrypt(privateRsa), MyClass) ' MyObject now contains the decrypted, deserialized object. 

Note that both of these steps force you to submit the appropriate RSACryptoServiceProvider object representing the public key or the full key pair. To ensure optimum security, this information is never stored in the class.

It's difficult to say categorically whether custom wrappers are a good approach for distributed applications that require a significant amount of encryption. As a general rule, the more custom encryption you add, the more you need to modify the interface of your server-side methods. Over time, this might make the system less manageable.

Authentication and encryption are widely identified as key areas where XML Web services and remote object technologies such as .NET Remoting must be improved in fact, Microsoft is probably considering these limitations even as you read this chapter. In a future version of the .NET Framework, you can expect more consistent, automated security functions that can be integrated into your code in a declarative fashion (probably with the use of attributes). Until then, you need to make the difficult decision between SSL and custom encryption.

Code Access Security

The final topic in this chapter is code access security, which is a new addition to .NET designed to restrict unknown or suspicious code. You might be aware that Java programs on the Windows platform run inside a protected "sandbox" that restricts the code from directly performing certain low-level operations. .NET extends this concept with code access security. Conceptually, code access security is a dynamic sandbox that gives some assemblies more permissions than others.

When the CLR first loads an assembly, it considers several details to determine the security level that should be allowed. These details, called evidence, include the following:

  • Zone

    The security level, which is based on whether the code is executed locally or accessed from a remote site. Zones in .NET are the same zones configured in Internet Explorer and include Internet, Local Intranet, Trusted Sites, Restricted Sites, and Local Machine.

  • URL

    A specific URL or file location for a specific resource, such as http://localhost/MyFiles/Assembly.dll or file:///c:\temp\Assembly.dll.

  • Application directory

    The directory from which the code is loaded.

  • Site

    Similar to the URL but less specific. For example, http://www.microsoft.com is a site.

  • Strong name

    The strong-name signature that identifies the assembly.

  • Publisher certificate

    The Authenticode digital signature of the assembly.

The CLR evaluates all of this information and then considers the security policy on the current computer. The security policy sets the allowed permissions based on the evidence. The security policy isn't defined anywhere in .NET code. Instead, it's stored in each computer's machine.config configuration file. You can edit this file manually or (more easily) use the .NET Framework Configuration tool to edit it. Just choose Settings | Control Panel | Administrative Tools | Microsoft .NET Framework Configuration. Ideally, you won't need to change security policy at all, particularly on the client side. Applying security policy changes to multiple client computers can be quite a challenge.

By default, code that a user executes directly from a local hard drive has unlimited permissions. Code access security becomes more important when one or more parts of the application are downloaded dynamically from a less-trusted source, such as the Internet. Chapter 15 tackles this issue in detail and shows how you can display the evidence for an assembly and customize security policy when needed.

Security and the Stack Walk

Security policy isn't worth much if there isn't an infrastructure that can enforce it. In .NET, code access security is enforced by the CLR, which uses a stack walk technique. The concept is quite simple. In a .NET application, certain actions trigger security checks. These security checks might occur when the program attempts to access a file or display a dialog box using the .NET Framework classes. However, these security checks pertain only to managed code in unmanaged code, all bets are off. Therefore, to execute any unmanaged code, an assembly needs full permissions.

When a security check is made, the CLR walks the stack to discover all the assemblies involved in the current request. Therefore, if a Windows application (assembly A) calls another component (assembly B), which in turn calls a third component (assembly C), which triggers a security check, the CLR examines the permissions of all three assemblies (as shown in Figure 13-9). If each assembly on the stack has been granted the permission demanded by the security check, the call succeeds. If any assembly has not been granted the demanded permission, the stack walk fails and a security exception is thrown.

Figure 13-9. A security stack walk

graphics/f13dp09.jpg

This stack walk process prevents the infamous luring attack, in which malicious code tricks more trusted code into doing something it can't do on its own. In other words, an assembly that has limited trust uses an assembly with full trust to perform a nefarious deed. This kind of attack is extremely difficult for developers to guard against without the help of .NET's stack-walking mechanism.

Remember that if you download code and then run it from the local hard drive, it automatically acquires full permissions (unless another rule, such as one that acts based on assembly signature, prevents it). The CLR has no way to know how the code was installed. On the other hand, if you launch the same code from the remote site, increased restrictions come into play. Chapter 15 explores this topic in more detail.

Note

Remember that code access security acts in addition to other levels of security. If a user attempts to access a file that is restricted based on the user account, for example, the program won't be able to read the file, regardless of the code access security level. If code access security restricts a given application from accessing the file system, however, the user won't be able to read any file, even one that could be accessed directly using Windows Explorer or another program.


Security Demands

You can trigger a stack walk manually using a security demand. This doesn't necessarily tighten the security for your application or component, but it can save time and simplify error handling. Suppose, for example, that you have a component that requires file access, which is used by another application that doesn't have this permission. If you don't use a security demand, a SecurityException will occur when your component attempts to access the file system. You need to specifically catch this exception to distinguish it from other possible problems. Depending on the design of your component, you might need to add this exception-handling code to several methods.

If, on the other hand, you demand file access permission in the constructor for your class, a SecurityException will be thrown immediately when the client attempts to create the class. This prevents the class from being created and subsequently placed in an invalid state after an operation fails. It also simplifies your exception-handling code and identifies the problem immediately.

You can use security demands in two ways. You can add an imperative security demand at any point in your code by using the appropriate permission class (which always derives from System.Security.CodeAccessPermission). Here's a list of permission objects that are a part of the .NET class library:

 System.Data.Common.DBDataPermission System.Drawing.Printing.PrintingPermission System.Messaging.MessageQueuePermission System.Net.DnsPermission System.Net.SocketPermission System.Net.WebPermission System.Security.Permissions.EnvironmentPermission System.Security.Permissions.FileDialogPermission System.Security.Permissions.FileIOPermission System.Security.Permissions.IsolatedStoragePermission System.Security.Permissions.PublisherIdentityPermission System.Security.Permissions.ReflectionPermission System.Security.Permissions.RegistryPermission System.Security.Permissions.ResourcePermissionBase System.Security.Permissions.SecurityPermission System.Security.Permissions.SiteIdentityPermission System.Security.Permissions.StrongNameIdentityPermission System.Security.Permissions.UIPermission System.Security.Permissions.UrlIdentityPermission System.Security.Permissions.ZoneIdentityPermission 

Listing 13-32 shows how you use an imperative security check to demand a file access permission in a class constructor.

Listing 13-32 An imperative security demand
 Public Sub New(ByVal path as string)     ' Check for permissions on the specified file.     Dim Permission As New FileIOPermission( _       FileIOPermissionAccess.Read, path)     ' Force a stack walk.     Permission.Demand()     ' (Other code omitted.) End Sub 

Declarative demands work similarly but allow neater coding. For every permission class, a corresponding attribute class derives from System.Security.Permissions.SecurityAttribute. You use the attribute to mark a method. When the method is invoked (but before any code is executed), the security walk is performed. Listing 13-33 shows an example.

Listing 13-33 A declarative security demand
 <FileIOPermissionAttribute(SecurityAction.RequestMinimum, _  All := "C:\ ")> _ Public Sub New(ByVal path as string)     ' (Other code omitted.) End Sub 

Security attributes enable you to separate a code access security demand from the business logic in your code. However, you can't use this technique if you need to evaluate certain conditions and then decide whether to perform a demand or if you want to perform a demand with variable information.

You also can apply a declarative security demand to a class or an assembly, in which case it executes when the class is created or the assembly is accessed.

Security Assert, Deny, and PermitOnly

Code access security also enables you to assert permissions to stop a stack walk. By default, a stack walk continues until all assemblies on the stack have been verified. If the stack walk discovers a method that explicitly asserts the permission, however, the CLR stops checking and decides that the security requirement has been met. Therefore, an assertion could allow a less-trusted piece of code to use a more fully trusted assembly to perform an action such as writing to a file. It's the responsibility of the asserting component to ensure that this capability can't be exploited for the wrong purposes.

For example, you might create a component that writes a small amount of information to a predetermined file. To use your class, an assembly will need full file I/O permissions. By using a security assertion, however, you can allow your code to perform its work, regardless of the caller. Of course, there's a darker side to security assertions as well. They make it easy to create code that's susceptible to a luring attack. Quite simply, it is hard to anticipate the ways that a permission could be used maliciously. For example, code might write a large amount of data using your component and fill up the hard drive. Alternatively, if you allow the calling code to specify the filename, the component could be tricked into overwriting a valuable system file.

You can assert permissions imperatively by creating a permission object and using the Assert method, or you can assert them declaratively, as shown in Listing 13-34.

Listing 13-34 A declarative security assertion
 <FileIOPermission(SecurityAction.Assert, All := "C:\")> _ Public Sub LogData()     ' If this assembly is permitted to access C:\, this method     ' will always succeed, even if the caller has insufficient     ' privileges.     ' (Code omitted.) End Sub 

Finally, .NET provides support for revoking permissions. An assembly can use this capability to prevent the other methods it calls from performing certain operations. Listing 13-35 presents an example.

Listing 13-35 Denying access to multiple permissions through a PermissionSet
 ' Create a permission set that represents read access to the TEMP ' environment variable and write access to files in C:\ Dim Permissions As New PermissionSet(PermissionState.None) Permissions.AddPermission( _   New EnvironmentPermission(EnvironmentPermissionAccess.Read, "TEMP")) Permissions.AddPermission( _   New FileIOPermission(FileIOPermissionAccess.Write, "C:\")) ' Deny this permissions from the current stack frame. Permissions.Deny() ' If the MyComponent.DoSomething() attempts to read a file or ' environment variables that we have denied, a SecurityException will ' occur. MyComponent.DoSomething() 

One exception applies to the preceding rule. If you call a method that asserts a required security permission, the action will succeed even if the caller has been denied the permission. This is because the stack walk will never make it to the caller's method, and therefore the denial will never come into effect (as shown in Figure 13-10). Thus, security denials are, on their own, an insufficient way of restricting the permissions of a trusted assembly.

Figure 13-10. Security assertions and denials

graphics/f13dp10.jpg

Finally, you can use the PermitOnly method to revoke all privileges except those you identify specifically. Keep in mind that these permissions are revoked only for the current call. When the stack is cleared and a new method is executed, the permissions will be present and active. If you want to deny a permission for all methods in a class, you can apply a permission attribute to the class declaration.

Note

The .NET code access security framework is a rich and extensible model. For more information about how it works and the advanced techniques you can use with it, you can refer to the excellent book Visual Basic .NET Code Access Security Handbook (Lippert) from Wrox.




Microsoft. NET Distributed Applications(c) Integrating XML Web Services and. NET Remoting
MicrosoftВ® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting (Pro-Developer)
ISBN: 0735619336
EAN: 2147483647
Year: 2005
Pages: 174

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net