Click here to Skip to main content
15,895,746 members
Articles / Programming Languages / C++/CLI

Keeping Secrets with Data Protection API (DPAPI) in .NET

Rate me:
Please Sign up or sign in to vote.
2.67/5 (3 votes)
5 Oct 20051 min read 32.2K   370   9  
A full blown sample of keeping data secret in memory or on a file.
/*
 * Project : Managing Secrets
 * Content : Data Protection using DPAPI on managed code needed to be done using
 *         : C++ unmanaged code or by writting some wrapper code as many of us
 *         : have done. Data Protection is available in VS2005 by using some
 *         : simple to use static methods located in the
 *         : "System::Security::Cryptography" namespace.  I have taken some
 *         : time to define a class that I called "Secret" that hides many
 *         : implementation details with the intention of making working with
 *         : those methods as simple as it can be.
 * File    : Secret.h
 * Author  : Eduardo Sobrino
 * Date    : Oct/2k5
 */

#pragma once
using namespace System ;

// -----------------------------------------------------------------------------

namespace Security
{

/// <summary>Allow the user to select the kind of protection he/she wants for
/// their memory/data secret</summary>
public enum class ProtectionScope
{
   InvalidDataProtectionScope   = -2,
   InvalidMemoryProtectionScope = -1,

   // memory protection

   ProtectMemoryCrossProcess = 1,
   ProtectMemorySameLogon    = 2,
   ProtectMemorySameProcess  = 3,

   // data protection

   ProtectDataCurrentUser  = 10,
   ProtectDataLocalMachine = 11,

   Default      = 99
}  ;  // end of ProtectionScope

/// <summary>The following is provided to allow the reporting of the status of
/// the protection reached on the "Secret" class without the nasty exception
/// throw/catch.  The user of the "Secret" class should then always check the
/// "Status" property to see how the data was protected (or not)</summary>
public enum class ProtectionStatus
{
   Protected              =  1,
   Unknown                =  0,
   Unprotected            = -1,

   // soft exceptions
   BufferLengthException  = -2,
   BufferNullException    = -3,
   EntropyLengthException = -4,
   EntropyNullException   = -5,
   StreamNullException    = -6,
   StreamReadNotPermited  = -7,

   EmptySecret            = -97,
   InvalidScope           = -98,
   ExceptionFound         = -99
}  ;  // end of ProtectionStatus

/// <summary>Provide a rich detailed result info rather than merly true, false
/// or a filled buffer (or not). The following class is returned on any
/// invocation of data protection API</summary>
public ref class ProtectionResult
{
   public:
   ProtectionStatus Status ;
   Int32 ResultLength ;
   array<Byte> ^Buffer ;
   System::Exception ^Exception ;

   ProtectionResult()
   {
      Status = ProtectionStatus::Unknown ;
      ResultLength = 0 ;
      Buffer = nullptr ;
      Exception = nullptr ;
   }
}  ;  // end of ProtectionResult

/// <summary>Provide an interface for managing secrets to allow various
/// alternate implementations</summary>
/// <date>Oct/2k5  (ESob)</date>
interface class ISecret
{
   property ProtectionScope Scope;
   property ProtectionStatus Status { ProtectionStatus get() ; };

   bool ProtectMemory(String ^Message);
   bool UnprotectMemory();

   bool ProtectDataToFile(
      String ^Message, String ^Entropy, System::IO::Stream ^Stream);
   bool UnprotectDataFromFile(String ^Entropy, System::IO::Stream ^Stream);

   bool ProtectDataToFile(String ^Message, String ^Entropy, String ^FilePath);
   bool UnprotectDataFromFile(String ^Entropy, String ^FilePath);
}  ;  // end of ISecret interface

/// <summary>Protect data using DPAPI. Wrote this class using the MSDN
/// documentation for the "ProtectedMemory" class.</summary>
public ref class ProtectedData
{
   public:

   #pragma region -- General Support Methods
   /// <summary>Create a random entropy</summary>
   /// <date>Oct/2k5  (ESob)</date>
   static array<Byte> ^CreateRandomEntropy()
   {
      // Create a byte array to hold the random value.
      array<Byte> ^entropy = gcnew array<Byte>(16);

      // Create a new instance of the RNGCryptoServiceProvider.
      // Fill the array with a random value.
      System::Security::Cryptography::RNGCryptoServiceProvider ^rng =
         gcnew System::Security::Cryptography::RNGCryptoServiceProvider();
      rng->GetBytes(entropy) ;

      // Return the array.
      return entropy;
   }  // end of CreateRandomEntropy
   #pragma endregion
   #pragma region -- Data Protection for In Memory Data
   /// <summary>Encrypt given buffer data in memory using DPAPI</summary>
   /// <param name="Buffer">Buffer to encrypt</param>
   /// <param name="Scope">Protection Scope (CrossProcess, SameLogon,
   ///    or SameProcess)</param>
   /// <returns>returns a summary of the protection result</returns>
   /// <date>Oct/2k5  (ESob)</date>
   static ProtectionResult ^EncryptInMemoryData(array<Byte> ^Buffer,
      System::Security::Cryptography::MemoryProtectionScope Scope)
   {
      ProtectionResult ^result = gcnew ProtectionResult() ;

      if (Buffer == nullptr)
      {
         result->Status = ProtectionStatus::BufferNullException ;
         return(result);
      }
      if (Buffer->Length <= 0)
      {
         result->Status = ProtectionStatus::BufferLengthException ;
         return(result);
      }

      // Encrypt the data in memory. The result is stored in the same same
      // array as the original data.

      System::Security::Cryptography::ProtectedMemory::Protect(Buffer, Scope);

      // summarize result
      result->Status = ProtectionStatus::Protected ;
      result->Buffer = Buffer ;
      result->ResultLength = Buffer->Length ;

      return(result);
   }  // end of EncryptInMemoryData

   /// <summary>Decrypt given buffer data from memory using DPAPI</summary>
   /// <param name="Buffer">Buffer to decrypt</param>
   /// <param name="Scope">Protection Scope (CrossProcess, SameLogon,
   ///    or SameProcess)</param>
   /// <returns>returns a summary of the protection result</returns>
   /// <date>Oct/2k5  (ESob)</date>
   static ProtectionResult ^DecryptInMemoryData(array<Byte> ^Buffer,
      System::Security::Cryptography::MemoryProtectionScope Scope)
   {
      ProtectionResult ^result = gcnew ProtectionResult() ;

      if (Buffer == nullptr)
      {
         result->Status = ProtectionStatus::BufferNullException ;
         return(result);
      }
      if (Buffer->Length <= 0)
      {
         result->Status = ProtectionStatus::BufferLengthException ;
         return(result);
      }

      // Decrypt the data in memory.
      // The result is stored in the same same array as the original data.
      System::Security::Cryptography::
         ProtectedMemory::Unprotect(Buffer, Scope);

      // summarize result
      result->Status = ProtectionStatus::Unprotected ;
      result->Buffer = Buffer ;
      result->ResultLength = Buffer->Length ;

      return(result);
   }  // end of DecryptInMemoryData
   #pragma endregion
   #pragma region -- Data Protection for Streams
   /// <summary>Encrypt given buffer data to a Stream using DPAPI</summary>
   /// <param name="Buffer">Buffer of data to encrypt</param>
   /// <param name="Entropy">Some user data to salt result</param>
   /// <param name="Scope">Protection Scope (CrossProcess, SameLogon,
   ///    or SameProcess)</param>
   /// <param name="Stream">Data stream to persist result too</param>
   /// <date>Oct/2k5  (ESob)</date>
   static ProtectionResult ^EncryptDataToStream(
      array<Byte> ^Buffer, array<Byte> ^Entropy,
      System::Security::Cryptography::DataProtectionScope Scope,
      System::IO::Stream ^Stream)
   {
      ProtectionResult ^result = gcnew ProtectionResult() ;

      if (Buffer == nullptr)
      {
         result->Status = ProtectionStatus::BufferNullException ;
         return(result);
      }
      if (Buffer->Length <= 0)
      {
         result->Status = ProtectionStatus::BufferLengthException ;
         return(result);
      }

      if (Entropy == nullptr)
      {
         result->Status = ProtectionStatus::EntropyNullException ;
         return(result);
      }
      if (Entropy->Length <= 0)
      {
         result->Status = ProtectionStatus::EntropyLengthException ;
         return(result);
      }

      if (Stream == nullptr)
      {
         result->Status = ProtectionStatus::StreamNullException ;
         return(result);
      }

      // Encrypt the data in memory.
      // The result is stored in the same same array as the original data.
      array<Byte> ^encrptedData =
         System::Security::Cryptography::ProtectedData::Protect(
            Buffer, Entropy, Scope);

      // Write the encrypted data to a stream.
      if (Stream->CanWrite && (encrptedData != nullptr))
      {
         Stream->Write(encrptedData, 0, encrptedData->Length);
         result->ResultLength = encrptedData->Length;
         result->Status = ProtectionStatus::Protected ;
      }
      else
         result->Status = ProtectionStatus::Unprotected ;

      return result ;
   }  // end of EncryptDataToStream

   /// <summary>Decrypt given buffer data from memory using DPAPI</summary>
   /// <param name="Buffer">Buffer to store decrypt result</param>
   /// <param name="Entropy">Some user data</param>
   /// <param name="Scope">Protection Scope (CrossProcess, SameLogon,
   ///    or SameProcess)</param>
   /// <param name="Stream">Data stream to decrypt</param>
   /// <date>Oct/2k5  (ESob)</date>
   static ProtectionResult ^DecryptDataFromStream(
      array<Byte> ^Entropy,
      System::Security::Cryptography::DataProtectionScope Scope,
      System::IO::Stream ^Stream)
   {
      ProtectionResult ^result = gcnew ProtectionResult() ;

      if (Entropy == nullptr)
      {
         result->Status = ProtectionStatus::EntropyNullException ;
         return(result);
      }
      if (Entropy->Length <= 0)
      {
         result->Status = ProtectionStatus::EntropyLengthException ;
         return(result);
      }

      if (Stream == nullptr)
      {
         result->Status = ProtectionStatus::StreamNullException ;
         return(result);
      }

      Int32 l = 512 ;
      array<Byte> ^inBuffer = gcnew array<Byte>(l);

      // Read the encrypted data from a stream.
      if (Stream->CanRead)
      {
         Stream->Read(inBuffer, 0, l);
         result->Buffer = System::Security::Cryptography::
            ProtectedData::Unprotect(inBuffer, Entropy, Scope);
         result->ResultLength = result->Buffer->Length ;
         result->Status = ProtectionStatus::Unprotected ;
      }
      else
      {
         result->Status = ProtectionStatus::StreamReadNotPermited ;
      }

      return result ;
   }  // end of DecryptDataFromStream
   #pragma endregion

}  ;  // end of ProtectedData

/// <summary>Provide a simple to use class that does proper handling of secrets
/// </summary>
/// <date>Oct/2k5  (ESob)</date>
public ref class Secret : ISecret
{

   #pragma region -- declarations and properties
   private:

   ProtectionScope scope ;
   ProtectionResult ^lastResult ;

   public:

   property ProtectionScope Scope
   {
      virtual ProtectionScope get()
      {
         return(scope);
      }
      virtual void set(ProtectionScope Value)
      {
         scope = Value ;
      }
   }  // end of Scope property

   property ProtectionStatus Status
   {
      virtual ProtectionStatus get()
      {
         if (lastResult == nullptr)
            return(ProtectionStatus::Unknown) ;
         return(lastResult->Status);
      }
   }  // end of Status property
   #pragma endregion
   #pragma region -- initialization and cleanup (ctor/dtor)
   Secret(String ^Message,ProtectionScope Scope)
   {
      scope = Scope ;
      if (Message != nullptr)
         ProtectMemory(Message) ;
   }  // end of ctor

   Secret(ProtectionScope Scope)
   {
      scope = Scope ;
   }  // end of ctor

   Secret()
   {
      scope = ProtectionScope::Default ;
   }  // end of ctor
   #pragma endregion
   #pragma region -- scope management support functions

   private:

   /// <summar>The following method returns the memory protection scope. If
   /// current setted scope is not a valid "MemoryProtectionScop" the
   /// current scope is set as invalid (="InvalidMemoryProtectionScope")
   /// </summary>
   System::Security::Cryptography::MemoryProtectionScope
      GetMemoryProtectionScope()
   {
      System::Security::Cryptography::MemoryProtectionScope s ;
      switch(scope)
      {
         case ProtectionScope::ProtectMemoryCrossProcess:
            s = System::Security::Cryptography::
               MemoryProtectionScope::CrossProcess ;
            break ;
         case ProtectionScope::ProtectMemorySameLogon:
            s = System::Security::Cryptography::
               MemoryProtectionScope::SameLogon ;
            break ;
         case ProtectionScope::ProtectMemorySameProcess:
            s = System::Security::Cryptography::
               MemoryProtectionScope::SameProcess ;
            break ;
         case ProtectionScope::Default:
            s = System::Security::Cryptography::
               MemoryProtectionScope::SameProcess ;
            break ;
         default:
            scope = ProtectionScope::InvalidMemoryProtectionScope ;
            s = System::Security::Cryptography::
               MemoryProtectionScope::SameProcess ;
            break ;
      }
      return(s) ;
   }  // end of GetMemoryProtectionScope

   /// <summar>The following method returns the data protection scope. If
   /// current setted scope is not a valid "DataProtectionScop" the
   /// current scope is set as invalid (="InvalidDataProtectionScope")
   /// </summary>
   System::Security::Cryptography::DataProtectionScope GetDataProtectionScope()
   {
      System::Security::Cryptography::DataProtectionScope s ;
      switch(scope)
      {
         case ProtectionScope::ProtectDataCurrentUser:
            s = System::Security::Cryptography::
               DataProtectionScope::CurrentUser ;
            break ;
         case ProtectionScope::ProtectDataLocalMachine:
            s = System::Security::Cryptography::
               DataProtectionScope::LocalMachine ;
            break ;
         case ProtectionScope::Default:
            s = System::Security::Cryptography::
               DataProtectionScope::CurrentUser ;
            break ;
         default:
            scope = ProtectionScope::InvalidDataProtectionScope ;
            s = System::Security::Cryptography::
               DataProtectionScope::CurrentUser ;
            break ;
      }
      return(s) ;
   }  // end of GetDataProtectionScope()

   #pragma endregion
   #pragma region -- memory protection methods

   public:

   /// <summary>Given a string store it on protected memory</summary>
   /// <result>true is returned if mesage is protected else user should check
   /// the "Secret.Status" to see what went wrong.</result>
   /// <date>Oct/2k5  (ESob)</date>
   virtual bool ProtectMemory(String ^Message)
   {
      System::Security::Cryptography::MemoryProtectionScope s ;
      s = GetMemoryProtectionScope() ;

      if (scope == ProtectionScope::InvalidMemoryProtectionScope)
      {
         lastResult = gcnew ProtectionResult() ;
         lastResult->Status = ProtectionStatus::InvalidScope ;
         return(false) ;
      }

      // ensure that length of bytes array is a multiple of 16, else an exception
      // will be thrown by EncryptInMemoryData. "l" needs to be 0, it is the
      // missing number of bytes to be a multiple of 16
      Int16 l = Message->Length % 16 ;

      System::Text::ASCIIEncoding ^ae = gcnew System::Text::ASCIIEncoding();

      array<Byte> ^buffer ;
      buffer = ae->GetBytes(Message) ;

      // resize buffer to ensure that is a multiple of 16 (if needed)
      if (l > 0)
         buffer->Resize(buffer,Message->Length + (16 - l)) ;

      lastResult = ProtectedData::EncryptInMemoryData(buffer,s) ;
      buffer = nullptr ;

      return(lastResult->Status == ProtectionStatus::Protected) ;
   }  // end of Protect

   /// <summary>Restore original data into internal buffer</summary>
   /// <return>true is returned if mesage is unprotected else user should check
   /// the "Secret.Status" to see what went wrong.</return>
   /// <date>Oct/2k5  (ESob)</date>
   virtual bool UnprotectMemory()
   {
      if (lastResult == nullptr)
      {
         lastResult = gcnew ProtectionResult() ;
         lastResult->Status = ProtectionStatus::EmptySecret ;
         return(false) ;
      }

      if (lastResult->Status != ProtectionStatus::Protected)
         return(false) ;

      System::Security::Cryptography::MemoryProtectionScope s ;
      s = GetMemoryProtectionScope() ;

      if (scope == ProtectionScope::InvalidMemoryProtectionScope)
      {
         lastResult = gcnew ProtectionResult() ;
         lastResult->Status = ProtectionStatus::InvalidScope ;
         return(false) ;
      }

      array<Byte> ^buffer = lastResult->Buffer ;
      lastResult = ProtectedData::DecryptInMemoryData(buffer,s) ;
      return(lastResult->Status == ProtectionStatus::Unprotected) ;
   }  // end of Unprotect

   #pragma endregion
   #pragma region -- data protection methods

   /// <summary>Protect info and store result to a file</summary>
   /// <param name="Message">String message to protect</param>
   /// <param name="Entropy">Some user data to salt protection</param>
   /// <param name="Stream">Target stream where protected result will be
   /// stored into</param>
   /// <return>true is returned if mesage is protected else user should check
   /// the "Secret.Status" to see what went wrong.</return>
   /// <remarks>the supplied stream is released here, therefore don't ever
   /// never use it again</remarks>
   /// <date>Oct/2k5  (ESob)</date>
   virtual bool ProtectDataToFile(
      String ^Message, String ^Entropy, System::IO::Stream ^Stream)
   {
      System::Security::Cryptography::DataProtectionScope s ;
      s = GetDataProtectionScope() ;

      if (scope == ProtectionScope::InvalidDataProtectionScope)
      {
         lastResult = gcnew ProtectionResult() ;
         lastResult->Status = ProtectionStatus::InvalidScope ;
         return(false) ;
      }

      System::Text::ASCIIEncoding ^ae = gcnew System::Text::ASCIIEncoding();

      array<Byte> ^buffer ;
      buffer = ae->GetBytes(Message) ;

      array<Byte> ^entropy = ae->GetBytes(Entropy) ;

      try
      {
         lastResult = ProtectedData::EncryptDataToStream(
            buffer,entropy,s,Stream) ;
      }
      catch (System::Exception ^ex)
      {
         lastResult = gcnew ProtectionResult();
         lastResult->Exception = ex ;
         lastResult->Status = ProtectionStatus::ExceptionFound ;
      }
      finally
      {
         Stream->Close() ;
         delete Stream ;
      }

      return(lastResult->Status == ProtectionStatus::Protected) ;
   }  // end of ProtectToFile

   /// <summary>Protect info and store result to a file</summary>
   /// <param name="Message">String message to protect</param>
   /// <param name="Entropy">Some user data to salt protection</param>
   /// <param name="FilePath">Target file path where protected result will be
   /// stored into</param>
   /// <return>true is returned if mesage is protected else user should check
   /// the "Secret.Status" to see what went wrong.</return>
   /// <date>Oct/2k5  (ESob)</date>
   virtual bool ProtectDataToFile(
      String ^Message, String ^Entropy, String ^FilePath)
   {
      System::IO::Stream ^stream ;

      try
      {
         stream = gcnew System::IO::FileStream(
            FilePath, System::IO::FileMode::OpenOrCreate);
      }
      catch (System::Exception ^ex)
      {
         lastResult = gcnew ProtectionResult() ;
         lastResult->Exception = ex ;
         lastResult->Status = ProtectionStatus::ExceptionFound ;
         stream = nullptr ;
      }
      if (stream == nullptr)
         return(false) ;

      return(ProtectDataToFile(Message,Entropy,stream)) ;
   }  // end of ProtectToFile

   /// <summary>Unprotect info by reading data from a file</summary>
   /// <param name="Entropy">Some user data to salt protection</param>
   /// <param name="Stream">Stream from where to read protected
   /// data</param>
   /// <return>true is returned if mesage is unprotected else user should check
   /// the "Secret.Status" to see what went wrong.</return>
   /// <remarks>the supplied stream is released here, therefore don't ever
   /// never use it again</remarks>
   /// <date>Oct/2k5  (ESob)</date>
   virtual bool UnprotectDataFromFile(
      String ^Entropy, System::IO::Stream ^Stream)
   {
      System::Security::Cryptography::DataProtectionScope s ;
      s = GetDataProtectionScope() ;

      if (scope == ProtectionScope::InvalidDataProtectionScope)
      {
         lastResult = gcnew ProtectionResult() ;
         lastResult->Status = ProtectionStatus::InvalidScope ;
         return(false) ;
      }

      System::Text::ASCIIEncoding ^ae = gcnew System::Text::ASCIIEncoding();
      array<Byte> ^entropy = ae->GetBytes(Entropy) ;

      try
      {
         lastResult = ProtectedData::DecryptDataFromStream(entropy,s,Stream);
      }
      catch (System::Exception ^ex)
      {
         lastResult = gcnew ProtectionResult();
         lastResult->Exception = ex ;
         lastResult->Status = ProtectionStatus::ExceptionFound ;
      }
      finally
      {
         Stream->Close() ;
         delete Stream ;
      }

      return(lastResult->Status == ProtectionStatus::Unprotected) ;
   }  // end of UnprotectFromFile

   /// <summary>Unprotect info by reading data from a file</summary>
   /// <param name="Entropy">Some user data to salt protection</param>
   /// <param name="FilePath">Source file path from where to read protected
   /// data</param>
   /// <return>true is returned if mesage is unprotected else user should check
   /// the "Secret.Status" to see what went wrong.</return>
   /// <date>Oct/2k5  (ESob)</date>
   virtual bool UnprotectDataFromFile(String ^Entropy, String ^FilePath)
   {
      System::IO::Stream ^stream ;

      try
      {
         stream = gcnew System::IO::FileStream(
            FilePath, System::IO::FileMode::Open);
      }
      catch (System::Exception ^ex)
      {
         lastResult = gcnew ProtectionResult() ;
         lastResult->Exception = ex ;
         lastResult->Status = ProtectionStatus::ExceptionFound ;
         stream = nullptr ;
      }
      if (stream == nullptr)
         return(false) ;

      return(UnprotectDataFromFile(Entropy,stream)) ;
   }  // end of UnprotectFromFile

   #pragma endregion
   #pragma region -- other support methods

   /// <summary>Return the string representation of the internal message
   /// </summary>
   /// <return>A representation of the internal message buffer is returned
   /// </return>
   /// <date>Oct/2k5  (ESob)</date>
   virtual String ^ToString() override
   {
      if (lastResult == nullptr)
         return(L"(null)") ;

      String ^secret = nullptr ;
      if (Status == ProtectionStatus::Protected)
      {
         System::Text::ASCIIEncoding ^ae = gcnew System::Text::ASCIIEncoding();
         secret = ae->GetString(lastResult->Buffer,0,lastResult->Buffer->Length);
      }
      else
      if (Status == ProtectionStatus::Unprotected)
      {
         System::Text::ASCIIEncoding ^ae = gcnew System::Text::ASCIIEncoding();
         secret = ae->GetString(lastResult->Buffer,0,lastResult->Buffer->Length);
      }
      else
         return(L"?") ;

      return(secret);
   }  // end of ToString

   #pragma endregion

}  ;  // end of Secret

}  // end of namespace Security

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Puerto Rico Puerto Rico
C/C++ programmer since 1984.

Comments and Discussions