Encrypting the Transfer

Even though using an asymmetric/symmetric combination such as HTTPS/SSL for the encryption of the network traffic provides the only real security, in some situations HTTPS isn't quite helpful.

First, .NET Remoting by default only supports encryption when using an HTTP channel and when hosting the server-side components in IIS. If you want to use a TCP channel or host your objects in a Windows service, there's no default means of secure communication.

Second, even if you use IIS to host your components, callbacks that are employed with event notification will not be secured. This is because your client (which is the server for the callback object) does not publish its objects using HTTPS, but only HTTP.

Essential Symmetric Encryption

Symmetric encryption is based on one key fact: client and server will have access to the same encryption key. This key is not a password as you might know it, but instead is a binary array in common sizes from 40 to 192 bits. Additionally, you have to choose from among a range of encryption algorithms supplied with the .NET Framework: DES, TripleDES, RC2, or Rijndael.

To generate a random key for a specified algorithm, you can use the following code snippet. You will find the key in the byte[] variable mykey afterwards.

 String algorithmName = "TripleDES"; SymmetricAlgorithm alg =  SymmetricAlgorithm.Create(algorithmName); int keylen = 128; alg.KeySize = keylen; alg.GenerateKey(); byte[] mykey = alg.Key; 

Because each algorithm has a limited choice of valid key lengths, and because you might want to save this key to a file, you can run the separate KeyGenerator console application, which is shown in Listing 9-6.

Listing 9-6: A Complete Keyfile Generator

start example
 using System; using System.IO; using System.Security.Cryptography; class KeyGen {    static void Main(string[] args)    {       if (args.Length != 1 && args.Length != 3)       {          Console.WriteLine("Usage:");          Console.WriteLine("KeyGenerator <Algorithm> [<KeySize> <Outputfile>]");          Console.WriteLine("Algorithm can be: DES, TripleDES, RC2 or Rijndael");          Console.WriteLine();          Console.WriteLine("When only <Algorithm> is specified, the program");          Console.WriteLine("will print a list of valid key sizes.");          return;       }       String algorithmname = args[0];       SymmetricAlgorithm alg = SymmetricAlgorithm.Create(algorithmname);       if (alg == null)       {          Console.WriteLine("Invalid algorithm specified.");          return;       }       if (args.Length == 1)       {          // just list the possible key sizes          Console.WriteLine("Legal key sizes for algorithm {0}:",algorithmname);          foreach (KeySizes size in alg.LegalKeySizes)          {             if (size.SkipSize != 0)             {                for (int i = size.MinSize;i<=size.MaxSize;i=i+size.SkipSize)                {                   Console.WriteLine("{0} bit", i);                }             }             else             {                if (size.MinSize != size.MaxSize)                {                   Console.WriteLine("{0} bit", size.MinSize);                   Console.WriteLine("{0} bit", size.MaxSize);                }                else                {                   Console.WriteLine("{0} bit", size.MinSize);                }             }          }          return;       }       // user wants to generate a key       int keylen = Convert.ToInt32(args[1]);       String outfile = args[2];       try       {          alg.KeySize = keylen;          alg.GenerateKey();          FileStream fs = new FileStream(outfile,FileMode.CreateNew);          fs.Write(alg.Key,0,alg.Key.Length);          fs.Close();          Console.WriteLine("{0} bit key written to {1}.",                       alg.Key.Length * 8,                       outfile);       }       catch (Exception e)       {          Console.WriteLine("Exception: {0}" ,e.Message);          return;       }    } } 
end example

When this key generator is invoked with KeyGenerator.exe (without any parameters), it will print a list of possible algorithms. You can then run KeyGenerator.exe <AlgorithmName> to get a list of possible key sizes for the chosen algorithm. To finally generate the key, you have to start KeyGenerator.exe <AlgorithmName> <KeySize> <OutputFile>. To generate a 128-bit key for a TripleDES algorithm and save it in c:\testfile.key, run KeyGenerator.exe TripleDES 128 c:\testfile.key.

The Initialization Vector

Another basic of symmetric encryption is the use of a random initialization vector (IV). This is again a byte array, but it's not statically computed during the application's development. Instead, a new one is generated for each encryption taking place.

To successfully decrypt the message, both the key and the initialization vector have to be known to the second party. The key is determined during the application's deployment (at least in the following example) and the IV has to be sent via remoting boundaries with the original message. The IV is therefore not secret on its own.

Creating the Encryption Helper

Next I show you how to build this sink in the same manner as the previous CompressionSink, which means that the sink's core logic will be extracted to a helper class. I call this class EncryptionHelper. The encryption helper will implement two methods, ProcessOutboundStream() and ProcessInboundStream(). The methods' signatures look like this:

 public static Stream ProcessOutboundStream(           Stream inStream,           String algorithm,           byte[] encryptionkey,           out byte[] encryptionIV) public static Stream ProcessInboundStream(           Stream inStream,           String algorithm,           byte[] encryptionkey,           byte[] encryptionIV) 

As you can see in the signature, both methods take a stream, the name of avalid cryptoalgorithm, and a byte array that contains the encryption key as parameters. The first method is used to encrypt the stream. It also internally generates the IV and returns it as an out parameter. This IV then has to be serialized by the sink and passed to the other party in the remoting call. ProcessInboundStream(), on the other hand, expects the IV to be passed to it, so this value has to be obtained by the sink before calling this method. The implementation of these helper methods can be seen in Listing 9-7.

Listing 9-7: The EncryptionHelper Encapsulates the Details of the Cryptographic Process

start example
 using System; using System.IO; using System.Security.Cryptography; namespace EncryptionSink {    public class EncryptionHelper    {       public static Stream ProcessOutboundStream(          Stream inStream,          String algorithm,          byte[] encryptionkey,          out byte[] encryptionIV)       {          Stream outStream = new System.IO.MemoryStream();          // setup the encryption properties          SymmetricAlgorithm alg = SymmetricAlgorithm.Create(algorithm);          alg.Key = encryptionkey;          alg.GenerateIV();          encryptionIV = alg.IV;          CryptoStream encryptStream = new CryptoStream(             outStream,             alg.CreateEncryptor(),             CryptoStreamMode.Write);          // write the whole contents through the new streams          byte[] buf = new Byte[1000];          int cnt = inStream.Read(buf,0,1000);          while (cnt>0)          {             encryptStream.Write(buf,0,cnt);             cnt = inStream.Read(buf,0,1000);          }          encryptStream.FlushFinalBlock();          outStream.Seek(0,SeekOrigin.Begin);          return outStream;       }       public static Stream ProcessInboundStream(              Stream inStream,              String algorithm,              byte[] encryptionkey,              byte[] encryptionIV)       {           // setup decryption properties           SymmetricAlgorithm alg = SymmetricAlgorithm.Create(algorithm);           alg.Key = encryptionkey;           alg.IV = encryptionIV;          // add the decryptor layer to the stream          Stream outStream = new CryptoStream(inStream,             alg.CreateDecryptor(),             CryptoStreamMode.Read);          return outStream;       }    } } 
end example

Creating the Sinks

The EncryptionClientSink and EncryptionServerSink look quite similar to the previous compression sinks. The major difference is that they have custom constructors that are called from their sink providers to set the specified encryption algorithm and key. For outgoing requests, the sinks will set the X-Encrypt header to "yes" and store the initialization vector in Base64 coding in the X-EncryptIV header. The complete client-side sink is shown in Listing 9-8.

Listing 9-8: The EncryptionClientSink

start example
 using System; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Messaging; using System.IO; using System.Text; namespace EncryptionSink {    public class EncryptionClientSink: BaseChannelSinkWithProperties,                                    IClientChannelSink    {       private IClientChannelSink _nextSink;       private byte[] _encryptionKey;       private String _encryptionAlgorithm;       public EncryptionClientSink(IClientChannelSink next,          byte[] encryptionKey,          String encryptionAlgorithm)       {          _encryptionKey = encryptionKey;          _encryptionAlgorithm = encryptionAlgorithm;          _nextSink = next;       }       public void ProcessMessage(IMessage msg,          ITransportHeaders requestHeaders,          Stream requestStream,          out ITransportHeaders responseHeaders,          out Stream responseStream)       {          byte[] IV;          requestStream = EncryptionHelper.ProcessOutboundStream(requestStream,             _encryptionAlgorithm,_encryptionKey,out IV);          requestHeaders["X-Encrypt"]="yes";          requestHeaders["X-EncryptIV"]= Convert.ToBase64String(IV);          // forward the call to the next sink          _nextSink.ProcessMessage(msg,             requestHeaders,             requestStream,             out responseHeaders,             out responseStream);          if (responseHeaders["X-Encrypt"] != null &&             responseHeaders["X-Encrypt"].Equals("yes"))          {             IV = Convert.FromBase64String(                      (String) responseHeaders["X-EncryptIV"]);             responseStream = EncryptionHelper.ProcessInboundStream(                responseStream,                _encryptionAlgorithm,                _encryptionKey,                IV);          }       }       public void AsyncProcessRequest(IClientChannelSinkStack sinkStack,                                 IMessage msg,                                 ITransportHeaders headers,                                 Stream stream)       {          byte[] IV;          stream = EncryptionHelper.ProcessOutboundStream(stream,             _encryptionAlgorithm,_encryptionKey,out IV);          headers["X-Encrypt"]="yes";          headers["X-EncryptIV"]= Convert.ToBase64String(IV);          // push onto stack and forward the request          sinkStack.Push(this,null);          _nextSink.AsyncProcessRequest(sinkStack,msg,headers,stream);       }       public void AsyncProcessResponse(IClientResponseChannelSinkStack sinkStack,                                     object state,                                     ITransportHeaders headers,                                     Stream stream)       {          if (headers["X-Encrypt"] != null && headers["X-Encrypt"].Equals("yes"))          {             byte[] IV =                Convert.FromBase64String((String) headers["X-EncryptIV"]);             stream = EncryptionHelper.ProcessInboundStream(                stream,                _encryptionAlgorithm,                _encryptionKey,                IV);          }          // forward the request          sinkStack.AsyncProcessResponse(headers,stream);       }       public Stream GetRequestStream(IMessage msg,                                         ITransportHeaders headers)       {          return null; // request stream will be manipulated later       }       public IClientChannelSink NextChannelSink {          get          {             return _nextSink;          }       }    } } 
end example

The EncryptionServerSink shown in Listing 9-9 works basically in the same way as the CompressionServerSink does. It first checks the headers to determine whether the request has been encrypted. If this is the case, it retrieves the encryption initialization vector from the header and calls EncryptionHelper to decrypt the stream.

Listing 9-9: The EncryptionServerSink

start example
 using System; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting; using System.Runtime.Remoting.Messaging; using System.IO; namespace EncryptionSink {    public class EncryptionServerSink: BaseChannelSinkWithProperties,                                  IServerChannelSink    {       private IServerChannelSink _nextSink;       private byte[] _encryptionKey;       private String _encryptionAlgorithm;       public EncryptionServerSink(IServerChannelSink next, byte[] encryptionKey,                String encryptionAlgorithm)       {          _encryptionKey = encryptionKey;          _encryptionAlgorithm = encryptionAlgorithm;          _nextSink = next;       }       public ServerProcessing ProcessMessage(IServerChannelSinkStack sinkStack,          IMessage requestMsg,          ITransportHeaders requestHeaders,          Stream requestStream,          out IMessage responseMsg,          out ITransportHeaders responseHeaders,          out Stream responseStream) {          bool isEncrypted=false;          //checking the headers          if (requestHeaders["X-Encrypt"] != null &&             requestHeaders["X-Encrypt"].Equals("yes"))          {             isEncrypted = true;             byte[] IV = Convert.FromBase64String(                (String) requestHeaders["X-EncryptIV"]);             // decrypt the request             requestStream = EncryptionHelper.ProcessInboundStream(                requestStream,                _encryptionAlgorithm,                _encryptionKey,                IV);          }          // pushing onto stack and forwarding the call,          // the flag "isEncrypted" will be used as state          sinkStack.Push(this,isEncrypted);          ServerProcessing srvProc = _nextSink.ProcessMessage(sinkStack,             requestMsg,             requestHeaders,             requestStream,             out responseMsg,             out responseHeaders,             out responseStream);          if (isEncrypted)          {             // encrypting the response if necessary             byte[] IV;             responseStream =                EncryptionHelper.ProcessOutboundStream(responseStream,                _encryptionAlgorithm,_encryptionKey,out IV);             responseHeaders["X-Encrypt"]="yes";             responseHeaders["X-EncryptIV"]= Convert.ToBase64String(IV);          }          // returning status information          return srvProc;       }       public void AsyncProcessResponse(IServerResponseChannelSinkStack sinkStack,          object state,          IMessage msg,          ITransportHeaders headers,          Stream stream)       {          // fetching the flag from the async-state          bool isEncrypted = (bool) state;          if (isEncrypted)          {             // encrypting the response if necessary             byte[] IV;             stream = EncryptionHelper.ProcessOutboundStream(stream,                _encryptionAlgorithm,_encryptionKey,out IV);             headers["X-Encrypt"]="yes";             headers["X-EncryptIV"]= Convert.ToBase64String(IV);          }          // forwarding to the stack for further ProcessIng          sinkStack.AsyncProcessResponse(msg,headers,stream);       }       public Stream GetResponseStream(IServerResponseChannelSinkStack sinkStack,          object state,          IMessage msg,          ITransportHeaders headers)       {          return null;       }       public IServerChannelSink NextChannelSink {          get {             return _nextSink;          }       }    } } 
end example

Creating the Providers

Contrary to the previous sink, the EncryptionSink expects certain parameters to be present in the configuration file. The first one is "algorithm", which specifies the cryptographic algorithm that should be used (DES, TripleDES, RC2, or Rijndael). The second parameter, "keyfile", specifies the location of the previously generated symmetric keyfile. The same file has to be available to both the client and the server sink.

The following excerpt from a configuration file shows you how the client-side sink will be configured:

  <configuration>   <system.runtime.remoting>    <application>     <channels>      <channel ref="http">      <clientProviders>       <formatter ref="soap" />       <provider type="EncryptionSink.EncryptionClientSinkProvider, EncryptionSink"                              algorithm="TripleDES" keyfile="testkey.dat" />       </clientProviders>     </channel>    </channels>   </application>  </system.runtime.remoting> </configuration> 

In the following snippet you see how the server-side sink can be initialized:

  <configuration>   <system.runtime.remoting>    <application>     <channels>      <channel ref="http" port="5555">       <serverProviders>       <provider type="EncryptionSink.EncryptionServerSinkProvider, EncryptionSink"                              algorithm="TripleDES" keyfile="testkey.dat" />       <formatter ref="soap"/>       </serverProviders>      </channel>     </channels>    </application>   </system.runtime.remoting> </configuration> 

You can access additional parameters in the sink provider's constructor as shown in the following source code fragment:

 public EncryptionClientSinkProvider(IDictionary properties,         ICollection providerData) {    String encryptionAlgorithm = (String) properties["algorithm"]; } 

In addition to reading the relevant configuration file parameters, both the client-side sink provider (shown in Listing 9-10) and the server-side sink provider (shown in Listing 9-11) have to read the specified keyfile and store it in a byte array. The encryption algorithm and the encryption key are then passed to the sink's constructor.

Listing 9-10: The EncryptionClientSinkProvider

start example
 using System; using System.IO; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting; using System.Collections; namespace EncryptionSink {    public class EncryptionClientSinkProvider: IClientChannelSinkProvider    {       private IClientChannelSinkProvider _nextProvider;       private byte[] _encryptionKey;       private String _encryptionAlgorithm;       public EncryptionClientSinkProvider(IDictionary properties,                 ICollection providerData)       {          _encryptionAlgorithm = (String) properties["algorithm"];          String keyfile = (String) properties["keyfile"];          if (_encryptionAlgorithm == null || keyfile == null)          {             throw new RemotingException("'algorithm' and 'keyfile' have to " +                "be specified for EncryptionClientSinkProvider");          }          // read the encryption key from the specified fike          FileInfo fi = new FileInfo(keyfile);          if (!fi.Exists)          {             throw new RemotingException("Specified keyfile does not exist");          }          FileStream fs = new FileStream(keyfile,FileMode.Open);          _encryptionKey = new Byte[fi.Length];          fs.Read(_encryptionKey,0,_encryptionKey.Length);       }       public IClientChannelSinkProvider Next       {          get {return _nextProvider; }          set {_nextProvider = value;}       }       public IClientChannelSink CreateSink(IChannelSender channel, string url,              object remoteChannelData)       {          // create other sinks in the chain          IClientChannelSink next = _nextProvider.CreateSink(channel,             url, remoteChannelData);          // put our sink on top of the chain and return it          return new EncryptionClientSink(next,_encryptionKey,             _encryptionAlgorithm);       }    } } 
end example

Listing 9-11: The EncryptionServerSinkProvider

start example
 using System; using System.IO; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting; using System.Collections; namespace EncryptionSink {    public class EncryptionServerSinkProvider: IServerChannelSinkProvider    {       private byte[] _encryptionKey;       private String _encryptionAlgorithm;       private IServerChannelSinkProvider _nextProvider;       public EncryptionServerSinkProvider(IDictionary properties,           ICollection providerData)       {         _encryptionAlgorithm = (String) properties["algorithm"];         String keyfile = (String) properties["keyfile"];           if (_encryptionAlgorithm == null || keyfile == null)           {             throw new RemotingException("'algorithm' and 'keyfile' have to " +                "be specified for EncryptionServerSinkProvider");           }           // read the encryption key from the specified fike           FileInfo fi = new FileInfo(keyfile);           if (!fi.Exists)           {              throw new RemotingException("Specified keyfile does not exist");           }           FileStream fs = new FileStream(keyfile,FileMode.Open);           _encryptionKey = new Byte[fi.Length];           fs.Read(_encryptionKey,0,_encryptionKey.Length);        }        public IServerChannelSinkProvider Next        {           get {return _nextProvider; }           set {_nextProvider = value;}        }        public IServerChannelSink CreateSink(IChannelReceiver channel)       {           // create other sinks in the chain           IServerChannelSink next = _nextProvider.CreateSink(channel);           // put our sink on top of the chain and return it           return new EncryptionServerSink(next,              _encryptionKey,_encryptionAlgorithm);       }       public void GetChannelData(IChannelDataStore channelData)       {           // not yet needed       }    } } 
end example

When including the sink providers in your configuration files a s presented previously, the transfer will be encrypted as shown in Figure 9-7.

click to expand
Figure 9-7: A TCP-trace of the encrypted HTTP traffic

You can, of course, also chain the encryption and compression sinks together to receive an encrypted and compressed stream.




Advanced  .NET Remoting C# Edition
Advanced .NET Remoting (C# Edition)
ISBN: 1590590252
EAN: 2147483647
Year: 2002
Pages: 91
Authors: Ingo Rammer

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