High Coding

Sunday, May 25, 2008

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
}
}



Recommendations:
  • 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).
I suggest you to check the following links before implementing your own SOAP extensions:

Tuesday, May 20, 2008

Welcome

Hi everybody! I hope you find my future posts interesting and have this blog as a handy code solution provider in your minds. I'll try to bring good programming recommendations and samples, and of course, I'll be happy to receive your suggestions anytime.