Custom Soap Extensions
For those who want their data encrypted in a single web service call there’s a very good way to do it as shown in Encrypting SOAP Messages article by Rob Howard at MSDN.
While trying to use such solution I recognize some bugs and that it’s half implemented. Here is my working version that supports Envelope, Body and Method target encryption. There’s also a TracingExtension that you can use to trace request and response messages in both service and client projects. Take your time to look it.
using System;
using System.IO;
using System.Xml;
using System.Text;
using System.Security.Cryptography;
using System.Web.Services.Protocols;
namespace SoapExtensions
{
/// <summary>
/// The soap EncryptionExtension class
/// Service:
/// [EncryptionExtension(Decrypt = DecryptMode.Request, Encrypt = EncryptMode.Response, Target=Target.[AnyTarget])]
/// Client:
/// - Add reference to SoapExtensions .dll
/// - Add 'using SoapExtensions' directive to Reference.cs file
/// [EncryptionExtension(Encrypt = EncryptMode.Request, Decrypt = DecryptMode.Response, Target=Target.[AnyTarget])]
/// </summary>
public class EncryptionExtension : SoapExtension
{
#region Fields
//memory streams containing the SOAP request & response
private Stream requestStream;
private Stream responseStream;
//the configured attributes
private DecryptMode decryptMode;
private EncryptMode encryptMode;
private Target target;
//encryption keys
private static Byte[] key = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
private static Byte[] IV = { 0x13, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef };
#endregion
#region Overriden Methods
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return attribute;
}
public override object GetInitializer(Type serviceType)
{
return typeof(EncryptionExtension);
}
public override void Initialize(object initializer)
{
//cast the initiualizer argument to a encryption attribute
EncryptionExtensionAttribute attribute = (EncryptionExtensionAttribute)initializer;
//find the mode we should be in
this.decryptMode = attribute.Decrypt;
this.encryptMode = attribute.Encrypt;
this.target = attribute.Target;
}
/// <summary>
/// Allows access to the memory stream containing the SOAP request or response
/// </summary>
/// <param name="stream">SOAP request before deserialization</param>
/// <returns>SOAP response after serialization</returns>
public override Stream ChainStream(Stream stream)
{
this.requestStream = stream;
this.responseStream = new MemoryStream();
return responseStream;
}
/// <summary>
/// Override the SoapExtension method for process the raw soap message
/// at any stage in its life cycle
/// </summary>
/// <param name="message">The soap message</param>
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
{
try
{
//attemps to encrypt the soap message
Encrypt();
}
catch (Exception ex)
{
Console.WriteLine(EncryptionExtensionMessage.CANNOT_ENCRYPT);
Console.WriteLine(ex.Message);
}
} break;
case SoapMessageStage.BeforeDeserialize:
{
try
{
//attemp to decrypt the soap message
Decrypt();
}
catch (Exception ex)
{
Console.WriteLine(EncryptionExtensionMessage.CANNOT_DECRYPT);
Console.WriteLine(ex.Message);
}
} break;
case SoapMessageStage.AfterDeserialize:
break;
default:
throw new Exception(EncryptionExtensionMessage.INVALID_STAGE);
}
}
#endregion
#region Decryption
/// <summary>
/// Main decryption method
/// </summary>
private void Decrypt()
{
if ((decryptMode == DecryptMode.Request)
|| (decryptMode == DecryptMode.Response))
{
//the stream to carry the decrypted soap
MemoryStream decryptedStream = new MemoryStream();
CopyStream(requestStream, decryptedStream);
decryptedStream = DecryptSoap(decryptedStream);
CopyStream(decryptedStream, responseStream);
}
else
{
//don't have to decrypt
CopyStream(requestStream, responseStream);
}
responseStream.Position = 0;
}
/// <summary>
///
/// </summary>
/// <param name="streamToDecrypt">The stream to decrypt</param>
/// <returns></returns>
public MemoryStream DecryptSoap(Stream streamToDecrypt)
{
streamToDecrypt.Position = 0;
//read the soap stream into a xml doc
XmlTextReader reader = new XmlTextReader(streamToDecrypt);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(reader);
//add a namespace manager to the xml doc
XmlNamespaceManager nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
nsmgr.AddNamespace(Constraints.SOAP_STR, Constraints.SOAP_NAMESPACE_URI);
//get the node we have to decrypt (default = soap:Envelope)
XmlNode targetNode = xmlDoc.SelectSingleNode(Constraints.SOAP_ENVELOPE_STR, nsmgr);
switch(this.target)
{
case Target.Envelope:
{
//don't do anything cause we already are in the soap:Envelope
}; break;
case Target.Body:
{
//go down to soap:Body child
targetNode = targetNode.FirstChild;
}; break;
case Target.Method:
{
//go down to method child
targetNode = targetNode.FirstChild.FirstChild;
}; break;
default:
break;
}
//now decrypt the inner xml of the target section
byte[] decryptedData = Decrypt(targetNode.InnerXml);
//get a string from decrypted data
string decryptedString = Encoding.UTF8.GetString(decryptedData);
//assign the decrypted string to the target section
targetNode.InnerXml = decryptedString;
//now save the decrypted xml doc to the returning stream
MemoryStream memStream = new MemoryStream();
xmlDoc.Save(memStream);
memStream.Position = 0;
return memStream;
}
private byte[] Decrypt(string stringToDecrypt)
{
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
byte[] inputByteArray = CovertStringToByteArray(stringToDecrypt);
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(key, IV),
CryptoStreamMode.Write);
cs.Write(inputByteArray, 0, inputByteArray.Length);
cs.FlushFinalBlock();
return ms.ToArray();
}
#endregion
#region Encryption
/**
* Main encryption method
*/
private void Encrypt()
{
responseStream.Position = 0;
if ((encryptMode == EncryptMode.Request)
|| (encryptMode == EncryptMode.Response))
{
//encrypt the soap stream
responseStream = EncryptSoap(responseStream);
}
CopyStream(responseStream, requestStream);
}
public MemoryStream EncryptSoap(Stream streamToEncrypt)
{
streamToEncrypt.Position = 0;
//read the soap stream into a xml doc
XmlTextReader reader = new XmlTextReader(streamToEncrypt);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(reader);
//add a namespace manager to the xml doc
XmlNamespaceManager nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
nsmgr.AddNamespace(Constraints.SOAP_STR, Constraints.SOAP_NAMESPACE_URI);
//get the node we have to encrypt (default = soap:Envelope)
XmlNode targetNode = xmlDoc.SelectSingleNode(Constraints.SOAP_ENVELOPE_STR, nsmgr);
switch (this.target)
{
case Target.Envelope:
{
//don't do anything cause we already are in the soap:Envelope
}; break;
case Target.Body:
{
//go down to soap:Body child
targetNode = targetNode.FirstChild;
}; break;
case Target.Method:
{
//go down to method child
targetNode = targetNode.FirstChild.FirstChild;
}; break;
default:
break;
}
//now encrypt the inner xml of the target section
byte[] encryptedData = Encrypt(targetNode.InnerXml);
//get a string from encrypted data
string encryptedString = CovertByteArrayToString(encryptedData);
//now save the encrypted xml doc to the returning stream
targetNode.InnerXml = encryptedString.ToString();
//now save the encrypted xml doc to the returning stream
MemoryStream ms = new MemoryStream();
xmlDoc.Save(ms);
ms.Position = 0;
return ms;
}
private byte[] Encrypt(string stringToEncrypt)
{
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
byte[] inputByteArray = Encoding.UTF8.GetBytes(stringToEncrypt);
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(key, IV), CryptoStreamMode.Write);
cs.Write(inputByteArray, 0, inputByteArray.Length);
cs.FlushFinalBlock();
return ms.ToArray();
}
#endregion
#region Utils
/// <summary>
/// Copy a string to a byte array
/// </summary>
/// <param name="s">The input string</param>
/// <returns>A byte array from the given string</returns>
private static byte[] CovertStringToByteArray(string s)
{
char[] c = { ' ' };
string[] ss = s.Split(c);
byte[] b = new byte[ss.Length];
for (int i = 0; i < b.Length; i++)
{
b[i] = Byte.Parse(ss[i]);
}
return b;
}
/// <summary>
/// Copy a string to a byte array
/// </summary>
/// <param name="s">The input byte array</param>
/// <returns>A string from the given byte array</returns>
private static string CovertByteArrayToString(byte[] bytes)
{
//get a string from encrypted data
StringBuilder strBuilder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
if (i == (bytes.Length - 1))
{
strBuilder.Append(bytes[i]);
}
else
{
strBuilder.Append(string.Format("{0} ", bytes[i]));
}
}
return strBuilder.ToString();
}
/// <summary>
/// Copy a stream into another stream
/// </summary>
/// <param name="from">Source stream</param>
/// <param name="to">Destination stream</param>
private static void CopyStream(Stream from, Stream to)
{
TextReader reader = new StreamReader(from);
TextWriter writer = new StreamWriter(to);
writer.WriteLine(reader.ReadToEnd());
writer.Flush();
}
#endregion
}
}
- Custom soap extensions commonly crashed if you have WSE enable in your web service or client project. Take care with that.
- While implementing a custom soap extension make sure you override correctly the ExtensionType property in your CustomSoapExtensionAttribute class (it should return typeof(CustomSoapExtension) and not its own type).
- Remember that every time you update your web references the auto-generated code of your proxy class is refreshed and soap extensions previously applied to proxy methods are lost. However there's a way to Apply a SOAP extension to a client proxy without altering the generated proxy code.
- Finally take a time to read about soap extensions trying to understand the way it works when overriding the common methods used in every implementation (ProcessMessage, GetInitializer, Initialize and specially ChainStream).