.NET内提供了许多加解密安全功能,但略显生硬,主要表示为字节数组块操作,而日常开发使用,常常需要对字符串或数据流进行加解密。因此,SecurityHelper只是薄薄的包了一层。

主要安全扩展方法,已经集成到码神工具中,可以直接使用  http://x.newlifex.com/CrazyCoder_v8.0.2022.0923.zip 

Nuget包:NewLife.Core

源码:https://github.com/NewLifeX/X/blob/master/NewLife.Core/Security/SecurityHelper.cs

视频:https://www.bilibili.com/video/BV1gq4y1g78w

视频:https://www.bilibili.com/video/BV12N4y1A7Xf


最佳实践

最常用的AES加解密

var data = "新生命团队".GetBytes();
var pass = "NewLife".GetBytes();
var aes = Aes.Create();
var rs = aes.Encrypt(data, pass).ToBase64();
var txt = aes.Decrypt(rs.ToBase64(), pass).ToStr();


随机数扩展

很多同学会说,随机数不就是Random类嘛,有啥好扩展的?

哈哈哈!那么你可以等到发现随机数不再随机的时候,再来看本文。


实际上,Random类的随机性的确不够,于是又搞了个强随机RNGCryptoServiceProvider。这里的随机扩展类Rand正是基于它。显然,这个类的代码性能要求极高,测量标准为纳秒级。


Int32 Next(Int32 min, Int32 max)

随机整数,可取最小值min,不取最大值max。如果需要Int16或者Int64,按照极简设计原则,自个想办法转一下O(∩_∩)O

调用平均耗时37.76ns,其中GC耗时77.56%

Byte[] NextBytes(Int32 count)

返回指定长度随机字节数组。生成随机令牌特有用。

调用平均耗时5.46ns,其中GC耗时15%

String NextString(Int32 length, Boolean symbol = false)

返回指定长度随机字符串。生成密码挺好,第二个参数还可以加入特殊字符。


哈希扩展

哈希扩展集中在SecurityHelper类,以扩展方法提供。

衡量哈希算法质量的主要指标是离散性,也就是哈希一批数据,出现相同哈希值的几率。此外还应该关注计算速度以及结果长度。

MD5

对于web开发来说,最熟悉的莫过于MD5,特别是ASP/PHP时代。后来我国王小云教授发明了彩虹碰撞,很容易找到一个明文,让其MD5得到目标值。同时也表明其离散性比较差。各大网站纷纷“加固”,常用手段为加盐(加上另一个字符串后再取MD5),或者多次MD5。

Byte[] MD5(this Byte[] data);
String MD5(this String data, Encoding encoding = null);
String MD5_16(this String data, Encoding encoding = null);

MD5哈希结果是16字节,一边用32字符的HEX编码字符串表示。有些场合取前8字节。

SHA

近些年主流应用逐步使用SHA256,早期的SHA1已经公认为不安全。

SHA哈希有个key参数,有时候也叫密码,本质上类似于MD5加盐,相同原文,不同key得到的散列值不同。

SHA256的结果是32字节,尽管SHA384/SHA512安全性更高,但是计算速度也大大下降。当今主流SSL证书就是使用SHA256哈希算法。

常用哈希算法中,SHA离散性最强!(那是当然,也不看看它的结果用了多少个字节)

Byte[] SHA1(this Byte[] data, Byte[] key);
Byte[] SHA256(this Byte[] data, Byte[] key);
Byte[] SHA384(this Byte[] data, Byte[] key);
Byte[] SHA512(this Byte[] data, Byte[] key);

CRC

在通信协议以及zip等文件协议中,常常可以见到CRC哈希算法,得以于其极其简单而又具有很不错的离散性。这里的“很不错的离散性”,只是相对于其简单而言。实际上,CRC可能是离散性最差的哈希算法,毕竟它只有4个字节来表示结果。

CRC常见于嵌入式应用,它最大的坑在于多项式!尽管都是Crc32/Crc16,多项式不同,得到结果也不同。

// Crc32多项式 x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1
UInt32 Crc(this Byte[] data);

// CRC16-CCITT x16+x12+x5+1 1021  ISO HDLC, ITU X.25, V.34/V.41/V.42, PPP-FCS
UInt16 Crc16(this Byte[] data);

Crc16最常用于大数据分表分库,哈希后对表个数取模即可得知数据放在哪张表上。


Murmur128

该哈希算法很少见,常用于作为字典集合或者布隆过滤器的哈希算法。

Byte[] Murmur128(this Byte[] data, UInt32 seed = 0);

在布隆过滤器BloomFilter中,可以用极少内存实现海量数据的存在性判断,碰撞几率很小。

BloomFilter本质上是把目标数据经Murmur128哈希后得到16字节,标记在一定长度的位数组上,判断目标数据是否存在时,只需要再次哈希,取数组的目标位置查找,只要每个标记为都存在,则认为该数据已存在。

一般建议位数组长度取预估数据总量的32倍,此时误判率约为0.004%。

例如10亿数据时,约等于2的35次方,位数组长度取35,误判率为10亿*0.004%=4万,一共10亿数据才4万可能误判。


对称加密

对称加密的常用算法有DES/AES,还有最新的SM4。

对称加密最常见的问题就是C#/Java/C++加解密能否通用。答案显然是可以的。

DES/AES都是块加密,也就是凑够一块再加密(8/16),最后多出来部分,就需要以一定方法凑够一块,这就是填充算法PaddingMode。默认PKCS7,等同Java的PKCS5。

前后两个块之间是否有关系,由块密码模式CipherMode决定。.Net默认CBC,Java默认ECB。

很多人问,C#加密而Java不能解密,或者Java加密而C#不能解密,主要问题就在于两者默认的填充算法和块密码模式不同。

SymmetricAlgorithm Encrypt(this SymmetricAlgorithm sa, Stream instream, Stream outstream);
Byte[] Encrypt(this SymmetricAlgorithm sa, Byte[] data, Byte[] pass = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7);

SymmetricAlgorithm Decrypt(this SymmetricAlgorithm sa, Stream instream, Stream outstream);
Byte[] Decrypt(this SymmetricAlgorithm sa, Byte[] data, Byte[] pass = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7);


如果纠结于C/C++、Delphi等更多语言的加解密兼容,可以采用RC4。尽管它已经过时,但是它算法极其简单,在各种语言平台上非常容易实现,总代码量不到50行。并且,RC4是流式加密,密码长度不限,也没有DES/AES块填充的问题。

安全专家认为,当RC4的密钥设定为128位甚至更大时,至今还没有找到有效的攻击方法。

Byte[] RC4(this Byte[] data, Byte[] pass);


非对称加密

非对称加密的代表作是RSA,基于大素数乘法。

RSA使用公钥加密,只能使用私钥解密,仅用公钥也不能解密。因此你需要保管好私钥,把公钥交给别人,当别人给你发送数据时,使用公钥加密,最后只有你的私钥能够解密。

注意,在.NET体系中,RSA封装的私钥包含公钥,而C++安全库的RSA封装,一般两个密钥互不包含。


当今RSA密钥主要使用2048位,1024位已被认为不安全,而512位RSA密钥,曾在2007年用AMD电脑跑了两个小时爆破。4096位RSA密钥计算速度很慢,特别是配置不高的电脑。


我们的RSA扩展位于RSAHelper类。

String[] GenerateKey(Int32 keySize = 2048);
Byte[] Encrypt(Byte[] buf, String pubKey, Boolean fOAEP = true);
Byte[] Decrypt(Byte[] buf, String priKey, Boolean fOAEP = true);

这是常用方法,生成密钥对,以及加解密少量数据。


实际上,需要加密大量数据时,往往是生成随机密码,然后AES/DES加密原始数据,而RSA加密那个随机密码。这里涉及加密算法、哈希算法、密钥交互等一系列工作,SSL就是一个典型案例。


数字签名

我们一般采用DSA做数据签名,椭圆曲线算法,封装于DSAHelper类中。RSA也可以做签名,具体看使用场景和个人喜好。

DSA使用私钥对数据进行签名,只需要公钥即可验证该签名是否合法,别人就知道这份数据是否真的来自于你的“真迹”。在.NET体系中私钥也可以验证签名,因为它包含了公钥,但是没有意义,私钥绝对不能泄露。

String[] GenerateKey(Int32 keySize = 1024);
Byte[] Sign(Byte[] buf, String priKey);
Boolean Verify(Byte[] buf, String pukKey, Byte[] rgbSignature);

这是常用方法,生成密钥对,数据签名以及验证签名。


实际上,因为非对称加密很慢,数字签名不适合签名大量数据。于是,常见使用SHA256/SHA512对原始数据(如一个文件)进行哈希散列,得到哈希值,再对其进行数字签名。

例如:s = $sign($hash(data), priKey); $verify($hash(data), pubKey, s)


数字签名的典型用途由jwt令牌。