|
|
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.
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
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; } } }
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.
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.
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
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; } } }
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
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; } } } }
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
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; } } } }
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
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); } } }
Listing 9-11: The EncryptionServerSinkProvider
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 } } }
When including the sink providers in your configuration files a s presented previously, the transfer will be encrypted as shown in Figure 9-7.
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.
|
|