String Encryption using DPAPI and Extension Methods
Latest Version: 2009.04.14 (Bugfix in Matches extension method)
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:
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.
NetDrives relies on the DPAPI to encrypt user passwords that are stored on disk. Accordingly, I didn’t want to deal with binary data at all: Both input and output were supposed to be strings, which why I came up with a few extension methods that nicely wrap string encryptions for me:
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:
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:
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.
[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 user’s. The same goes for the optional entropy that is not used at all for simplicity.
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) != ' ' && ((char)byteB) != ' ') { 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); } } } }