【发布时间】:2009-07-22 17:37:20
【问题描述】:
我知道 .NET 库提供了一种以受保护/安全方式存储字符串的方法 = SecureString。
我的问题是,如果我想存储一个字节数组,保存它的最好、最安全的容器是什么?
【问题讨论】:
我知道 .NET 库提供了一种以受保护/安全方式存储字符串的方法 = SecureString。
我的问题是,如果我想存储一个字节数组,保存它的最好、最安全的容器是什么?
【问题讨论】:
了解 System.String 类型的漏洞很重要。让它完全安全是不可能的,SecureString 的存在是为了最小化暴露的风险。 System.String 是有风险的,因为:
这里的明显风险是字符串内容在使用字符串很久之后仍然可见,从而大大增加了攻击者可以看到它的几率。 SecureString 提供了一种解决方法,将字符串存储在非托管内存中,它不受垃圾收集器的影响而留下字符串内容的杂散副本。
现在应该清楚如何创建自己的安全数组版本,并具有与 SecureString 提供的相同类型的保证。您确实没有有不变性问题,使用后擦洗数组不是问题。这本身几乎总是足够好,隐含在减少暴露的可能性是你不会长时间保持对数组的引用。因此,在垃圾回收之后,未清理的数组数据副本幸存的几率应该已经很低了。您也可以降低这种风险,仅针对小于 85,000 字节的数组显示。要么按照 SecureString 的方式进行,要么使用 Marshal.AllocHGlobal()。或者通过pinning 数组 GCHandle.Alloc() 更容易。
【讨论】:
从 .Net 2.0 开始使用 ProtectedData.Protect 方法,看起来将范围设置为 DataProtectionScope.CurrentUser 应该会产生与安全字符串相同的预期效果
取自此处的示例用法
http://msdn.microsoft.com/en-us/library/system.security.cryptography.protecteddata.protect.aspx
using System;
using System.Security.Cryptography;
public class DataProtectionSample
{
// Create byte array for additional entropy when using Protect method.
static byte [] s_aditionalEntropy = { 9, 8, 7, 6, 5 };
public static void Main()
{
// Create a simple byte array containing data to be encrypted.
byte [] secret = { 0, 1, 2, 3, 4, 1, 2, 3, 4 };
//Encrypt the data.
byte [] encryptedSecret = Protect( secret );
Console.WriteLine("The encrypted byte array is:");
PrintValues(encryptedSecret);
// Decrypt the data and store in a byte array.
byte [] originalData = Unprotect( encryptedSecret );
Console.WriteLine("{0}The original data is:", Environment.NewLine);
PrintValues(originalData);
}
public static byte [] Protect( byte [] data )
{
try
{
// Encrypt the data using DataProtectionScope.CurrentUser. The result can be decrypted
// only by the same current user.
return ProtectedData.Protect( data, s_aditionalEntropy, DataProtectionScope.CurrentUser );
}
catch (CryptographicException e)
{
Console.WriteLine("Data was not encrypted. An error occurred.");
Console.WriteLine(e.ToString());
return null;
}
}
public static byte [] Unprotect( byte [] data )
{
try
{
//Decrypt the data using DataProtectionScope.CurrentUser.
return ProtectedData.Unprotect( data, s_aditionalEntropy, DataProtectionScope.CurrentUser );
}
catch (CryptographicException e)
{
Console.WriteLine("Data was not decrypted. An error occurred.");
Console.WriteLine(e.ToString());
return null;
}
}
public static void PrintValues( Byte[] myArr )
{
foreach ( Byte i in myArr )
{
Console.Write( "\t{0}", i );
}
Console.WriteLine();
}
}
【讨论】:
没有“最好”的方法来做到这一点 - 您需要识别您试图防御的威胁,以便决定要做什么,或者是否需要做任何事情。
需要注意的一点是,与不可变的字符串不同,您可以在完成处理后将字节数组中的字节清零,因此您不会遇到与 SecureString 设计的相同的问题解决。
加密数据可能适用于某些问题,但您需要确定如何保护密钥免受未经授权的访问。
我发现很难想象以这种方式加密字节数组会有用的情况。详细说明您正在尝试做的事情会有所帮助。
【讨论】:
RtlZeroMemory 和 VirtualLock 的组合可以做你想做的事。 VirtualLock 如果您想阻止数据交换到磁盘和 RtlZeroMemory 以确保内存归零(我尝试使用RtlSecureZeroMemory,但在 kernel.dll 中似乎不存在)下面的类将存储任何数组内置类型的安全。我将解决方案分为两个类以分离出与类型无关的代码。
第一个类只是分配并保存一个数组。它会在运行时检查模板类型是否为内置类型。不幸的是,我在编译时想不出办法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
/// <summary>
/// Manage an array that holds sensitive information.
/// </summary>
/// <typeparam name="T">
/// The type of the array. Limited to built in types.
/// </typeparam>
public sealed class SecureArray<T> : SecureArray
{
private readonly T[] buf;
/// <summary>
/// Initialize a new instance of the <see cref="SecureArray{T}"/> class.
/// </summary>
/// <param name="size">
/// The number of elements in the secure array.
/// </param>
/// <param name="noswap">
/// Set to true to do a Win32 VirtualLock on the allocated buffer to
/// keep it from swapping to disk.
/// </param>
public SecureArray(int size, bool noswap = true)
{
this.buf = new T[size];
this.Init(this.buf, ElementSize(this.buf) * size, noswap);
}
/// <summary>
/// Gets the secure array.
/// </summary>
public T[] Buffer => this.buf;
/// <summary>
/// Gets or sets elements in the secure array.
/// </summary>
/// <param name="i">
/// The index of the element.
/// </param>
/// <returns>
/// The element.
/// </returns>
public T this[int i]
{
get
{
return this.buf[i];
}
set
{
this.buf[i] = value;
}
}
}
下一节课做真正的工作。它告诉垃圾收集器将数组固定在内存中。然后它会锁定它,所以它不会交换。处理后,它将数组归零并解锁,然后告诉垃圾收集器取消固定它。
/// <summary>
/// Base class of all <see cref="SecureArray{T}"/> classes.
/// </summary>
public class SecureArray : IDisposable
{
/// <summary>
/// Cannot find a way to do a compile-time verification that the
/// array element type is one of these so this dictionary gets
/// used to do it at runtime.
/// </summary>
private static readonly Dictionary<Type, int> TypeSizes =
new Dictionary<Type, int>
{
{ typeof(sbyte), sizeof(sbyte) },
{ typeof(byte), sizeof(byte) },
{ typeof(short), sizeof(short) },
{ typeof(ushort), sizeof(ushort) },
{ typeof(int), sizeof(int) },
{ typeof(uint), sizeof(uint) },
{ typeof(long), sizeof(long) },
{ typeof(ulong), sizeof(ulong) },
{ typeof(char), sizeof(char) },
{ typeof(float), sizeof(float) },
{ typeof(double), sizeof(double) },
{ typeof(decimal), sizeof(decimal) },
{ typeof(bool), sizeof(bool) }
};
private GCHandle handle;
private uint byteCount;
private bool virtualLocked;
/// <summary>
/// Initialize a new instance of the <see cref="SecureArray"/> class.
/// </summary>
/// <remarks>
/// You cannot create a <see cref="SecureArray"/> directly, you must
/// derive from this class like <see cref="SecureArray{T}"/> does.
/// </remarks>
protected SecureArray()
{
}
/// <summary>
/// Gets the size of the buffer element. Will throw a
/// <see cref="NotSupportedException"/> if the element type is not
/// a built in type.
/// </summary>
/// <typeparam name="T">
/// The array element type to return the size of.
/// </typeparam>
/// <param name="buffer">
/// The array.
/// </param>
/// <returns></returns>
public static int BuiltInTypeElementSize<T>(T[] buffer)
{
int elementSize;
if (!TypeSizes.TryGetValue(typeof(T), out elementSize))
{
throw new NotSupportedException(
$"Type {typeof(T).Name} not a built in type. "
+ $"Valid types: {string.Join(", ", TypeSizes.Keys.Select(t => t.Name))}");
}
return elementSize;
}
/// <summary>
/// Zero the given buffer in a way that will not be optimized away.
/// </summary>
/// <typeparam name="T">
/// The type of the elements in the buffer.
/// </typeparam>
/// <param name="buffer">
/// The buffer to zero.
/// </param>
public static void Zero<T>(T[] buffer)
where T : struct
{
var bufHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr bufPtr = bufHandle.AddrOfPinnedObject();
UIntPtr cnt = new UIntPtr(
(uint)buffer.Length * (uint)BuiltInTypeElementSize(buffer));
RtlZeroMemory(bufPtr, cnt);
}
finally
{
bufHandle.Free();
}
}
/// <inheritdoc/>
public void Dispose()
{
IntPtr bufPtr = this.handle.AddrOfPinnedObject();
UIntPtr cnt = new UIntPtr(this.byteCount);
RtlZeroMemory(bufPtr, cnt);
if (this.virtualLocked)
{
VirtualUnlock(bufPtr, cnt);
}
this.handle.Free();
}
/// <summary>
/// Call this with the array to secure and the number of bytes in that
/// array. The buffer will be zeroed and the handle freed when the
/// instance is disposed.
/// </summary>
/// <param name="buf">
/// The array to secure.
/// </param>
/// <param name="sizeInBytes">
/// The number of bytes in the buffer in the pinned object.
/// </param>
/// <param name="noswap">
/// True to lock the memory so it doesn't swap.
/// </param>
protected void Init<T>(T[] buf, int sizeInBytes, bool noswap)
{
this.handle = GCHandle.Alloc(buf, GCHandleType.Pinned);
this.byteCount = (uint)sizeInBytes;
IntPtr bufPtr = this.handle.AddrOfPinnedObject();
UIntPtr cnt = new UIntPtr(this.byteCount);
if (noswap)
{
VirtualLock(bufPtr, cnt);
this.virtualLocked = true;
}
}
[DllImport("kernel32.dll")]
private static extern void RtlZeroMemory(IntPtr ptr, UIntPtr cnt);
[DllImport("kernel32.dll")]
static extern bool VirtualLock(IntPtr lpAddress, UIntPtr dwSize);
[DllImport("kernel32.dll")]
static extern bool VirtualUnlock(IntPtr lpAddress, UIntPtr dwSize);
}
要使用该类,只需执行以下操作:
using (var secret = new SecureArray<byte>(secretLength))
{
DoSomethingSecret(secret.Buffer);
}
现在,这个类做了两件你不应该轻易做的事情,首先,它固定了内存。这会降低性能,因为垃圾收集器现在必须绕过它无法移动的内存。其次,它可以锁定内存中操作系统可能希望换出的页面。这会缩短系统上的其他进程,因为现在它们无法访问该 RAM。
为了最大程度地减少SecureArray<T> 的不利影响,请不要大量使用它,而只在短时间内使用它。如果您想将数据保留更长时间,则需要对其进行加密。为此,您最好的选择是 ProtectedData 类。不幸的是,这会将您的敏感数据放入不安全的字节数组中。您可以从那里做的最好的事情是快速复制到SecureArray<byte>.Buffer,然后在敏感字节数组上复制SecureArray.Zero。
【讨论】:
您可以使用 SecureString 来存储字节数组。
SecureString testString = new SecureString();
// Assign the character array to the secure string.
foreach (byte b in bytes)
testString.AppendChar((char)b);
然后您只需反转该过程以取回字节。
这不是唯一的方法,您始终可以使用 MemoryBuffer 和 System.Security.Cryptography 之外的东西。但这是唯一专门设计用于以这种方式确保安全的东西。您必须使用 System.Security.Cryptography 创建所有其他内容,这可能是您的最佳选择。
【讨论】:
一个选项:
您可以将字节存储在内存流中,并使用System.Security.Cryptography 命名空间中的任何提供程序进行加密。
【讨论】: