Click here to Skip to main content
15,879,490 members
Articles / Programming Languages / C#

String Encryption using DPAPI and Extension Methods

Rate me:
Please Sign up or sign in to vote.
4.00/5 (7 votes)
15 May 2009CPOL2 min read 48.2K   15   8
Extension methods that encrypt / decrypt strings through the Windows Data Protection API (DPAPI) with optional usage of secure strings that protect data in memory

The Windows Data Protection API (DPAPI) is a great technology to securely encrypt user or machine specific data without having to worry about an encryption key. Since .NET 2.0, DPAPI is part of the .NET Framework, so encrypting data is as easy as this:

C#
public static byte[] Encrypt(byte[] data)
{
  var scope = DataProtectionScope.CurrentUser;
  return ProtectedData.Protect(data, null, scope);
} 

As you can see, the Protect method of the ProtectedData class takes binary input and returns a byte array that contains the encrypted data. This means that you’ll have to do some conversions when dealing with strings, and the result of the encryption is a byte array anyway.

I recently published NetDrives, a tool that relies on the DPAPI to encrypt user passwords that are stored on disk in XML format. Accordingly, I didn’t want to deal with binary data at all: Both input and output were supposed to be strings, which is why I came up with a few extension methods that nicely wrap string encryptions for me:

image

Basic String Encryption

In case in-memory protection is not an issue and you just need to encrypt/decrypt strings (e.g. to store encrypted data in a configuration file), you just need two extension methods. First, in order to encrypt a string, just invoke the Encrypt extension method:

C#
string password = "hello world";
string encrypted = password.Encrypt(); 

Encrypt returns you the encrypted data, represented as base64 encoded string. In order to get your password back, just invoke the Decrypt extension method:

C#
string plainText = encrypted.Decrypt(); 

Managed Strings vs. SecureString

The above methods are convenient to encrypt sensitive data that is supposed to be serialized or transmitted in any way. They do, however, not protect data at runtime as the decrypted strings remain in memory. In case this is an issue, you should revert to the SecureString rather than using plain strings (but keep in mind that this may lure you into a false sense of security!).

Accordingly, I also created extension methods that use SecureString instances rather than managed strings and allow you to wrap / unwrap strings quite easily. Here’s a test that shows off the various conversions:

Attention: Always keep in mind that once you are dealing with a managed string (such as the plainText variable below), your code can be compromised! Accordingly, the ToSecureString / Unwrap methods should be treated carefully.

C#
[Test]
public void Encryption_And_Decryption_Cycle_Should_Return_Original_Value()
{
  string plainText = "this is a password";

  //encrypt plain text
  string cipher = plainText.Encrypt();
  Assert.AreNotEqual(plainText, cipher);

  //decrypt cipher into managed string
  string decrypted = cipher.Decrypt();
  Assert.AreEqual(plainText, decrypted);

  //create a SecureString from the plain text
  SecureString plainSecure = plainText.ToSecureString();

  //test unwrapping of a SecureString
  Assert.AreEqual(plainText, plainSecure.Unwrap());

  //encrypt the string that is wrapped into the SecureString
  string cipherFromSecure = plainSecure.Encrypt();

  //decrypt the cipher that was created from the the SecureString
  Assert.AreEqual(plainText, cipherFromSecure.Decrypt());
} 

Implementation

Here’s the class that provides the extension methods including a few helper methods that facilitate dealing with SecureString (e.g. SecureString.IsNullOrEmpty).

Note that you need to set an assembly reference to the System.Security assembly. Also keep in mind that the class always performs DPAPI encryption with user scope. You might want to provide some additional overloads in order to support encryption that uses the context of the machine rather than the users. The same goes for the optional entropy that is not used here all for simplicity.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
using System.Text;

namespace Hardcodet.NetDrives.Platform
{
  /// <summary>
  /// Provides extension methods that deal with
  /// string encryption/decryption and
  /// <see cref="SecureString"/> encapsulation.
  /// </summary>
  public static class SecurityExtensions
  {
    /// <summary>
    /// Specifies the data protection scope of the DPAPI.
    /// </summary>
    private const DataProtectionScope Scope = DataProtectionScope.CurrentUser;

    /// <summary>
    /// Encrypts a given password and returns the encrypted data
    /// as a base64 string.
    /// </summary>
    /// <param name="plainText">An unencrypted string that needs
    /// to be secured.</param>
    /// <returns>A base64 encoded string that represents the encrypted
    /// binary data.
    /// </returns>
    /// <remarks>This solution is not really secure as we are
    /// keeping strings in memory. If runtime protection is essential,
    /// <see cref="SecureString"/> should be used.</remarks>
    /// <exception cref="ArgumentNullException">If <paramref name="plainText"/>
    /// is a null reference.</exception>
    public static string Encrypt(this string plainText)
    {
      if (plainText == null) throw new ArgumentNullException("plainText");

      //encrypt data
      var data = Encoding.Unicode.GetBytes(plainText);
      byte[] encrypted = ProtectedData.Protect(data, null, Scope);

      //return as base64 string
      return Convert.ToBase64String(encrypted);
    }

    /// <summary>
    /// Decrypts a given string.
    /// </summary>
    /// <param name="cipher">A base64 encoded string that was created
    /// through the <see cref="Encrypt(string)"/> or
    /// <see cref="Encrypt(SecureString)"/> extension methods.</param>
    /// <returns>The decrypted string.</returns>
    /// <remarks>Keep in mind that the decrypted string remains in memory
    /// and makes your application vulnerable per se. If runtime protection
    /// is essential, <see cref="SecureString"/> should be used.</remarks>
    /// <exception cref="ArgumentNullException">If <paramref name="cipher"/>
    /// is a null reference.</exception>
    public static string Decrypt(this string cipher)
    {
      if (cipher == null) throw new ArgumentNullException("cipher");

      //parse base64 string
      byte[] data = Convert.FromBase64String(cipher);

      //decrypt data
      byte[] decrypted = ProtectedData.Unprotect(data, null, Scope);
      return Encoding.Unicode.GetString(decrypted);
    }

    /// <summary>
    /// Encrypts the contents of a secure string.
    /// </summary>
    /// <param name="value">An unencrypted string that needs
    /// to be secured.</param>
    /// <returns>A base64 encoded string that represents the encrypted
    /// binary data.
    /// </returns>
    /// <exception cref="ArgumentNullException">If <paramref name="value"/>
    /// is a null reference.</exception>
    public static string Encrypt(this SecureString value)
    {
      if (value == null) throw new ArgumentNullException("value");

      IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(value);
      try
      {
        char[] buffer = new char[value.Length];
        Marshal.Copy(ptr, buffer, 0, value.Length);

        byte[] data = Encoding.Unicode.GetBytes(buffer);
        byte[] encrypted = ProtectedData.Protect(data, null, Scope);

        //return as base64 string
        return Convert.ToBase64String(encrypted);
      }
      finally
      {
        Marshal.ZeroFreeCoTaskMemUnicode(ptr);
      }
    }

    /// <summary>
    /// Decrypts a base64 encrypted string and returns the decrpyted data
    /// wrapped into a <see cref="SecureString"/> instance.
    /// </summary>
    /// <param name="cipher">A base64 encoded string that was created
    /// through the <see cref="Encrypt(string)"/> or
    /// <see cref="Encrypt(SecureString)"/> extension methods.</param>
    /// <returns>The decrypted string, wrapped into a
    /// <see cref="SecureString"/> instance.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="cipher"/>
    /// is a null reference.</exception>
    public static SecureString DecryptSecure(this string cipher)
    {
      if (cipher == null) throw new ArgumentNullException("cipher");

      //parse base64 string
      byte[] data = Convert.FromBase64String(cipher);

      //decrypt data
      byte[] decrypted = ProtectedData.Unprotect(data, null, Scope);

      SecureString ss = new SecureString();

      //parse characters one by one - doesn't change the fact that
      //we have them in memory however...
      int count = Encoding.Unicode.GetCharCount(decrypted);
      int bc = decrypted.Length/count;
      for (int i = 0; i < count; i++)
      {
        ss.AppendChar(Encoding.Unicode.GetChars(decrypted, i*bc, bc)[0]);
      }

      //mark as read-only
      ss.MakeReadOnly();
      return ss;
    }

    /// <summary>
    /// Wraps a managed string into a <see cref="SecureString"/> 
    /// instance.
    /// </summary>
    /// <param name="value">A string or char sequence that 
    /// should be encapsulated.</param>
    /// <returns>A <see cref="SecureString"/> that encapsulates the
    /// submitted value.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="value"/>
    /// is a null reference.</exception>
    public static SecureString ToSecureString(this IEnumerable<char> value)
    {
      if (value == null) throw new ArgumentNullException("value");

      var secured = new SecureString();

      var charArray = value.ToArray();
      for (int i = 0; i < charArray.Length; i++)
      {
        secured.AppendChar(charArray[i]);
      }

      secured.MakeReadOnly();
      return secured;
    }

    /// <summary>
    /// Unwraps the contents of a secured string and
    /// returns the contained value.
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    /// <remarks>Be aware that the unwrapped managed string can be
    /// extracted from memory.</remarks>
    /// <exception cref="ArgumentNullException">If <paramref name="value"/>
    /// is a null reference.</exception>
    public static string Unwrap(this SecureString value)
    {
      if (value == null) throw new ArgumentNullException("value");

      IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(value);
      try
      {
        return Marshal.PtrToStringUni(ptr);
      }
      finally
      {
        Marshal.ZeroFreeCoTaskMemUnicode(ptr);
      }
    }

    /// <summary>
    /// Checks whether a <see cref="SecureString"/> is either
    /// null or has a <see cref="SecureString.Length"/> of 0.
    /// </summary>
    /// <param name="value">The secure string to be inspected.</param>
    /// <returns>True if the string is either null or empty.</returns>
    public static bool IsNullOrEmpty(this SecureString value)
    {
      return value == null || value.Length == 0;
    }

    /// <summary>
    /// Performs bytewise comparison of two secure strings.
    /// </summary>
    /// <param name="value"></param>
    /// <param name="other"></param>
    /// <returns>True if the strings are equal.</returns>
    public static bool Matches(this SecureString value, SecureString other)
    {
      if (value == null && other == null) return true;
      if (value == null || other == null) return false;
      if (value.Length != other.Length) return false;
      if (value.Length == 0 && other.Length == 0) return true;

      IntPtr ptrA = Marshal.SecureStringToCoTaskMemUnicode(value);
      IntPtr ptrB = Marshal.SecureStringToCoTaskMemUnicode(other);
      try
      {
        //parse characters one by one - doesn't change the fact that
        //we have them in memory however...
        byte byteA = 1;
        byte byteB = 1;

        int index = 0;
        while (((char)byteA) != '\0' && ((char)byteB) != '\0')
        {
          byteA = Marshal.ReadByte(ptrA, index);
          byteB = Marshal.ReadByte(ptrB, index);
          if (byteA != byteB) return false;
          index += 2;
        }

        return true;
      }
      finally
      {
        Marshal.ZeroFreeCoTaskMemUnicode(ptrA);
        Marshal.ZeroFreeCoTaskMemUnicode(ptrB);
      }
    }
  }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect I'm a gun for hire
Switzerland Switzerland
Philipp is an independent software engineer with great love for all things .NET.
He lives in Winterthur, Switzerland and his home on the web is at http://www.hardcodet.net.

Comments and Discussions

 
Generalgive me a idea Pin
wadeblack21-May-09 19:18
wadeblack21-May-09 19:18 
GeneralRe: give me a idea Pin
Philipp Sumi21-May-09 20:43
Philipp Sumi21-May-09 20:43 
GeneralMachine Independant Encryption. Pin
Ruchit S.15-May-09 23:13
Ruchit S.15-May-09 23:13 
Hi,

I've used your classes alrady and they seem to work great so far. Thanks for this.

Can we force your library to use a machineKey specified in web.config [mine is a web app.] ? then the DPAPI uses the machineKey from web.config. This way I can have a machine independant version of your lib. This is essential in case of application migration.


Thanks.

Ruchit S.
http://ruchitsurati.net

******************************************

GeneralRe: Machine Independant Encryption. Pin
Philipp Sumi15-May-09 23:34
Philipp Sumi15-May-09 23:34 
GeneralRe: Machine Independant Encryption. Pin
Ruchit S.16-May-09 0:06
Ruchit S.16-May-09 0:06 
GeneralRe: Machine Independant Encryption. Pin
Philipp Sumi16-May-09 0:28
Philipp Sumi16-May-09 0:28 
Generaloverflow... Pin
Daniel M. Camenzind15-May-09 4:21
Daniel M. Camenzind15-May-09 4:21 
GeneralRe: overflow... Pin
Philipp Sumi15-May-09 4:30
Philipp Sumi15-May-09 4:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.