Since, ISO27001, Machine-Learning and Game dev, I recently wanted to store some sensitive data (private key) in a string in C# and keep it in memory during the duration of an operation.
Due to the design of some 3rd party APIs that required a string representation of the private key, I decided on encrypting the string and only unencrypting it when I want to use it. In all other instances, the encrypted string would be copied or passed around, while the unencrypted string would not be.
I did some reading about System.
The implementation of an object that can encrypt a string, store it internally and decrypt upon request, is a ProtectedString:
using System;
using System.Security.Cryptography;
using System.Text;
namespace X.Y.Z
{
/// <summary>
/// Store string encrypted at rest.
/// </summary>
/// <remarks>You can copy this object freely</remarks>
/// <remarks>Portable alternative to SecureString, using DPAPI</remarks>
/// <remarks>Note SecureString is not recommended for new development</remarks>
/// <remarks>https://docs.microsoft.com/en-us/dotnet/api/system.security.securestring</remarks>
public class ProtectedString : IProtectedString
{
/// <summary>
/// Secret area that is encrypted/decrypted
/// </summary>
private byte[] _secretData;
private readonly object _lock = new();
private bool IsProtected { get; set; }
/// <summary>
/// DPAPI access control for securing data
/// </summary>
private readonly MemoryProtectionScope _scope;
/// <summary>
/// Creates a ProtectedString
/// </summary>
/// <param name="sensitiveString">Sensitive string</param>
/// <param name="scope">Scope of the protection</param>
public ProtectedString(string sensitiveString = null,
MemoryProtectionScope scope = MemoryProtectionScope.SameProcess)
{
_scope = scope;
// Store secret if provided and valid
if(InputValid(sensitiveString))
Set(sensitiveString);
}
private static bool InputValid(string sensitiveString)
{
return sensitiveString != null;
}
/// <inheritdoc />
public void Set(string sensitiveString)
{
try
{
lock (_lock)
{
if(!InputValid(sensitiveString))
throw new InvalidInputException();
// The secretData length should be a multiple of 16 bytes
var secretDataLength = RoundUp(
sizeof(int) + // We will store the length of the
// sensitiveString as the first sizeof(int) bytes in secretData
sensitiveString.Length, 16);
// Allocate array, all values set to \0 by .Net
_secretData = new byte[secretDataLength];
// Copy the length of the sensitiveString into the secretData
// first, starting at the first byte
BitConverter.GetBytes(sensitiveString.Length).CopyTo(_secretData, 0);
// Copy the sensitiveString itself after the bytes the above bytes
Encoding.ASCII.GetBytes(sensitiveString).CopyTo(_secretData, sizeof(int));
// Encrypt our encoded secretData using DPAPI
ProtectedMemory.Protect(_secretData, _scope);
IsProtected = true;
}
}
catch (Exception e)
{
IsProtected = false;
if (e is ProtectedStringException)
throw;
throw new Exception("Unexpected error while storing data from protected memory");
}
}
/// <inheritdoc />
public string Get()
{
try
{
lock (_lock)
{
if (!IsProtected)
throw new NotProtectedException();
// Decrypt secretData
ProtectedMemory.Unprotect(_secretData, _scope);
// Determine how long our sensitiveString was by reading the integer at byte 0
var secretLength = BitConverter.ToInt32(_secretData, 0);
// Read that many bytes to recover the original sensitiveString
var sensitiveString = Encoding.ASCII.GetString(_secretData, sizeof(int), secretLength);
// Re-protect secretData after retrieval
Set(sensitiveString);
// Return a reference to unprotected string.
return sensitiveString;
}
}
catch (Exception e)
{
if (e is ProtectedStringException)
throw;
throw new Exception("Unexpected error while retrieving data from protected memory");
}
}
private static int RoundUp(int numToRound, int multiple)
{
if (multiple == 0)
return numToRound;
int remainder = numToRound % multiple;
if (remainder == 0)
return numToRound;
return numToRound + multiple - remainder;
}
}
}
The question is if this is really useful at all from a security standpoint?
As soon as you unencrypt the contents, you get an unencrypted string back, and that string lives in memory and in theory, can be looked at by memory scanning. Also when that memory is freed (provided you don't have a reference to it), the garbage collector will free it but won't zero it out (securely clear it), so it'll be somewhere in memory, ...unencrypted.
Ultimately I never used this because of the reasons mentioned above, but it's still interesting...
Now, despite this, the above is still useful in some ways, provided you:
- a) only copy or store the protected string or pass it between functions.
- b) don't store the unencrypted string anywhere.
The other advantage is that the window of exposure of the unencrypted string is small (but it'll still get garbage collected), as you only unencrypt the ProtectedString when you want to use it, otherwise the secret is encrypted at rest.
Still, it doesn't help with the original problem of having unencrypted string copies lingering in system memory somewhere....