Almacenamiento de datos sensibles con DPAPI

Almacenamiento de datos sensibles con DPAPI

October 11, 2019

En ocasiones, tenemos que almacenar las contraseñas u otros datos sensibles en un archivo o una base de datos para utilizarlos posteriormente.

Para guardar estos datos de forma segura, podemos utilizar uno de los sistemas criptográficos existentes como AES o Triple DES.

Nuestro problema es que estos sistemas criptográficos precisan una contraseña y de dónde obtener esta contraseña.

Si no se la podemos solicitar al usuario si no que debe asociarse a la aplicación, una forma sencilla es escribirla directamente en el código. Esto, por supuesto, nos dará problemas en cuanto alguien decodifique nuestro código y obtenga la cadena.

Sería bastante mejor si realmente el propio sistema operativo nos proporcionase una clave única para almacenar datos en ese equipo. Afortunadamente, existe desde Windows 2000, se llama Data Protection application programming interface (DPAPI) y es bastante sencilla de utilizar.

De hecho, sólo tiene dos funciones, una para encriptar y la otra para desencriptar:

public byte[] Encrypt(string text)
{
	byte[] buffer;

		// La longitud de la cadena debe ser múltiplo de 16
		while (text.Length % 16 != 0)
			text += " ";
		// Convierte el texto a un array de bytes para encriptarlo
		buffer = Encoding.Unicode.GetBytes(text);
		// Encripta los datos en memoria, el resultado se almacena en el mismo buffer que los datos originales
		ProtectedMemory.Protect(buffer, MemoryProtectionScope.SameLogon);
		// Devuelve la cadena encriptada
		return buffer;
}

public string Decrypt(byte [] encrypted)
{
	// Desencripta los datos en memoria
	ProtectedMemory.Unprotect(encrypted, MemoryProtectionScope.SameLogon);
	// Devuelve la cadena desencriptada
	return Encoding.Unicode.GetString(encrypted);
}

Lo único especial de este código es que el texto debe tener una longitud múltiplo de 16, por eso le añadimos espacios al final de la cadena. Realmente, el número de bytes del buffer que utilizamos para encriptar debe ser múltiplo de 16, por eso utilizamos un Encoding.Unicode para transformar la cadena en un array de bytes.

Como vemos, en ningún caso nos solicita contraseña para encriptar o desencriptar, simplemente le indicamos el ámbito del que debe recogerla, en este ejemplo utiliza una contraseña asociada al usuario (MemoryProtectionScope.SameLogon).

Por supuesto, también tenemos funciones para encriptar / desencriptar un stream de datos:

public int EncryptDataToStream(byte[] buffer, byte[] entropy, DataProtectionScope scope, Stream streamData)
{
	byte[] encrypted = ProtectedData.Protect(buffer, entropy, ConvertScope(scope));

		// Escribe los datos encriptados
		if (streamData.CanWrite && encrypted.Length > 0)
			streamData.Write(encrypted, 0, encrypted.Length);
		// Devuelve el número de datos escritos
		return encrypted.Length;
}

public byte[] DecryptDataFromStream(byte[] entropy, DataProtectionScope scope, Stream streamData, int length)
{
	byte[] inBuffer = new byte[length];
	byte[] outBuffer;

		// Lee los datos encriptados del stream y los desencripta
		if (streamData.CanRead)
		{
			streamData.Read(inBuffer, 0, length);
			outBuffer = ProtectedData.Unprotect(inBuffer, entropy, ConvertScope(scope));
		}
		else
			throw new IOException("No se puede leer del stream");
		// Devuelve los datos desencriptados
		return outBuffer;
}

public byte [] CreateRandomEntropy()
{
	byte[] entropy = new byte[16];

		// Rellena el array con un valor aleatorio (a partir de RNGCryptoServiceProvider)
		new RNGCryptoServiceProvider().GetBytes(entropy);
		// Devuelve el array
		return entropy;
}

Aunque es bastante sencillo, debemos tener en cuenta que para encriptar / desencriptar podemos utilizar un valor de entropía, es decir, una serie de valores aleatorios que mejorarán la encriptación. Para ellos podemos usar tanto la función de ejemplo CreateRandomEntropy, como cualquier otra.

En segundo lugar, debemos mantener aparte del stream de datos, la longitud de este stream para poder descifrarlo posteriormente.