Web Services

Encrypting and securing web service traffic without SSL

Sometimes the need arises to have secured web service traffic without depending on SSL. The web service in a windows service without IIS from the previous post cannot use SSL anyway (well not that I know – any comments on this). There also may be a need to authenticate to the web service, either through the usual user and pass system, or my preferred method of using AES and pre-shared keys. Security experts agree the world over that AES is a very tough security standard. This method is great for two reasons:

  • No need to type in a user and pass, just ensure the keyfile is on each machine – if they keys match then the data is readable
  • Very high security

It does have a couple of downsides however:

  • Each client must have the same key – unless we use soap headers to first identify the user, the server will use this info to load the appropriate key
  • Keys cannot distinguish users without the soap header above or something similar, so user accounting is a smidge more tricky than straight user and pass

Of course on IIS you can use both the pre-shared key and user/pass authentication for extra security – NTLM kerberos and pre-shared AES key… pretty damn secure in my book.

A basic outline of the steps involved in this little project is:

  • Create webservice
  • Create encryption class
  • Create SOAP header class to provide a secondary layer of security
  • Maybe some user/pass authentication if I get time (for IIS only)

I’m going to use the web service created in the previous post, but this should work with any web service, WSE or not.

First we will get the basic encryption stuff out of the way, so create a new public class called Crypto… mine is in a class library. This class will create a key if one doesn’t exist, and will provide the encryption and decryption duties. This article isn’t focused on encryption, so I’m going to just dump the encryption class code in one blob.


using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using System.Xml;

namespace Web_Service_inside_a_Windows_Service
{
    public class Crypto
    {
        byte[] key;
        byte[] iv;

        public Crypto()
        {
            keyInit();
        }

        //pretty much a straight copy of MSDN
        public byte[] Encrypt(string input)
        {
            UTF8Encoding textConverter = new UTF8Encoding();
            RijndaelManaged rijndael = new RijndaelManaged();
            byte[] toEncrypt;         

            ICryptoTransform encryptor = rijndael.CreateEncryptor(key, iv);

            MemoryStream msEncrypt = new MemoryStream();
            CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write);

            toEncrypt = textConverter.GetBytes(input);

            csEncrypt.Write(toEncrypt, 0, toEncrypt.Length);
            csEncrypt.FlushFinalBlock();

            return msEncrypt.ToArray();
        }

        public string Decrypt(byte[] input)
        {
            UTF8Encoding textConverter = new UTF8Encoding();
            RijndaelManaged rijndael = new RijndaelManaged();

            byte[] fromEncrypt;
            int discarded = 0;

            ICryptoTransform decryptor = rijndael.CreateDecryptor(key, iv);

            MemoryStream msDecrypt = new MemoryStream(input);
            CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);

            fromEncrypt = new byte[input.Length];

            csDecrypt.Read(fromEncrypt, 0, fromEncrypt.Length);

            return textConverter.GetString(fromEncrypt);
        }

        //check for keys and if not present the create them
        //load keys into local vars
        void keyInit()
        {
            //place the keys in a nicely accessible location so other applications that use this class can use them - note this location is probably
            //not secure on most machines
            FileInfo keyFile = new FileInfo(System.Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\CryptoKeys");

            string sKey;
            string sIv;

            if (!keyFile.Exists)
            {
                sKey = generateHexKey();//set the keys into the local vars as they are created
                sIv = generateHexIV();
                XmlDocument doc = new XmlDocument();
                doc.LoadXml("");

                XmlNode keyNode = doc.CreateElement("key");
                keyNode.InnerText = sKey;
                doc.DocumentElement.AppendChild(keyNode);

                XmlNode ivNode = doc.CreateElement("iv");
                ivNode.InnerText = sIv;
                doc.DocumentElement.AppendChild(ivNode);

                if (!keyFile.Directory.Exists)
                {
                    keyFile.Directory.Create();
                }

                doc.Save(keyFile.FullName);
            }
            else
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(keyFile.FullName);
                sKey = doc.DocumentElement.SelectSingleNode("key").InnerText;
                sIv = doc.DocumentElement.SelectSingleNode("iv").InnerText;
            }

            key = Convert.FromBase64String(sKey);
            iv = Convert.FromBase64String(sIv);
        }

        string generateHexKey()
        {
            //Rijndael *is* AES
            RijndaelManaged rijndael = new RijndaelManaged();
            rijndael.GenerateKey();
            return Convert.ToBase64String(rijndael.Key);
        }

        string generateHexIV()
        {
            RijndaelManaged rijndael = new RijndaelManaged();
            rijndael.GenerateIV();
            return Convert.ToBase64String(rijndael.IV);
        }
    }
}

There. Nothing fancy, pretty much the default encryption stuff from MSDN, except i change the ASCIIEncoder to a UTF one so that way it will support any character we throw at it. Oh and the comments are poor as you can see.

Okay, so now we can encrypt stuff, lets use it and get some secure web service action happening. First we need to update the web service. In your web service code behind make the following adjustments.


using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;

[WebService(Namespace = "http://myURI/MyLittleWebService")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class WebServiceCodeEncrypted : System.Web.Services.WebService
{
    Web_Service_inside_a_Windows_Service.Crypto crypto = new Web_Service_inside_a_Windows_Service.Crypto();
    public WebServiceCodeEncrypted()
    {
    }

    [WebMethod]
    [SoapHeader("AuthKey", Direction = SoapHeaderDirection.In)]
    public string GetMessages(string sGuid)
    {
        string guid = decString(sGuid);
        return encString("At the third stroke the time will be " + DateTime.Now.ToString() + ". Your guid was " + guid);
    }

    string encString(string input)
    {
        //convert the byte array that comes out for easy transport.
        return Convert.ToBase64String(crypto.Encrypt(input));
    }

    string decString(string input)
    {
        return crypto.Decrypt(Convert.FromBase64String(input));
    }

    public class AuthorisationKey : SoapHeader
    {
        public byte[] EncData = null;

        public AuthorisationKey()
        {

        }

        public bool Validate(System.Web.HttpContext context, string WSMethodName)
        {
            try
            {
                //EncData should contain the ticks of the date time that hte request was made...
                //by checking this we are doing two things a) the key is right if we get a valid date time and
                //b) the request is only valid for 5 minutes, cause after that runDate will be less than now - 5 mins.
                string ticks = crypto.Decrypt(EncData);
                DateTime runDate = new DateTime(Convert.ToInt64(ticks));
                return runDate > DateTime.Now.AddMinutes(-5);
            }
            catch (Exception ex)
            {
                //maybe throw a nice exception here - like the key was bad or something.
                throw ex;
            }
        }
    }

}

A few notes:

  • Added decrypt and encrypt string methods. They user the base64encoding stuff in the Convert class library to turn the byte array into a string.
  • We could have just put return type of byte[] on the GetMessages web method (.NET will base64 encode it anyway), but I like strings better because they are easily stored in formats like XML.
  • We added in the soap header authorisation class. More on this a bit later.
  • Changed the web service class name to WebServiceCodeEncrypted because you need to include a reference to your main project to bring in the new crypto class library, and there would have been two classes named the same (the original copy of the webservice is in the other class remember)
  • Note that any unsafe string (i.e. un encrypted) should always be named in a way that indicates its status. For example, you could call your safe encrypted string sSomeName and your un-encrypted ones uSomeName… then hopefully you wont ever accidentally transmit around an unsecured string.

Ensure you copy this updated webservice code into an IIS based “real” web service and update the reference in your project.

Now accessing the webservice is the fun part. Update the code in your WebServiceQuery class:


WebServiceCodeReference.WebServiceCodeWse wsc = new WebServiceCodeReference.WebServiceCodeWse();
WebServiceCodeReference.AuthorisationKey key = new WebServiceCodeReference.AuthorisationKey();

Crypto crypto = new Crypto();
key.EncData = crypto.Encrypt(DateTime.Now.Ticks.ToString());

wsc.AuthorisationKeyValue = key;

IPHostEntry ip = Dns.GetHostEntry(targetMachineName);
wsc.Url = "soap.tcp://" + ip.AddressList[0].ToString() + "/MyLittleWebService";

string result = wsc.GetMessages(Convert.ToBase64String(crypto.Encrypt("This here is a test string")));

string decResult = crypto.Decrypt(Convert.FromBase64String(result));</pre>
<pre>return decResult;

First and foremost if you run this from your test application or service, the data will be encrypted! Secondly you can see we are creating an object to implement the call to the soap security header… we put the current date time in there, so the server can get *its* current date time, take off five minutes, and compare it to the one passed in… this means that each message is only valid for five minutes (so no nefarious beings can pinch your packets and spoof another request to the server – well not after five minutes anyway).

If you are running your web service on IIS then you can add in the following code to authenticate with user and pass while still using the pre-shared key system:


	if (user != null && user!=string.Empty)
            {
                if (user.ToLower() == "integrated")
                {
                    wsc.Credentials = System.Net.CredentialCache.DefaultCredentials;
                }
                else
                {
                    NetworkCredential netCred = new NetworkCredential(user, pass);
                    wsc.Credentials = netCred;
                }
            }

Pass the user and pass variables into your method. Set the user the integrated to use the NTLM based single sign on authentication of the current windows user.

Update: 31 October 2007

Another reason why using built in encryption rather than SSL etc is that SSL is transport level encryption, meaning you cannot access the encrypted version of the content. With the AES inbuilt version you could save off your messages to DB or filesystem without the need to re-encrypt them.

Update: 16 May 2008

Added missing soap attribute from GetMessages method.

2 thoughts on “Encrypting and securing web service traffic without SSL

  1. I was wondering if you have a demo of the code for Encrypting and securing web service traffic without SSL?

    Regards,
    Kevin

Comments are closed.