Almacenamiento de información confidencial en aplicaciones Windows - Cómo evitar la configuración en texto plano con DPAPI

· Actualizado el: · · Desarrollo Windows, Seguridad, DPAPI, C# / .NET, Win32

En el artículo anterior, «Lista de verificación para mantener la seguridad mínima en el desarrollo de aplicaciones Windows», dejamos escrita la línea mínima: «no colocar información confidencial en el código fuente ni en configuración en texto plano» y «en Win32 / .NET, usar DPAPI / ProtectedData».

En esta ocasión profundizamos un poco más en «usar DPAPI para, como mínimo, mejorar respecto al texto plano».

El público objetivo son aplicaciones Windows como las siguientes:

  • Aplicaciones de escritorio en WPF / WinForms / WinUI
  • Clientes Windows en C# / .NET
  • Aplicaciones que necesitan guardar credenciales de conexión o tokens de API en un archivo de configuración local

Lo que se trata aquí es un diseño realista para «no dejar, como mínimo, en texto plano dentro de appsettings.json un secreto que inevitablemente hay que almacenar localmente». No se trata de «una defensa completa capaz de vencer a cualquier atacante». Si se exagera ese punto, la seguridad se convierte de golpe en un cuento de terror.

1. Primero, la conclusión

En la práctica, resulta más claro pensarlo en este orden.

  1. Ante todo, no dejar que el cliente tenga secretos de larga duración
    • Priorizar la autenticación de Windows, la autenticación integrada, el inicio de sesión interactivo del usuario y la gestión de secretos del lado del servidor
  2. Si de verdad hace falta almacenamiento local, no dejarlo en texto plano
    • En Windows, la primera opción debe ser DPAPI / ProtectedData
  3. En una aplicación de escritorio normal, la base es DataProtectionScope.CurrentUser
    • LocalMachine tiene un uso bastante limitado
  4. DPAPI no protege hasta el punto de «un equipo completamente comprometido»
    • El código que se ejecuta con los mismos permisos de usuario puede, en general, descifrar lo que ese usuario puede descifrar

Y el punto más importante de este artículo es este:

«Ya que de todos modos la clave secreta hay que guardarla en algún lado, ¿no da igual, en términos de seguridad, usar texto plano o DPAPI?»

Esto es cierto a medias, y la conclusión es incorrecta.

  • Si usa AES propio y coloca la clave en la misma aplicación o la misma configuración, el resultado se acerca bastante al texto plano
  • Pero DPAPI traslada la gestión de la clave al sistema operativo y vincula al sujeto que puede descifrar con «ese usuario de Windows» o con «ese equipo»
  • Como resultado, la resistencia frente a incidentes como la fuga de un archivo de configuración aislado, su traslado a otro PC, un envío equivocado, una fuga de copia de seguridad o su inclusión en un repositorio cambia mucho

En otras palabras, aunque a nivel abstracto de «la clave está en algún lado» parezca lo mismo, «quién, en qué contexto y con cuánta facilidad puede usarla» es completamente distinto.

Decir que dejar la llave bajo el felpudo de la entrada es lo mismo que pedirla en portería tras verificar la identidad es, cuando menos, una simplificación un poco brusca.

2. Por qué es peligrosa la configuración en texto plano

El motivo por el que guardar en texto plano es peligroso es mucho más prosaico que la teoría criptográfica. En la práctica, la fuga suele producirse por caminos como estos:

  • Subir el archivo de configuración tal cual a Git
  • Que el archivo de configuración entero quede incluido en un ZIP de investigación de incidentes
  • Que se adjunte el archivo de configuración en una consulta a soporte
  • Que un tercero pueda leerlo a través de una copia de seguridad o un recurso compartido
  • Que la cadena de conexión o el token aparezcan tal cual en el log
  • Que un empleado que ya se fue, u otro usuario, pueda leer el archivo en el mismo equipo

El mayor problema del texto plano es que «en el instante en que se puede leer, deja de ser un secreto».

  • Si se puede abrir el archivo, se acabó
  • Si se puede copiar, se acabó
  • Si se adjunta a un correo, se acabó
  • Si queda en un repositorio, hay que ocuparse de él casi para siempre

Ni siquiera hace falta que el atacante sea sofisticado. Que se pueda abrir con un editor de texto ya es, por sí solo, bastante débil.

3. Respuesta a «si de todos modos la clave secreta se guarda en algún lado, ¿no es lo mismo?»

Esta duda es razonable. Y si se responde de forma descuidada, un artículo de seguridad se vuelve vago de golpe.

La respuesta es: sí, en el sentido de que «hace falta una clave en algún lado»; no, en el sentido de que por eso sea lo mismo.

3.1. Qué es igual y qué es diferente

Es cierto que el cifrado necesita, en última instancia, alguna raíz de confianza (root of trust). Un secreto no brota gratis de ningún lugar del universo. En eso el mundo es implacable.

Sin embargo, la diferencia en términos de seguridad se decide por estos tres puntos:

  • Si la aplicación tiene la clave directamente
  • A qué sujeto está vinculada la clave
  • Si se puede descifrar cuando roban solo el archivo

Si se resume esta diferencia de forma esquemática en una tabla, queda así.

Método Si leen el archivo de configuración Si solo el archivo se lleva a otro PC Si lo lee otro usuario del mismo PC Código que se ejecuta con los mismos permisos de usuario
Texto plano Se filtra en el acto Se filtra tal cual Se filtra tal cual Puede leerlo, obviamente
Cifrado propio + clave en la misma configuración/binario Se filtra bastante Se filtra bastante Se filtra bastante Puede descifrarlo, obviamente
DPAPI + CurrentUser No se puede leer de inmediato solo con el archivo Normalmente es difícil de descifrar Normalmente es difícil de descifrar Puede descifrarlo
DPAPI + LocalMachine No se puede leer de inmediato solo con el archivo Fuera de ese PC, normalmente es difícil de descifrar En el mismo PC, se puede descifrar ampliamente Puede descifrarlo

Aquí lo importante es que DPAPI separa el poder leer el archivo del poder usar el secreto.

Dónde queda el texto cifrado frente a dónde queda la clave maestra necesaria para descifrarloDiagrama que muestra que el texto cifrado puede salir del equipo (archivo de configuración, columna de BD, ZIP de investigación), mientras que la clave maestra necesaria para descifrarlo permanece del lado del sistema operativo, ya sea la clave del usuario con CurrentUser o la del equipo con LocalMachine, y cómo eso determina quién puede descifrar en cada casoLo necesario para descifrar ── permanece del lado del SO, no va dentro del texto cifradoProtegido con CurrentUserProtegido con LocalMachineClave maestra del usuarioal proteger con CurrentUserClave maestra del equipoal proteger con LocalMachineTexto cifradoVa en el archivo de configuración / una columna de BD / un ZIP de investigación= lo que se puede llegar a extraerCódigo que se ejecuta como ese usuario→ puede descifrarOtro usuario del mismo PC→ no puede descifrarCódigo en el mismo PC→ puede descifrar aunque sea otro usuarioCopiar solo el texto cifrado a otro PC→ no puede descifrarMover el perfil itinerante completo→ el material de la clave se mueve con él, así que sí puede descifrar (sección 6.5)

Figura 1: solo el texto cifrado está del lado que se puede extraer; la clave maestra necesaria para descifrarlo permanece del lado del sistema operativo. Sin embargo, el alcance de LocalMachine cubre todo el PC, y no permite excluir a otro usuario del mismo equipo.

En texto plano, estas dos cosas son la misma. Si se puede leer el archivo, se puede leer el secreto.

Pero en DPAPI, al menos con CurrentUser,

  • Como ese usuario de Windows
  • En ese contexto de Windows
  • A través del mecanismo de protección del sistema operativo

hace falta pasar para poder descifrar.

Esta diferencia es bastante grande en el terreno de los incidentes reales.

3.2. «Aun así, si es el mismo usuario, se puede descifrar», es cierto

Este es un punto que conviene explicar sin rodeos.

El código que se ejecuta con los mismos permisos de usuario puede, en general, descifrar lo que ese usuario puede descifrar.

Es decir, DPAPI no tiene como objetivo principal situaciones como estas:

  • El equipo ya está comprometido por malware
  • El atacante puede ejecutar código como ese usuario
  • El equipo está totalmente tomado a nivel de administrador

En esta situación, dado que la propia aplicación puede descifrar, el código del atacante también puede hacerlo. Aquí, decir «pero está cifrado» no resulta demasiado tranquilizador.

DPAPI resulta eficaz sobre todo frente a la fuga de archivos, la ubicación incorrecta, el traslado fuera de línea y el acceso por parte de otro usuario.

Confundir esto lleva a dos errores:

  • Subestimar lo que sí se protege y no usarlo
  • Sobrestimar lo que no se protege y confiarse

Ambos son, discretamente, peligrosos.

3.3. Entonces, ¿cuál es la ventaja?

Si hay que resumir en una frase la ventaja de DPAPI, es esta:

«poder separar el secreto en sí mismo de la legibilidad del archivo de configuración».

Por ejemplo, en incidentes como los siguientes, la diferencia entre texto plano y DPAPI se nota:

  • Un usuario envió el archivo de configuración a soporte por error
  • El archivo de configuración quedó incluido en un ZIP de investigación
  • Solo el archivo de configuración se filtró desde una copia de seguridad
  • Se copió a una carpeta compartida
  • Un desarrollador pudo dejar el contenido ilegible con solo ver el texto cifrado

Esta es una ventaja bastante realista. Sin necesidad de convertir al atacante en un superhombre de película, se puede reducir el radio de los incidentes cotidianos.

4. Por qué DPAPI es la opción adecuada

Al manejar secretos con almacenamiento local en Windows, estas son las razones por las que DPAPI resulta adecuado en la práctica.

4.1. Se puede delegar la gestión de claves al sistema operativo

Generar su propia clave AES, guardarla, asignarle permisos, rotarla, considerar el impacto de una fuga y además incorporar detección de manipulación. Esto pesa más de lo que parece. Y si se hace de forma descuidada, casi siempre termina con la clave colocada en el mismo lugar.

Al usar DPAPI, se puede separar de la implementación de la aplicación el problema de «cómo crear la clave de cifrado y dónde colocarla».

En ese sentido, es más preciso ver DPAPI no como «una API para elegir un algoritmo de cifrado», sino como «una API que delega la gestión de claves al sistema operativo».

4.2. Se puede vincular el sujeto de descifrado al usuario de Windows o al equipo

En una aplicación de escritorio normal, en muchos casos basta con elegir CurrentUser.

  • Que ese usuario tenga la sesión iniciada
  • Que el proceso se ejecute en el contexto de ese usuario

son los requisitos previos para poder descifrar.

Por eso se obtiene la propiedad de que copiar solo el texto cifrado a otro PC no permite usarlo tal cual.

4.3. Es fácil incluir detección de manipulación

Un error común en el cifrado propio es dar por terminado el trabajo con «ya cifré con AES» y olvidar la detección de manipulación.

DPAPI también incluye protección de integridad sobre los datos cifrados, así que tiene la ventaja práctica de que detectar una reescritura no autorizada del texto cifrado se puede apoyar fácilmente en el mecanismo del propio sistema operativo.

4.4. Se puede usar directamente desde C# / .NET

Desde C# se puede usar tal cual System.Security.Cryptography.ProtectedData. No tener que añadir bibliotecas adicionales también ayuda bastante en una aplicación exclusiva de Windows.

5. Qué protege DPAPI y qué no protege

Aquí conviene separar las cosas con claridad; es más seguro así.

5.1. Lo que se vuelve más fácil de proteger

DPAPI resulta eficaz al menos en escenarios como estos.

  • Fuga en texto plano del archivo de configuración
  • Traslado del archivo a otro PC
  • Acceso desde otro usuario del mismo PC (asumiendo CurrentUser)
  • Fuga como copia de seguridad o archivo adjunto
  • Situaciones de «se pudo leer sin querer» en el entorno de desarrollo o mantenimiento

5.2. Lo que no se protege, o cuya protección es débil

Por otro lado, en las siguientes situaciones más vale no confiarse demasiado.

  • Código de ataque que se ejecuta con los mismos permisos de usuario
  • Compromiso completo del propio equipo
  • Toma de control con privilegios de administrador
  • El texto plano en memoria una vez que la aplicación ya lo descifró
  • Un secreto de larga duración distribuido por igual a todos los clientes

Este último, «un secreto de larga duración común a todos los clientes», es especialmente importante.

Por ejemplo, diseños como estos:

  • Incrustar la misma clave de API en todos los clientes
  • Que todos los equipos compartan la misma contraseña
  • Distribuir una clave de descifrado fija que se completa solo del lado del cliente

tienden a propagarse a todo el conjunto en cuanto se extraen de un solo equipo. La lógica es sencilla: si la aplicación puede descifrar en un solo equipo, ese secreto se puede extraer.

DPAPI es eficaz para «hacer que ese lugar de almacenamiento sea mejor que el texto plano», pero no justifica un secreto que, para empezar, no debería estar en el cliente.

Este tipo de secreto, más que buscar una forma más ingeniosa de guardarlo, lo apropiado es hacerlo escapar en alguna de estas direcciones:

  • Colocarlo del lado del servidor
  • Que el cliente solo tenga un token
  • Convertirlo en una credencial por usuario
  • Convertirlo en un token con vencimiento

6. Cómo elegir entre CurrentUser y LocalMachine

Este punto es bastante importante. Elegir a la ligera cambia el significado.

6.1. Lo básico es CurrentUser

En una aplicación de escritorio de Windows normal, la primera opción a considerar es CurrentUser.

Ejemplos donde encaja bien:

  • Aplicaciones de escritorio orientadas al usuario en WPF / WinForms / WinUI
  • Aplicaciones que tienen configuración o credenciales por usuario
  • Aplicaciones que guardan su configuración bajo %LocalAppData% o %AppData%

En este caso, resulta más fácil tratarlo como «el secreto de ese usuario de Windows».

6.2. LocalMachine tiene un uso bastante limitado

LocalMachine parece cómodo, pero en una aplicación de escritorio normal resulta demasiado amplio.

Encaja, por ejemplo, en casos como estos:

  • Un servicio de Windows en una máquina de confianza y de un solo propósito
  • Un secreto que solo usa un proceso específico en esa máquina
  • Casos en los que hace falta usarlo en el mismo equipo entre distintos usuarios que inician sesión

Sin embargo, las advertencias pesan.

  • Se puede descifrar ampliamente desde cualquier proceso que se ejecute en ese PC
  • Tiende a ser peligroso en equipos compartidos, RDS, equipos de salto (jump hosts) o entornos con varios usuarios
  • Elegirlo porque «total, así pueden usarlo todos y es más cómodo» suele traer problemas más adelante

Y el motivo por el que dan ganas de elegir LocalMachine suele reducirse a estos tres:

  • Se puede leer aunque se cambie de usuario
  • También se puede leer desde un servicio
  • Es cómodo si funciona

Todos son motivos de «comodidad», no de «protección». Si una aplicación de escritorio normal elige LocalMachine, amplía la posibilidad de descifrado a otros procesos de ese mismo PC, así que el significado cambia bastante.

6.3. Si tiene dudas, piense así

  • Aplicación de UI normal -> CurrentUser
  • Caso especial en el que de verdad hay que proteger a nivel de máquina -> LocalMachine
  • Hace falta poder descifrar con cualquier usuario, pero en el equipo también hay otros usuarios -> casi siempre conviene replantear el diseño

6.4. Con servicios o impersonation hay que tener algo más de cuidado

Cuando entran en juego servicios de Windows o impersonation, el significado de CurrentUser se vuelve algo más pesado.

  • Quién es la cuenta de ejecución
  • Si el perfil de esa cuenta está cargado
  • En qué contexto ocurre el momento de descifrar

Si algo de esto se desalinea, es fácil terminar con «se pudo cifrar, pero no se puede descifrar». En casos de servicio, no siempre basta con «de momento, CurrentUser».

En el caso de impersonation, el fallo típico que también documenta Microsoft Learn es «Key not valid for use in specified state.». DPAPI mantiene los datos de la clave en el perfil de usuario, así que si el perfil no está cargado, no se puede descifrar. Antes de hacer impersonate, hace falta cargar el perfil del usuario objetivo.

6.5. Conozca los casos operativos en los que «deja de poder descifrarse»

En la práctica, duele más un incidente en el que deja de poder descifrarse que el propio cifrado. DPAPI es un mecanismo que vincula al sujeto que puede descifrar con el usuario o el equipo de Windows, así que en cuanto se rompe ese vínculo, deja de poder leerse.

Conviene conocer de antemano estos cinco casos.

Caso Qué ocurre Cómo prepararse
Restablecimiento de contraseña por un administrador La protección ligada a la contraseña del usuario se desvincula y puede dejar de poder accederse a los datos protegidos con DPAPI. La información de soporte de Microsoft también documenta este fenómeno como algo que ocurre después de que un administrador restablece la contraseña Diseñe el secreto de forma que se pueda «volver a obtener». Ante un fallo de descifrado, oriente al usuario a volver a introducirlo
Recreación del perfil El perfil nuevo tiene material de clave distinto, así que el texto cifrado anterior no se puede descifrar Mantenga una versión en el archivo de configuración y no trate el fallo de descifrado como un error fatal
Copiar solo el texto cifrado a otro PC El material de clave necesario para descifrar está del lado del perfil de usuario, así que llevarse solo el texto cifrado de CurrentUser no permite leerlo (esta es la otra cara de la «ventaja» mencionada en 3.1) Diseñe partiendo de que hay que volver a guardar el secreto en cada equipo
Perfil itinerante En este caso sí se puede leer. El material de clave se mueve junto con el perfil, y Microsoft Learn también indica explícitamente que un usuario con perfil itinerante puede descifrar desde otro equipo de la red. Si le da el mismo trato que a la fila anterior, terminará recreando credenciales innecesariamente durante la migración No dé por hecho que «al ser otro PC, no se puede leer». Confirme si hay itinerancia antes de decidir el procedimiento de migración
Cambio de la cuenta de ejecución del servicio Si la cuenta de ejecución cambia entre el momento de proteger y el de descifrar, con CurrentUser no se puede leer Incluya en la operación un procedimiento para volver a proteger el secreto al cambiar de cuenta

En resumen, escriba el código partiendo de la premisa de que ProtectedData.Unprotect puede fallar. Cuando el descifrado falla, se lanza CryptographicException, así que hay que capturarla y orientar al usuario a volver a introducir el valor.

using System;
using System.Security.Cryptography;
using System.Text;

// protectedBase64: texto cifrado leído del archivo de configuración (Base64)
// entropy: pase el mismo valor que usó al proteger. También puede ser null
static bool TryUnprotect(string protectedBase64, byte[]? entropy, out string plaintext)
{
    plaintext = string.Empty;

    try
    {
        byte[] plainBytes = ProtectedData.Unprotect(
            Convert.FromBase64String(protectedBase64),
            optionalEntropy: entropy,
            scope: DataProtectionScope.CurrentUser);

        plaintext = Encoding.UTF8.GetString(plainBytes);
        return true;
    }
    catch (CryptographicException)
    {
        // No se puede descifrar = es muy probable que el entorno haya cambiado.
        // No lo deje fallar aquí; oriente al llamador hacia pedir el valor de nuevo
        return false;
    }
    catch (FormatException)
    {
        // Cuando el Base64 está corrupto
        return false;
    }
}

Cuando llegan consultas del tipo «lo cifré, pero no se puede descifrar», casi siempre corresponden a alguno de los casos de esta tabla.

7. Pautas mínimas de implementación

Si el objetivo es simplemente «dejar de tener texto plano en el archivo de configuración» en una aplicación Windows, el diseño no tiene por qué complicarse tanto. Aun así, hay varios puntos que conviene no pasar por alto.

7.1. Proteja solo los secretos

En lugar de cifrar toda la configuración de una vez, resulta más manejable proteger solo los elementos secretos.

Por ejemplo, se puede dividir así:

  • URL del servidor
  • Nombre de usuario
  • Nombre de la base de datos
  • Indicadores de funcionalidad (feature flags)

Estos pueden quedarse en texto plano en muchos casos.

Por otro lado,

  • Contraseña
  • Token de API
  • Token de actualización (refresh token)
  • Credenciales de una carpeta compartida

son los que hay que proteger.

Con esta división se obtiene:

  • La configuración es más fácil de editar
  • Es más fácil comparar diferencias
  • Queda claro dónde está el secreto
  • La operación en conjunto es más simple

7.2. El destino de almacenamiento debe ser, en principio, por usuario

En una aplicación de escritorio normal, el destino de almacenamiento debe ser, en principio, una ubicación por usuario.

  • %LocalAppData%\Vendor\App\settings.json
  • %AppData%\Vendor\App\settings.json

Al menos, más vale no colocarlo descuidadamente dentro de la carpeta de instalación ni en un lugar fácil de compartir.

Aunque esté protegido con DPAPI, si la ACL del destino de almacenamiento es descuidada, se termina en una situación de «el texto cifrado se puede leer», «la estructura de la configuración se puede ver» y «los errores operativos ocurren». La defensa no es de una sola capa; funciona mejor cuando se acumulan varias.

7.3. optionalEntropy no es una segunda clave todopoderosa

ProtectedData permite pasar optionalEntropy. Es útil, pero no es «una segunda clave mágica que, con solo incrustarla en el binario, hace todo seguro».

  • Si se coloca en el mismo archivo, no se convierte en un secreto
  • Aunque se incruste como valor fijo en el binario, no se puede considerar un secreto fuerte
  • Aun así, resulta útil para identificar el propósito y prevenir usos incorrectos

En la práctica, basta con pasar como secuencia fija de bytes algo como:

  • El nombre de la aplicación
  • El nombre del propósito
  • Un identificador de versión

y usarlo para «no aceptar por error texto cifrado destinado a otro propósito».

7.4. Que el texto cifrado pueda ir a Git no significa que deba hacerlo

Este punto también es discretamente importante.

El texto cifrado de DPAPI es mucho mejor que el texto plano, pero eso no significa que esté bien subir el archivo de configuración entero al repositorio.

El motivo es sencillo:

  • El texto cifrado permanece mucho tiempo
  • Es posible que algún día se reproduzca el mismo equipo o el mismo contexto
  • El archivo también contiene información distinta del secreto
  • Se termina creando una cultura de «como está protegido, se puede tratar sin cuidado»

«Mejor que el texto plano» y «seguro en cualquier lugar» son cosas completamente distintas.

7.5. No lo registre en el log

Un caso sorprendentemente frecuente es descifrar el valor y luego arruinarlo todo al volcarlo al log.

  • Volcar la cadena de conexión entera cuando falla la conexión
  • Dejar la cabecera Authorization cuando la API devuelve 401
  • Mezclar el secreto en el mensaje de una excepción

Si hace algo así, aunque haya dejado de tener texto plano en el archivo de configuración, el log termina siendo, en la práctica, un almacén de texto plano. Es una pena, pero es bastante habitual en la práctica.

8. Ejemplo de implementación mínima en C# / .NET

8.1. Antes que nada, hace falta agregar una referencia

ProtectedData parece formar parte del BCL, pero de dónde viene depende del framework de destino. Si tropieza aquí, el nombre de tipo ProtectedData no se resuelve.

Destino Trabajo necesario Origen
.NET Framework Agregar al proyecto una referencia al ensamblado System.Security System.Security.dll
.NET Core / .NET 5 en adelante (incluye .NET 6 / 8) Agregar el paquete NuGet System.Security.Cryptography.ProtectedData System.Security.Cryptography.ProtectedData.dll

Este paquete no está incluido en ningún framework compartido de .NET Core / .NET 5 en adelante. Incluso con un destino orientado a Windows como net8.0-windows, hace falta agregar la referencia de forma explícita.

dotnet add package System.Security.Cryptography.ProtectedData

Hay otro punto que conviene saber antes de la implementación. ProtectedData es exclusivo de Windows. Como depende de DPAPI, si se invoca desde .NET en una plataforma que no sea Windows, se lanza PlatformNotSupportedException. Si su base de código está pensada para ser multiplataforma, diséñela de otra forma desde el principio, tal como se indica en 10.1.

8.2. Implementación mínima

A continuación se muestra un ejemplo mínimo que protege con CurrentUser una cadena de texto que se va a guardar en un archivo de configuración. Se incluye un optionalEntropy fijo para identificar el propósito, pero no lo considere una clave secreta.

using System;
using System.Security.Cryptography;
using System.Text;

public static class DpapiSecretProtector
{
    // Para identificar el uso. No es una segunda clave secreta.
    private static readonly byte[] Entropy =
        Encoding.UTF8.GetBytes("ComComponent:DesktopApp:SettingsSecret:v1");

    public static string ProtectToBase64(string plaintext)
    {
        ArgumentNullException.ThrowIfNull(plaintext);

        byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext);
        byte[] protectedBytes = Array.Empty<byte>();

        try
        {
            protectedBytes = ProtectedData.Protect(
                plainBytes,
                optionalEntropy: Entropy,
                scope: DataProtectionScope.CurrentUser);

            return Convert.ToBase64String(protectedBytes);
        }
        finally
        {
            Array.Clear(plainBytes, 0, plainBytes.Length);

            if (protectedBytes.Length > 0)
            {
                Array.Clear(protectedBytes, 0, protectedBytes.Length);
            }
        }
    }

    public static string UnprotectFromBase64(string protectedBase64)
    {
        ArgumentNullException.ThrowIfNull(protectedBase64);

        byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
        byte[] plainBytes = Array.Empty<byte>();

        try
        {
            plainBytes = ProtectedData.Unprotect(
                protectedBytes,
                optionalEntropy: Entropy,
                scope: DataProtectionScope.CurrentUser);

            return Encoding.UTF8.GetString(plainBytes);
        }
        finally
        {
            Array.Clear(protectedBytes, 0, protectedBytes.Length);

            if (plainBytes.Length > 0)
            {
                Array.Clear(plainBytes, 0, plainBytes.Length);
            }
        }
    }
}

El uso es sencillo.

string protectedPassword = DpapiSecretProtector.ProtectToBase64(password);

// Guardar, por ejemplo, en JSON
// settings.DbPasswordProtected = protectedPassword;

string password = DpapiSecretProtector.UnprotectFromBase64(settings.DbPasswordProtected);

El archivo de configuración puede quedar, por ejemplo, con esta forma.

{
  "ApiBaseUrl": "https://api.example.com/",
  "UserName": "app-user",
  "PasswordProtected": "AQAAANCMnd8BFdERjHoAwE..."
}

Lo bueno de esta forma es que:

  • La URL y el nombre de usuario se pueden editar con normalidad
  • Solo la contraseña queda protegida
  • La estructura de la configuración es fácil de ver
  • Es menos propensa a incidentes que dejarla toda en texto plano

9. Diseños que siguen siendo peligrosos

Aunque use DPAPI, los siguientes diseños todavía son peligrosos.

9.1. Mantener el valor descifrado en memoria durante mucho tiempo

Conviene evitar:

  • Volcarlo al log
  • Mostrarlo en pantalla
  • Incluirlo en una excepción
  • Dejarlo cargado indefinidamente en un objeto de vida larga

«Cifrado al guardarlo» y «seguro mientras se usa» son problemas distintos.

9.2. Dar el mismo secreto a todas las instalaciones

Un diseño en el que todos los usuarios comparten la misma clave de API no se resuelve de raíz por guardarla con DPAPI. El motivo, y hacia dónde hacerla escapar en su lugar, están resumidos en 5.2.

9.3. Elegir LocalMachine «porque es cómodo»

Esto también es bastante habitual. Pero es «comodidad», no «protección». El motivo por el que dan ganas de elegirlo, y qué se amplía al hacerlo, están en 6.2. Si tiene dudas al decidir, revise las tres líneas de 6.3.

9.4. Sentirse seguro por agregar cifrado propio

En lugar de DPAPI, incorporar implementaciones como:

  • Incrustar la clave AES en el código fuente
  • Colocar la clave AES en otro elemento del archivo de configuración
  • Tratar como clave una «cadena un poco ofuscada»

suele tener un efecto bastante limitado.

Entre «no es texto plano» y «es seguro» hay una brecha bastante grande.

10. Casos en los que DPAPI no es suficiente

DPAPI es útil, pero no es todopoderoso. En los siguientes casos conviene considerar otra opción.

10.1. Cuando también se necesita ejecutar fuera de Windows

DPAPI / ProtectedData está orientado a Windows. En una aplicación multiplataforma, no se puede construir partiendo de esa premisa.

10.2. Cuando se necesita compartir el mismo secreto entre varias máquinas o usuarios

Un requisito como «descifrar el mismo texto cifrado en varios PC» o «compartirlo entre varios usuarios» queda fuera del terreno en el que DPAPI destaca, que es vincularlo a «ese equipo, ese usuario».

En este caso conviene considerar otro diseño acorde al requisito, como:

  • Gestión de secretos del lado del servidor
  • Una plataforma de credenciales
  • Autenticación de Windows / autenticación integrada
  • Un almacén de credenciales para la aplicación

10.3. Cuando lo que se guarda son credenciales de usuario en sí

Cuando lo que se quiere guardar es claramente un par de

  • nombre de usuario
  • contraseña

resulta más natural usar el almacén de credenciales que ofrece Windows en lugar de escribirlo con DPAPI en un archivo propio. Este es un punto que suele generar dudas en la práctica, así que se incluye una comparación.

Aspecto DPAPI (ProtectedData) Credential Locker (PasswordVault) Credential Manager (CredWrite / CredRead)
Dónde se guarda Un archivo que usted decide (cómo colocar el texto cifrado depende de la aplicación) El almacén de credenciales que gestiona Windows El almacén de credenciales que gestiona Windows
Qué se puede guardar Cualquier secuencia de bytes (cadena de conexión, token, o incluso parte de una configuración) Un par de usuario y contraseña Una credencial (una estructura según el tipo)
API System.Security.Cryptography Windows.Security.Credentials de WinRT Win32 (wincred.h / Advapi32.dll)
¿Se puede usar desde una app de escritorio? Se usa directamente Se puede usar no solo desde WinUI, sino también desde WPF / WinForms (hace falta configurar la llamada a la API de WinRT) Se usa directamente
Sincronización Ninguna Se sincroniza entre equipos mediante una cuenta Microsoft (itinerancia) Ninguna (conjunto de credenciales de usuario local)
Límites Prácticamente ninguno Hasta 20 elementos por aplicación. No está pensado para datos grandes Ligado a la sesión de inicio de sesión (logon) del token actual
Quién administra La aplicación (decide tanto el destino de almacenamiento como la ACL) El sistema operativo (no hace falta diseñar dónde guardarlo) El sistema operativo (se puede administrar desde el Administrador de credenciales del Panel de control)

El criterio para elegir es este:

  • Lo que se guarda es un par de usuario y contraseña, y la cantidad es pequeña -> Credential Locker / Credential Manager es la primera opción. Así no hace falta diseñar ni el destino de almacenamiento ni la ACL usted mismo
  • Lo que se guarda no tiene la forma de «usuario + contraseña» -> cosas como una cadena de conexión, un token de API, un token de actualización o parte de un archivo de configuración encajan mejor con DPAPI. Este es el caso que trata este artículo
  • Hace falta trasladarlo entre equipos -> la itinerancia de Credential Locker resulta útil. El CurrentUser de DPAPI, en cambio, tiene como ventaja precisamente «no trasladarse», así que aquí el objetivo es el opuesto
  • La cantidad es grande o el tamaño es grande -> se topa con el límite de 20 elementos de Credential Locker. En ese caso, use DPAPI con su propio archivo

Además, el almacén de credenciales también se apoya, en el fondo, en el mecanismo de protección del sistema operativo, así que no se trata de un orden de «más seguro que DPAPI» o «DPAPI es inferior». Lo práctico es elegir según la forma de lo que se quiere guardar y si hace falta itinerancia.

Y elija lo que elija, si es una aplicación nueva, vale la pena considerar primero una opción sin contraseña como Windows Hello o las claves de acceso (passkeys). Si de entrada se puede evitar tener una contraseña de larga duración, esa es la opción más sólida.

El eje central de este artículo sigue siendo, ante todo, la línea práctica de DPAPI para «dejar de tener texto plano en el archivo de configuración de un cliente Windows».

11. Orden de prioridad recomendado en la práctica

Por último, en la práctica resulta más fácil ordenar las ideas si, ante la duda, se piensa en este orden. Evalúe cada paso de arriba hacia abajo y baje al siguiente solo cuando no se cumpla la condición.

Flujo de decisión para elegir el método de almacenamiento de secretos en un cliente WindowsDiagrama de flujo que primero evalúa si se puede evitar que el equipo tenga el secreto y si se puede separar por usuario, luego determina si lo que se quiere guardar es un par de usuario y contraseña (a favor de Credential Locker o Credential Manager) o tiene otra forma como un token (a favor de DPAPI), considera si hace falta trasladarlo entre equipos y cuántas cuentas necesitan descifrarlo, y termina recomendando DPAPI con CurrentUser, DPAPI con LocalMachine como excepción, o revisar el método de autenticaciónSe puede evitarNo se puede evitarNo se puede separarSe puede separarEse par, y pocos registrosCuenta de dominio o localNoOtra forma, como un tokenNoBasta con una (el propio usuario,o una cuenta de servicio dedicada)Hace falta descifrardesde varias cuentasSí, lo puedo garantizarNo lo puedo garantizar¿Se puede evitar que el equipotenga el secreto a largo plazo?Prioridad 1: no tenerlo(autenticación de Windows, tokens de corta vida)¿Se puede separar el secretopor usuario?¿Se volvió una clave común?Revise el diseño¿Lo que se quiere guardar tiene la formade usuario + contraseña? (sección 10.3)¿Hay que trasladarlo entre equipos?¿Es un equipo sincronizadocon una cuenta Microsoft? (sección 10.3)Use la itineranciade Credential LockerDPAPI no permite trasladarlo.Gestiónelo en el servidor (sección 10.2)Credential Locker /Credential Manager¿Hay que trasladarlo entre equipos?¿Cuántas cuentas descifranel mismo secreto?Prioridad 3: DPAPI + CurrentUserSi se ejecuta sin usuario interactivo,verifique que el perfil esté cargado (sección 6.4)¿Puede garantizar queno habrá otros usuarios?Prioridad 4: DPAPI + LocalMachineTrátelo como excepción y deje constancia del motivoOtros usuarios también podrían descifrarlo.Revise el método de autenticación

Figura 2: orden de selección del método de almacenamiento. Primero se observa la forma del secreto y si hace falta itinerancia, y solo después se entra en DPAPI. LocalMachine no se elige «porque es cómodo», sino como excepción cuando ninguna otra opción es viable.

Prioridad 1: ante todo, no tenerlo

  • Autenticación de Windows
  • Autenticación integrada
  • Inicio de sesión interactivo
  • Mantener el secreto del lado del servidor
  • Tokens de corta vida

Prioridad 2: orientarse a secretos por usuario

  • Preferir lo por usuario sobre un secreto común
  • Preferir un token renovable sobre una credencial fija de larga duración
  • Evitar una clave común a todos los clientes

Prioridad 3: si hace falta almacenamiento local, use DPAPI

  • Normalmente, CurrentUser
  • El destino de almacenamiento debe ser por usuario
  • Proteja solo los elementos secretos
  • No lo registre en el log

Prioridad 4: LocalMachine como caso excepcional

  • ¿De verdad hace falta a nivel de máquina?
  • ¿No entrarán otros usuarios en ese equipo?
  • ¿Es razonable como diseño de servicio?

12. Resumen

Cuando una aplicación Windows necesita guardar información confidencial en un archivo de configuración, es mejor evitar dejarla en texto plano.

Y ante la pregunta:

«De todos modos la clave se guarda en algún lado, ¿no da igual?»

en la práctica conviene responder así:

  • Si con un cifrado propio la clave se coloca en el mismo lugar, el resultado es bastante parecido
  • Con DPAPI no es lo mismo
    • Se puede trasladar la gestión de la clave al sistema operativo
    • Se puede vincular al sujeto que descifra con el usuario de Windows o con el equipo
    • Se puede evitar que la fuga de un archivo de configuración aislado se convierta directamente en la fuga del secreto
  • Sin embargo

    • El código que se ejecuta con los mismos permisos de usuario
    • Un equipo completamente comprometido
    • Un secreto común de larga duración que no debería estar en el cliente

no se resuelven con esto.

En resumen, DPAPI no es una muralla todopoderosa. Pero sí tiene el efecto de convertir el ventanal sin protección del texto plano en el archivo de configuración en, al menos, una ventana razonablemente decente.

En la práctica con clientes Windows, esta diferencia es bastante grande. Empezar por no fallar en este punto es, en definitiva, lo más realista.

13. Referencias

  • Artículo anterior: https://comcomponent.com/es/blog/windows-app-security-minimum-checklist/
  • Microsoft Learn: CryptProtectData https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata
  • Microsoft Learn: ProtectedData https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0
  • Microsoft Learn: DataProtectionScope https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotectionscope?view=windowsdesktop-10.0
  • Microsoft Learn: How to: Use Data Protection https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection
  • Microsoft Learn: Credential locker for Windows apps https://learn.microsoft.com/en-us/windows/apps/develop/security/credential-locker
  • Microsoft Learn: CredWrite (API Win32 de Windows Credential Manager) https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credwritew
  • NuGet: System.Security.Cryptography.ProtectedData https://www.nuget.org/packages/System.Security.Cryptography.ProtectedData
  • Soporte de Microsoft: no se puede acceder a los datos de DPAPI después de que un administrador restablece la contraseña https://support.microsoft.com/en-us/topic/you-cannot-access-dpapi-data-after-an-administrator-resets-your-password-on-a-windows-server-2012-based-domain-controller-4aa890cd-12b5-fe5c-9e68-06244e70673d

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Desarrollo de aplicaciones para Windows

Este tema abarca desde el método de almacenamiento de credenciales hasta la ubicación de la configuración por usuario y la forma de registrar información en el log, por lo que se relaciona con el diseño integral de la aplicación Windows y encaja bien con Desarrollo de aplicaciones Windows.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué es DPAPI?
DPAPI (Data Protection API) es un mecanismo de protección de datos que ofrece Windows: delega la gestión de las claves de cifrado al sistema operativo y vincula al sujeto que puede descifrar los datos con ese usuario de Windows o con ese equipo. Desde C# / .NET se puede usar a través de la clase System.Security.Cryptography.ProtectedData sin añadir bibliotecas adicionales. Más que una API para elegir un algoritmo de cifrado, es más preciso verla como una API que delega la gestión de claves al sistema operativo. Es una opción realista para no dejar en texto plano las contraseñas o los tokens de API que se guardan en los archivos de configuración.
Como la clave se guarda en algún lado de todos modos, ¿no es lo mismo usar texto plano que DPAPI?
No es lo mismo. Si implementa su propio cifrado AES y guarda la clave en la misma aplicación o el mismo archivo de configuración, el resultado se acerca bastante al texto plano; en cambio, DPAPI traslada la gestión de la clave al sistema operativo y vincula al sujeto que puede descifrar con el usuario de Windows o con el equipo. Como resultado, cambia mucho la resistencia frente a incidentes como la fuga de un archivo de configuración aislado, su traslado a otro PC, un envío equivocado, una fuga de copia de seguridad o su inclusión accidental en un repositorio. La diferencia decisiva de DPAPI frente al texto plano es que separa el poder leer el archivo del poder usar el secreto.
¿Qué es lo que DPAPI no puede proteger?
El código que se ejecuta con los mismos permisos de usuario puede, en general, descifrar todo lo que ese usuario puede descifrar. Por eso no protege situaciones en las que el equipo está comprometido por malware, ha sido tomado con privilegios de administrador, o el texto plano ya se encuentra en memoria después de descifrarse. Además, un secreto de larga duración que se distribuye por igual a todos los clientes tiende a propagarse a todo el conjunto en cuanto se extrae de un solo equipo, así que guardarlo con DPAPI no resuelve el problema de raíz. DPAPI resulta eficaz sobre todo frente a la fuga de archivos, la ubicación incorrecta, el traslado fuera de línea y el acceso por parte de otro usuario.
¿Debo usar CurrentUser o LocalMachine en DataProtectionScope?
En una aplicación de escritorio de Windows normal, CurrentUser es la opción básica. Permite tratar el secreto como propio de ese usuario y hace que el texto cifrado, si se copia solo a otro PC, resulte difícil de usar tal cual. LocalMachine se puede descifrar ampliamente desde cualquier proceso que se ejecute en ese PC, por lo que tiende a ser peligroso en equipos compartidos o en entornos con varios usuarios, y su uso queda bastante limitado a casos como un servicio de Windows en una máquina de un solo propósito y de confianza. Elegir LocalMachine porque «lo pueden usar todos y es cómodo» suele traer problemas más adelante.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog