In the previous example, cryptography is used to assist in user authentication. However, no steps are taken to hide data as it flows over the wire. Malicious users can eavesdrop and discover valuable information such as the ObjRef (where a client can be reached), or the e-mails of users that are currently online, and so on. The same problem occurs with communication between peers. Currently, messages flow over the network as plain text, which is visible to any user in the right place with a network sniffer.
You can solve this problem by adding a new class to the cryptography component, which you can use on both the client and web-server end. This is the EncryptedObject class.
In adding an encryption solution, you can use the same approach we used for signing data. In this case, you'll need a dedicated class, which we'll name EncryptedObject. The methods exposed by this class are quite similar to those provided by the SignedObject class, but the code involved is somewhat more complicated. This is because when you use asymmetric encryption you must encrypt data one block at a time. If you need to encrypt data that's larger than one block, you must divide it into multiple blocks, encrypt each one individually, and piece the encrypted blocks back together.
Here's an overview of how you would use the EncryptedObject:
First, create and configure a serializable object.
Create the EncryptedObject class. The EncryptedObject class provides a constructor that takes any object, along with the public key XML (which should be the public key of the recipient). This constructor serializes the object, encrypts it, and stores it in an internal member variable.
You can then convert the encrypted object into a byte array through .NET serialization using the Serialize() method. This is the data you'd send to the other peer.
The recipient deserializes the byte array into an EncryptedObject, using the shared Deserialize() method.
The recipient calls the DecryptContainedObject() method with its private key to retrieve the original object.
The EncryptedObject code is shown here. The Serialize() and Deserialize() methods are omitted, because they're identical to those used in the SignedObject class.
<Serializable()> _ Public Class EncryptedObject Private SerializedObject As New MemoryStream() Public Sub New(ByVal objectToEncrypt As Object, ByVal publicKeyXml As String) ' Serialize a copy of objectToEncrypt in memory. Dim f As New BinaryFormatter() Dim ObjectStream As New MemoryStream() f.Serialize(ObjectStream, objectToEncrypt) ObjectStream.Position = 0 Dim Rsa As New RSACryptoServiceProvider() Rsa.FromXmlString(publicKeyXml) ' 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 ' (Serialize and Deserialize methods omitted.) Public Function DecryptContainedObject(ByVal keyPairXml As String) As Object Dim Rsa As New RSACryptoServiceProvider() Rsa.FromXmlString(keyPairXml) ' Create the memory stream where the decrypted data ' will be stored. Dim ObjectStream As New MemoryStream() 'Dim ObjectBytes() As Byte = Me.SerializedObject.ToArray() Me.SerializedObject.Position = 0 ' Determine the block size for decrypting. Dim keySize As Integer = Rsa.KeySize / 8 ' Move through the data one block at a time. 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
Now, you only need to make minor changes to the ClientProcess class in order to use encryption with the EncryptedObject class. First, you need to define a Message class that will contain the information that's being sent:
<Serializable()> _ Public Class Message Public SenderAlias As String Public MessageBody As String Public Sub New(ByVal sender As String, ByVal body As String) Me.SenderAlias = sender Me.MessageBody = body End Sub End Class
You also need to modify the ITalkClient interface:
Public Interface ITalkClient ' The server calls this to forward a message to the appropriate client. Sub ReceiveMessage(ByVal encryptedMessage As EncryptedObject) End Interface
When sending a message, you need to construct a Message object and encrypt it. You don't need to use the Serialize() method to convert it to a byte stream because the .NET Remoting infrastructure can automatically convert serializable types for you. The full code is shown here, with the modified lines highlighted in bold. Note that the public XML information is retrieved from the web service as needed for the peer.
Public Sub SendMessage(ByVal emailAddress As String, ByVal messageBody As String) Dim PeerInfo As localhost.PeerInfo ' Check if the peer-connectivity information is cached. If RecentClients.Contains(emailAddress) Then PeerInfo = CType(RecentClients(emailAddress), localhost.PeerInfo) Else PeerInfo = DiscoveryService.GetPeerInfo(emailAddress) RecentClients.Add(PeerInfo.EmailAddress, PeerInfo) End If Dim ObjStream As New MemoryStream(PeerInfo.ObjRef) Dim f As New BinaryFormatter() Dim Obj As Object = f.Deserialize(ObjStream) Dim Peer As ITalkClient = CType(Obj, ITalkClient) Dim Message As New Message(Me.Alias, messageBody) Dim Package As New EncryptedObject(Message, PeerInfo.PublicKeyXml) Try Peer.ReceiveMessage(Package) Catch ' Ignore connectivity errors. End Try End Sub
When receiving a message, the peer simply decrypts the contents using its private key.
Private Sub ReceiveMessage(ByVal encryptedMessage As EncryptedObject) _ Implements ITalkClient.ReceiveMessage Dim Message As Message Message = CType(encryptedMessage.DecryptContainedObject( _ Me.Rsa.ToXmlString(True)), Message) RaiseEvent MessageReceived(Me, _ New MessageReceivedEventArgs(Message.MessageBody, Message.SenderAlias)) End Sub
The same technique can be applied to protect any data. For example, you could (and probably should) use it to encrypt messages exchanged between the client and discovery service.
The designs of the EncryptedObject and SignedObject classes lend themselves particularly well to being used together. For example, you can create a signed, encrypted message by wrapping a Message object in an EncryptedObject, and then wrapping the EncryptedObject in a SignedObject. (You could also do it the other way around, but the encrypt-and-sign approach is convenient because it allows you to validate the signature before you perform the decryption.)
Figure 11-5 diagrams this process.
Here's the code you would use to encrypt and sign the message:
Dim Message As New Message(Me.Alias, messageBody) ' Encrypt the message using the recipient's public key. Dim EncryptedPackage As New EncryptedObject(Message, PeerInfo.PublicKeyXml) ' Sign the message with the sender's private key. Dim SignedPackage As New SignedObject(Message, Me.Rsa.ToXmlString(True)) Try Peer.ReceiveMessage(SignedPackage) Catch ' Ignore connectivity errors. End Try
The recipient would then validate the signature, deserialize the encrypted object, and then decrypt it:
' Verify the signature. If Not encryptedPackage.ValidateSignature(PeerInfo.PublicKeyXml) Then ' Ignore this message. Else Dim EncryptedMessage As EncryptedObject EncryptedMessage = CType(encryptedPackage.GetObjectWithoutSignature, _ EncryptedObject) ' Decrypt the message. Dim Message As Message Message = CType(EncryptedMessage.DecryptContainedObject( _ Me.Rsa.ToXmlString(True)), Message) RaiseEvent MessageReceived(Me, _ New MessageReceivedEventArgs(Message.MessageBody, Message.SenderAlias)) End If
There's one other enhancement that you might want to make to this example. As described earlier, asymmetric encryption is much slower than symmetric encryption. In the simple message-passing example this won't make much of a difference, but if you need to exchange larger amounts of data it becomes much more important.
In this case, the solution is to use symmetric encryption. However, because both peers won't share a symmetric key, you'll have to create one dynamically and then encrypt it asymmetrically. The recipient will use its private key to decrypt the symmetric key, and then use the symmetric key to decrypt the remainder of the message.
This pattern is shown, in abbreviated form, with the following LargeEncryptedObject class. It includes the code used to encrypt the serializable object, but leaves out the asymmetric encryption logic used to encrypt the dynamic symmetric key for brevity. The code used for symmetric encryption is much shorter, because it can use a special object called the CryptoStream. The CryptoStream manages blockwise encryption automatically and can be used to wrap any other .NET stream object. For example, you can use a CryptoStream to perform automatic encryption before data is sent to a FileStream, or perform automatic decryption as it is read to memory. In the case of the LargeEncryptedObject, the CryptoStream wraps another memory stream.
<Serializable()> _ Public Class LargeEncryptedObject Private SerializedObject As New MemoryStream() Private EncryptedDynamicKey() As Byte Public Sub New(ByVal objectToEncrypt As Object, ByVal publicKeyXml As String) ' Generate the new symmetric key. ' In this example, we'll use the Rijndael algorithm. Dim Rijn As New RijndaelManaged() ' Encrypt the RijndaelManaged.Key and RijndaelManaged.IV properties. ' Store the data in the EncryptedDynamicKey member variable. ' (Asymmetric encryption code omitted.) ' Write the data to a stream that encrypts automatically. Dim cs As New CryptoStream(Me.SerializedObject,_ Rijn.CreateEncryptor(), CryptoStreamMode.Write) ' Serialize and encrypt the object in one step using the CryptoStream. Dim f As New BinaryFormatter() f.Serialize(cs, objectToEncrypt) ' Write the final block. cs.FlushFinalBlock() End Sub Public Function DecryptContainedObject(ByVal keyPairXml As String) As Object ' Generate the new symmetric key. Dim Rijn As New RijndaelManaged() ' Decrypt the EncryptedDynamic key member variable, and use it to set ' the RijndaelManaged.Key and RijndaelManaged.IV properties. ' (Asymmetric decryption code omitted.) ' Write the data to a stream that decrypts automatically. Dim ms As New MemoryStream() Dim cs As New CryptoStream(ms, Rijn.CreateDecryptor(), _ CryptoStreamMode.Write) ' Decrypt the object 1 KB at a time. Dim i, BytesRead As Integer Dim Bytes(1023) As Byte For i = 0 To Me.SerializedObject.Length BytesRead = Me.SerializedObject.Read(Bytes, 0, Bytes.Length) cs.Write(Bytes, 0, BytesRead) Next ' Write the final block. cs.FlushFinalBlock() ' Now deserialize the decrypted memory stream. ms.Position = 0 Dim f As New BinaryFormatter() Return f.Deserialize(ms) End Function ' (Serialize and Deserialize methods omitted.) End Class
A full description of the .NET cryptography classes and the CryptoStream is beyond the scope of this book.