也许很多人都是第一次知道还有SecureString这样一个类型,我也不例外。SecureString并不是一个常用的类型,但在一些拥有特殊需求的额场合,它就会有很大的作用。顾名思义,SecureString意为安全的字符串,它被设计用来保存一些机密的字符串,完成传统字符串所不能做到的工作。
(1)传统字符串以明码的形式被分配在内存中,一个简单的内存读写软件就可以轻易地捕获这些字符串,而在这某些机密系统中是不被允许的。也许我们会觉得对字符串加密就可以解决类似问题,But,事实总是残酷的,对字符串加密时字符串已经以明码方式驻留在内存中很久了!对于该问题唯一的解决办法就是在字符串的获得过程中直接进行加密,SecureString的设计初衷就是解决该类问题。
(2)为了保证安全性,SecureString是被分配在非托管内存上的(而普通String是被分配在托管内存中的),并且SecureString的对象从分配的一开始就以加密的形式存在,我们所有对于SecureString的操作(无论是增删查改)都是逐字符进行的。
逐字符机制:在进行这些操作时,驻留在非托管内存中的字符串就会被解密,然后进行具体操作,最后再进行加密。不可否认的是,在具体操作的过程中有小段时间字符串是处于明码状态的,但逐字符的机制让这段时间维持在非常短的区间内,以保证破解程序很难有机会读取明码的字符串。
(3)为了保证资源释放,SecureString实现了标准的Dispose模式(Finalize+Dispose双管齐下,因为上面提到它是被分配到非托管内存中的),保证每个对象在作用域退出后都可以被释放掉。
Note:关于内存释放方式:将其对象内存全部置为0,而不是仅仅告诉CLR这一块内存可以分配,当然这样做仍然是为了确保安全。熟悉C/C++的朋友可能就会很熟悉,这不就是 memset 函数干的事情嘛!下面这段C代码便使用了memset函数将内存区域置为0:
// 下面申请的20个字节的内存有可能被别人用过 char chs[20]; // memset内存初始化:memset(void *,要填充的数据,要填充的字节个数) memset(chs,0,sizeof(chs));
看完了SecureString的原理,现在我们通过下面的代码来熟悉一下在.NET中的基本用法:
using System; using System.Runtime.InteropServices; using System.Security; namespace UseSecureString { class Program { static void Main(string[] args) { // 使用using语句保证Dispose方法被及时调用 using (SecureString ss = new SecureString()) { // 只能逐字符地操作SecureString对象 ss.AppendChar('e'); ss.AppendChar('i'); ss.AppendChar('s'); ss.AppendChar('o'); ss.AppendChar('n'); ss.InsertAt(1, 'd'); // 打印SecureStrign对象 PrintSecureString(ss); } Console.ReadKey(); } // 打印SecureString对象 public unsafe static void PrintSecureString(SecureString ss) { char* buffer = null; try { // 只能逐字符地访问SecureString对象 buffer = (char*)Marshal.SecureStringToCoTaskMemUnicode(ss); for (int i = 0; *(buffer + i) != '\0'; i++) { Console.Write(*(buffer + i)); } } finally { // 释放内存对象 if (buffer != null) { Marshal.ZeroFreeCoTaskMemUnicode((System.IntPtr)buffer); } } } } }
其运行显示的结果很简单:
这里需要注意的是:为了显示SecureString的内容,程序需要访问非托管内存,因此会用到指针,而要在C#使用指针,则需要使用unsafe关键字(前提是你在项目属性中勾选了允许不安全代码,对你没看错,指针在C#可以使用,但是被认为是不安全的!)。此外,程序中使用了Marshal.SecureStringToCoTaskMemUnicode方法来把安全字符串解密到非托管内存中,最后就是就是我们不要忘记在使用非托管资源时需要确保及时被释放。