Pages

Tuesday 12 October 2010

Standardising String Cryptography

I was writing the new Twitter Client for TweetPivot when I hit a problem. I needed to store a small amount of information on our users' machines; specifically in isolated storage. Most of this would be fine as plain text but one field needed to be encrypted. This meant I had to solve a problem I seem to come back to again and again - simple string cryptography.

Crypto Rule #1 seems to be "Don't roll your own.". OK fine, so give me something pre-rolled. C# / .Net doesn't contain the method I was looking for:

string Encrypt(string plainText, string password, string salt);

Luckily, @blowdart came to my rescue and posted this code. Now, there's a few things worth pointing out about this:
  1. Barry's book sales suggest he might actually know what he's doing, but who else amongst us has the confidence and knowledge to say that it's safe and reliable?
  2. Did anyone read the code and think "actually, TripleDES would suit my needs, not Rijndael"?
Answer: probably no.

So, given that a) it's hard to do correctly and b) this standard method would probably suit 99% of developers 99% of the time why, then, do we not find this in the BCL? If I want to open a file I can use System.IO.File.OpenRead(string path). If I need to take more control over the process I can use a StreamReader and dictate encodings etc.

I've included my final code below (you deserve something for reading this far!). Porting it into Silverlight simplifies matters - mainly because there's only one implementation of SymmetricAlgorithm. "But Barry defaulted to Rijndael. Does this mean that Silverlight is less secure than standard .Net applications?" Another crypto conundrum!

I do feel that this functionality needs abstracting behind a simplified facade. I genuinely don't care how long my initialisation vector is or whether I'm using DES, DoubleDES or even TripleDES. Cryptography is simply a tool for hiding stuff and it should be easier, safer and more confidence-imspiring than it currently is. @blowdart's subsequent tweet kinda sums up the complexity of the subject. Signatures, really? You might just as well have asked me if I wanted tweeters with it! #referenceToOldJoke
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public static class CryptographicExtensions
{
  public static string Encrypt(this string plainText, string password, string salt)
  {
    var algorithm = new AesManaged();

    algorithm.SetKeyAndIV(password, salt);

    return Encrypt(algorithm, plainText);
  }

  public static string Encrypt(this string plainText, ref byte[] key, ref byte[] iv)
  {
    if (string.IsNullOrEmpty(plainText))
      return plainText;

    var algorithm = new AesManaged();

    if (key != null)
      algorithm.Key = key;
    else
      key = algorithm.Key;

    if (iv != null)
      algorithm.IV = iv;
    else
      iv = algorithm.IV;

    return Encrypt(algorithm, plainText);
  }

  private static string Encrypt(SymmetricAlgorithm algorithm, string plainText)
  {
    var plainTextAsBytes = Encoding.UTF8.GetBytes(plainText);

    using (ICryptoTransform transform = algorithm.CreateEncryptor())
    {
      using (var outputStream = new MemoryStream())
      {
        using (var inputStream = new CryptoStream(outputStream, transform, CryptoStreamMode.Write))
        {
          inputStream.Write(plainTextAsBytes, 0, plainText.Length);
          inputStream.FlushFinalBlock();
          var cipherText = outputStream.ToArray();
          return Convert.ToBase64String(cipherText);
        }
      }
    }
  }

  public static string Decrypt(this string cypherText, string password, string salt)
  {
    var algorithm = new AesManaged();

    algorithm.SetKeyAndIV(password, salt);

    return Decrypt(algorithm, cypherText);
  }

  public static string Decrypt(this string cypherText, byte[] key, byte[] iv)
  {
    if (string.IsNullOrEmpty(cypherText))
      return cypherText;

    if (key == null)
      throw new ArgumentNullException("key");

    if (iv == null)
      throw new ArgumentNullException("iv");

    var algorithm = new AesManaged() { Key = key, IV = iv };

    return Decrypt(algorithm, cypherText);
  }

  private static string Decrypt(SymmetricAlgorithm algorithm, string cypherText)
  {
    var cipherTextAsBytes = Convert.FromBase64String(cypherText);

    using (ICryptoTransform transform = algorithm.CreateDecryptor())
    {
      using (var outputStream = new MemoryStream())
      {
        using (var inputStream = new CryptoStream(outputStream, transform, CryptoStreamMode.Write))
        {
          inputStream.Write(cipherTextAsBytes, 0, cipherTextAsBytes.Length);
          inputStream.FlushFinalBlock();
          var plainTextAsBytes = outputStream.ToArray();

          return Encoding.UTF8.GetString(plainTextAsBytes, 0, plainTextAsBytes.Length);
        }
      }
    }
  }

  public static void SetKeyAndIV(this SymmetricAlgorithm algorithm, string password, string salt)
  {
    string paddedSalt = salt;

    while (paddedSalt.Length < 8)
      paddedSalt += salt;

    var rfc2898 = new Rfc2898DeriveBytes(password, Encoding.UTF8.GetBytes(paddedSalt));

    algorithm.Key = rfc2898.GetBytes(algorithm.KeySize / 8);
    algorithm.IV = rfc2898.GetBytes(algorithm.BlockSize / 8);
  }
}

3 comments:

  1. Ah you didn't say Silverlight :)

    But yes, I could have been simply and defaulted to AES/Rijndael (they're the same algorithm) and used the maximum keysize/smallest blocksize to simplify :)

    Now note that this just encrypts - if you take away the lessons learnt from the POET stuff you should release that's not enough, you also need to sign/checksum/HMAC ... because otherwise you won't detect if someone has changed the encrypted data ... but I'll leave that as an exercise for the reader *grin* (HMACSHA256 is what you want)

    ReplyDelete
  2. Oh, one more thing, always create a new instance of the algorithm - otherwise, with your current code, the initial key and IV will be used again and again and again for subsequent encryptions, and that's bad (and leads to interesting thread safety problems too).

    ReplyDelete
  3. Thanks Barry. I've now updated the routines so that they're all thread safe and always new-up an algorithm for each pass.

    ReplyDelete