Cómo: Crear una biblioteca DPAPI

Publicado: 26 de junio de 2006

Consulte la Página de entrada como punto de partida y para obtener una descripción completa del documento Crear aplicaciones ASP.NET seguras.

Resumen: En este artículo se describe cómo crear una biblioteca de clases administradas que expone la funcionalidad DPAPI a las aplicaciones que desean cifrar datos, por ejemplo, cadenas de conexión de base de datos y credenciales de cuentas.

Es frecuente que las aplicaciones Web necesiten almacenar datos importantes para la seguridad, como cadenas de conexión de base de datos y credenciales de cuentas de servicio en archivos de configuración de aplicaciones. Por razones de seguridad, este tipo de información no debe almacenarse nunca en texto sin cifrar y debe cifrarse siempre antes de su almacenamiento.

En este artículo se describe cómo crear una biblioteca de clases administradas que encapsula las llamadas a la API de protección de datos (DPAPI, Data Protection API) para cifrar y descifrar los datos. Después, esta biblioteca se puede utilizar en otras aplicaciones administradas, como las aplicaciones Web ASP.NET, los servicios Web y las aplicaciones de Servicios Empresariales.
Si desea ver artículos relacionados en los que se utiliza la biblioteca DPAPI creada en este documento, consulte los siguientes artículos en la sección Referencia de esta guía:

Notas
En el sistema operativo Microsoft® Windows® 2000 y los sistemas operativos posteriores se proporciona la API de protección de datos (DPAPI) de Win32® para cifrar y descifrar datos.
DPAPI forma parte de la API de cifrado (Crypto API) y se implementa en el archivo crypt32.dll. Consta de dos métodos: CryptProtectData y CryptUnprotectData. DPAPI es especialmente útil en cuanto que puede eliminar el problema de administración de claves al que están expuestas las aplicaciones que utilizan criptografía. Si bien el cifrado asegura que los datos son seguros, deben tomarse medidas adicionales para garantizar la seguridad de la clave. DPAPI utiliza la contraseña de la cuenta de usuario asociada al código que llama a las funciones DPAPI para obtener a partir de ella la clave de cifrado. Como resultado, la clave la administra el sistema operativo, en lugar de la aplicación.
DPAPI puede trabajar con el almacén del equipo o el almacén del usuario (lo que requiere que se cargue un perfil de usuario). De forma predeterminada, DPAPI utiliza el almacén del usuario, aunque se puede especificar que se utilice el almacén del equipo si se pasa el indicador CRYPTPROTECT_LOCAL_MACHINE a las funciones DPAPI.
El uso del perfil de usuario permite un nivel de seguridad adicional porque limita quién puede tener acceso al secreto. Sólo el usuario que cifra los datos puede descifrarlos. Sin embargo, el uso del perfil de usuario requiere mayor trabajo de desarrollo si se utiliza DPAPI en una aplicación Web ASP.NET, porque es necesario realizar pasos explícitos para cargar y descargar los perfiles de usuario (ASP.NET no carga automáticamente los perfiles de usuario).
El uso del almacén del equipo es más fácil de desarrollar porque no requiere la administración de perfiles de usuario. Sin embargo, a menos que se utilice un parámetro de entropía adicional, la seguridad es menor porque cualquier usuario del equipo puede descifrar los datos. (El parámetro de entropía es un valor aleatorio diseñado para hacer más difícil la posibilidad de descifrar el secreto.) El problema de utilizar un parámetro de entropía adicional es que debe almacenarse de forma segura en la aplicación, lo que presenta otro problema de administración de claves.

Nota: si se utiliza DPAPI con el almacén del equipo, la cadena cifrada es específica de un equipo determinado y, por lo tanto, deben generarse los datos cifrados en todos los equipos. No copie los datos cifrados entre los equipos de un grupo o clúster de servidores.
Si se utiliza DPAPI con el almacén del usuario, puede descifrar los datos en cualquier equipo con un perfil de usuario móvil.

Requisitos
A continuación se describen las recomendaciones de hardware, software, infraestructura de red, conocimientos y Service Pack que se necesitan:
Microsoft Windows 2000
Sistema de desarrollo Microsoft Visual Studio® .NET
Para llevar a cabo los procedimientos de este artículo, también es necesario que tenga conocimientos de la herramienta de desarrollo Microsoft Visual C#™.

En esta página

Crear una biblioteca de clases en C# Crear una biblioteca de clases en C#
Asignar un nombre seguro al ensamblado (opcional) Asignar un nombre seguro al ensamblado (opcional)
Referencias Referencias

Crear una biblioteca de clases en C#

En este procedimiento se crea una biblioteca de clases en C# que expone los métodos Encrypt y Decrypt. Encapsula las llamadas a las funciones DPAPI de Win32.

Para crear una biblioteca de clases en C#

  1. Inicie Visual Studio .NET y cree un nuevo proyecto de biblioteca de clases de Visual C# con el nombre DataProtection.

  2. Utilice el Explorador de soluciones para cambiar el nombre del archivo class1.cs por DataProtection.cs.

  3. En el archivo DataProtection.cs, cambie el nombre class1 por DataProtector, y cambie el nombre del constructor predeterminado de igual manera.

  4. En el Explorador de soluciones, haga clic con el botón secundario del mouse (ratón) en DataProtection y, a continuación, haga clic en Propiedades.

  5. Haga clic en la carpeta Propiedades de configuración y configure Permitir bloques de código no seguros en True.

  6. Haga clic en Aceptar para cerrar el cuadro de diálogo Propiedades.

  7. Al principio del archivo DataProtection.cs, agregue las siguientes instrucciones using debajo de la instrucción using existente.

    using System.Text;
       using System.Runtime.InteropServices;
    
  8. Agregue las siguientes instrucciones DllImport al principio de la clase DataProtector para que se pueda llamar a las funciones DPAPI de Win32 y a la función de utilidad FormatMessage mediante P/Invoke.

    [DllImport("Crypt32.dll", SetLastError=true, 
       CharSet=System.Runtime.InteropServices.CharSet.Auto)]
       private static extern bool CryptProtectData(
       ref DATA_BLOB pDataIn, 
       String szDataDescr, 
       ref DATA_BLOB pOptionalEntropy,
       IntPtr pvReserved, 
       ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct, 
       int dwFlags, 
       ref DATA_BLOB pDataOut);
       [DllImport("Crypt32.dll", SetLastError=true, 
       CharSet=System.Runtime.InteropServices.CharSet.Auto)]
       private static extern bool CryptUnprotectData(
       ref DATA_BLOB pDataIn, 
       String szDataDescr, 
       ref DATA_BLOB pOptionalEntropy,
       IntPtr pvReserved, 
       ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct, 
       int dwFlags, 
       ref DATA_BLOB pDataOut);
       [DllImport("kernel32.dll", 
       CharSet=System.Runtime.InteropServices.CharSet.Auto)]
       private unsafe static extern int FormatMessage(int dwFlags, 
       ref IntPtr lpSource, 
       int dwMessageId,
       int dwLanguageId, 
       ref String lpBuffer, int nSize, 
       IntPtr *Arguments);
    
  9. Agregue las siguientes definiciones de estructura y constantes utilizadas por las funciones DPAPI.

    [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
       internal struct DATA_BLOB
       {
       public int cbData;
       public IntPtr pbData;
       }
    
    [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
       internal struct CRYPTPROTECT_PROMPTSTRUCT
       {
       public int cbSize;
       public int dwPromptFlags;
       public IntPtr hwndApp;
       public String szPrompt;
       }
       static private IntPtr NullPtr = ((IntPtr)((int)(0)));
       private const int CRYPTPROTECT_UI_FORBIDDEN = 0x1;
       private const int CRYPTPROTECT_LOCAL_MACHINE = 0x4;
    
  10. Agregue a la clase un tipo enumerado público con el nombre Store. Se usa para indicar si DPAPI debe utilizarse en combinación con el almacén del equipo o el almacén del usuario.

    public enum Store {USE_MACHINE_STORE = 1, USE_USER_STORE};
    
  11. Agregue a la clase una variable miembro privada de tipo Store.

    private Store store;
    
  12. Reemplace el constructor predeterminado de la clase por el siguiente constructor que acepta un parámetro Store y coloca el valor suministrado en la variable miembro privada store.

    public DataProtector(Store tempStore)
       {
       store = tempStore;
       }
    
  13. Agregue a la clase el siguiente método Encrypt público.

    public byte[] Encrypt(byte[] plainText, byte[] optionalEntropy)
       {
       bool retVal = false;
    
    DATA_BLOB plainTextBlob = new DATA_BLOB();
       DATA_BLOB cipherTextBlob = new DATA_BLOB();
       DATA_BLOB entropyBlob = new DATA_BLOB();
    
    CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT();
       InitPromptstruct(ref prompt);
    
    int dwFlags;
       try
       {
       try
       {
       int bytesSize = plainText.Length;
       plainTextBlob.pbData = Marshal.AllocHGlobal(bytesSize);
       if(IntPtr.Zero == plainTextBlob.pbData)
       {
       throw new Exception("Unable to allocate plaintext buffer.");
       }
       plainTextBlob.cbData = bytesSize;
       Marshal.Copy(plainText, 0, plainTextBlob.pbData, bytesSize);
       }
       catch(Exception ex)
       {
       throw new Exception("Exception marshalling data. " + ex.Message);
       }
       if(Store.USE_MACHINE_STORE == store)
       {//Using the machine store, should be providing entropy.
       dwFlags = CRYPTPROTECT_LOCAL_MACHINE|CRYPTPROTECT_UI_FORBIDDEN;
       //Check to see if the entropy is null
       if(null == optionalEntropy)
       {//Allocate something
       optionalEntropy = new byte[0];
       }
       try
       {
       int bytesSize = optionalEntropy.Length;
       entropyBlob.pbData = Marshal.AllocHGlobal(optionalEntropy.Length);;
       if(IntPtr.Zero == entropyBlob.pbData)
       {
       throw new Exception("Unable to allocate entropy data buffer.");
       }
       Marshal.Copy(optionalEntropy, 0, entropyBlob.pbData, bytesSize);
       entropyBlob.cbData = bytesSize;
       }
       catch(Exception ex)
       {
       throw new Exception("Exception entropy marshalling data. " + 
       ex.Message);
       }
       }
       else
       {//Using the user store
       dwFlags = CRYPTPROTECT_UI_FORBIDDEN;
       }
       retVal = CryptProtectData(ref plainTextBlob, ", ref entropyBlob,    
       IntPtr.Zero, ref prompt, dwFlags, 
       ref cipherTextBlob);
       if(false == retVal)
       {
       throw new Exception("Encryption failed. " + 
       GetErrorMessage(Marshal.GetLastWin32Error()));
       }
       }
       catch(Exception ex)
       {
       throw new Exception("Exception encrypting. " + ex.Message);
       }
       byte[] cipherText = new byte[cipherTextBlob.cbData];
       Marshal.Copy(cipherTextBlob.pbData, cipherText, 0, cipherTextBlob.cbData);
       return cipherText;
       }
    
  14. Agregue a la clase el siguiente método Decrypt público.

    public byte[] Decrypt(byte[] cipherText, byte[] optionalEntropy)
       {
       bool retVal = false;
       DATA_BLOB plainTextBlob = new DATA_BLOB();
       DATA_BLOB cipherBlob = new DATA_BLOB();
       CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT();
       InitPromptstruct(ref prompt);
       try
       {
       try
       {
       int cipherTextSize = cipherText.Length;
       cipherBlob.pbData = Marshal.AllocHGlobal(cipherTextSize);
       if(IntPtr.Zero == cipherBlob.pbData)
       {
       throw new Exception("Unable to allocate cipherText buffer.");
       }
       cipherBlob.cbData = cipherTextSize; 
       Marshal.Copy(cipherText, 0, cipherBlob.pbData, cipherBlob.cbData);
       }
       catch(Exception ex)
       {
       throw new Exception("Exception marshalling data. " + ex.Message);
       }
       DATA_BLOB entropyBlob = new DATA_BLOB();
       int dwFlags;
       if(Store.USE_MACHINE_STORE == store)
       {//Using the machine store, should be providing entropy.
       dwFlags = CRYPTPROTECT_LOCAL_MACHINE|CRYPTPROTECT_UI_FORBIDDEN;
       //Check to see if the entropy is null
       if(null == optionalEntropy)
       {//Allocate something
       optionalEntropy = new byte[0];
       }
       try
       {
       int bytesSize = optionalEntropy.Length;
       entropyBlob.pbData = Marshal.AllocHGlobal(bytesSize);
       if(IntPtr.Zero == entropyBlob.pbData)
       {
       throw new Exception("Unable to allocate entropy buffer.");
       }
       entropyBlob.cbData = bytesSize;
       Marshal.Copy(optionalEntropy, 0, entropyBlob.pbData, bytesSize);
       }
       catch(Exception ex)
       {
       throw new Exception("Exception entropy marshalling data. " + 
       ex.Message);
       }
       }
       else
       {//Using the user store
       dwFlags = CRYPTPROTECT_UI_FORBIDDEN;
       }
       retVal = CryptUnprotectData(ref cipherBlob, null, ref entropyBlob, 
       IntPtr.Zero, ref prompt, dwFlags, 
       ref plainTextBlob);
       if(false == retVal)
       {
       throw new Exception("Decryption failed. " + 
       GetErrorMessage(Marshal.GetLastWin32Error()));
       }
       //Free the blob and entropy.
       if(IntPtr.Zero != cipherBlob.pbData)
       {
       Marshal.FreeHGlobal(cipherBlob.pbData);
       }
       if(IntPtr.Zero != entropyBlob.pbData)
       {
       Marshal.FreeHGlobal(entropyBlob.pbData);
       }
       }
       catch(Exception ex)
       {
       throw new Exception("Exception decrypting. " + ex.Message);
       }
       byte[] plainText = new byte[plainTextBlob.cbData];
       Marshal.Copy(plainTextBlob.pbData, plainText, 0, plainTextBlob.cbData);
       return plainText;
       }
    
  15. Agregue a la clase los siguientes métodos auxiliares privados.

    private void InitPromptstruct(ref CRYPTPROTECT_PROMPTSTRUCT ps) 
       {
       ps.cbSize = Marshal.SizeOf(typeof(CRYPTPROTECT_PROMPTSTRUCT));
       ps.dwPromptFlags = 0;
       ps.hwndApp = NullPtr;
       ps.szPrompt = null;
       }
    
    private unsafe static String GetErrorMessage(int errorCode)
       {
       int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
       int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
       int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
       int messageSize = 255;
       String lpMsgBuf = ";
       int dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |    
       FORMAT_MESSAGE_IGNORE_INSERTS;
       IntPtr ptrlpSource = new IntPtr();
       IntPtr prtArguments = new IntPtr();
       int retVal = FormatMessage(dwFlags, ref ptrlpSource, errorCode, 0, 
       ref lpMsgBuf, messageSize, &prtArguments);
       if(0 == retVal)
       {
       throw new Exception("Failed to format message for error code " + 
       errorCode + ". ");
       }
       return lpMsgBuf;
       }
    
  16. En el menú Generar, haga clic en Generar solución.

Asignar un nombre seguro al ensamblado (opcional)

Si la biblioteca de clases DPAPI administradas se va a llamar desde una aplicación de Servicios Empresariales (que debe tener un nombre seguro), la biblioteca de clases DPAPI también debe tener un nombre seguro. En este procedimiento se crea un nombre seguro para la biblioteca de clases.Si la biblioteca de clases DPAPI administradas se va a llamar directamente desde una aplicación Web ASP.NET (que no tiene un nombre seguro), puede omitir este procedimiento.

Para asignar un nombre seguro al ensamblado

  • Abra una ventana de comandos y cambie el directorio a la carpeta del proyecto DataProtection.

  • Utilice la herramienta sn.exe para generar un par de claves que se utilizará para firmar el ensamblado.

    sn -k dataprotection.snk
    
  • Vuelva a Visual Studio .NET y abra Assemblyinfo.cs.

  • Busque el atributo AssemblyKeyFile y agregue una ruta de acceso al archivo de claves en la carpeta del proyecto.

    [assembly: AssemblyKeyFile(@"..\..\dataprotection.snk")]
    
  • En el menú Generar, haga clic en Generar solución.

Referencias

Para obtener más información, consulte los siguientes artículos relacionados:

Mostrar: