用 .NET 加解密已是老生常談,.NET 內建 MD5、SHA1、RSA、AES、DES... 等雜湊及加密演算法,寫來易如反掌,網路上的文章也很多。但沒有自己整理過一次,每回要用都要爬文找半天。有些基本功不能省就是不能省,所以,我的 RSA 私房筆記來了。
程式範例 1 包含:產生隨機 RSA 金鑰、匯出公私鑰、對一小段文字加密、產生數位簽章。第二階段則包含匯入私鑰、解密加密內容、驗證數位簽章,並試著偷改內容驗證簽章是否因此失效。
staticvoid RSAEncDec()
{
//建立RSA公私鑰
var rsaEnc = new RSACryptoServiceProvider();
//Key長度384-16384, Win8.1+最小512
//預設1024,可new RSACryptoServiceProvider(2048)指定不同大小
Console.WriteLine($"KeySize={rsaEnc.KeySize}");
//匯出公鑰(用於解密,檢驗簽章),XML格式
var pubKey = rsaEnc.ToXmlString(false);
Console.WriteLine($"PubKey={pubKey}");
//匯出公私鑰
var rsaKeys = rsaEnc.ToXmlString(true);
Console.WriteLine($"RSAKeys={rsaKeys}");
//加密小段文字(用公鑰)
var rawText = ".NET Rocks!";
var rawData = Encoding.UTF8.GetBytes(rawText);
//第二個參數指定是否使用OAEP提高安全性
var encData = rsaEnc.Encrypt(rawData, true);
//產生數位簽章
var stream = new MemoryStream(rawData);
var signature = rsaEnc.SignData(stream,
new SHA1CryptoServiceProvider());
//** 解密 ** 需要公私鑰
var rsaDec = new RSACryptoServiceProvider();
//從XML還原公私鑰
rsaDec.FromXmlString(rsaKeys);
//使用私鑰解密
var decData = rsaDec.Decrypt(encData, true);
var test = Encoding.UTF8.GetString(decData);
Console.WriteLine($"解密結果: {test}");
//** 驗章 ** 只需公鑰
var rsa4Sign = new RSACryptoServiceProvider();
rsa4Sign.FromXmlString(pubKey);
//檢驗數位簽章
var valid = rsa4Sign.VerifyData(decData,
new SHA1CryptoServiceProvider(), signature);
Console.WriteLine($"數位簽章: {(valid?"PASS":"FAILED")}");
//測試修改一個Byte讓簽章無效
decData[0]++;
valid = rsa4Sign.VerifyData(decData,
new SHA1CryptoServiceProvider(), signature);
Console.WriteLine($"篡改版數位簽章: {(valid ? "PASS" : "FAILED")}");
}
執行結果:
KeySize=1024
PubKey=<RSAKeyValue><Modulus>q+SlvTWJ...+4BW7j0=</Modulus>
<Exponent>AQAB</Exponent></RSAKeyValue>
RSAKeys=<RSAKeyValue><Modulus>q+SlvTWJ...+4BW7j0=</Modulus>
<Exponent>AQAB</Exponent><P>ztDGDTc...d3ST3ow==</P>
<Q>1MW/8rq...ZrA3+Kxgnw==</Q>
<DP>cE4mfh6WruasI...IKsn/UiQ==</DP>
<DQ>dY81OPWZH...qtGoZ0MXQ==</DQ>
<InverseQ>cPTkfCrpSy...XmOH5qiu982pw==</InverseQ>
<D>IOtWDmld...+V3VeU=</D></RSAKeyValue>
解密結果: .NET Rocks!
數位簽章: PASS
篡改版數位簽章: FAILED
RSA 加解密只適用小段資料內容,資料長度不能超過其金鑰長度減去 Header、Padding 長度,以 2048 位元 RSA 只能加密 256 - 11 = 245 Bytes(參考: RFC2313 The length of the data D shall not be more than k-11 octets, which is positive since the length k of the modulus is at least 12 octets.) 實務上加密大量內容還是得靠對稱式加密(例如: DES、3DES、AES),RSA 則用來加密對稱式加密的金鑰。
程式範例 2 展示使用 RSA + AES 聯手處理 488MB 的 zip 檔的加解密以及數位簽章:
staticvoid RsaEncDecFile()
{
//建立RSA公私鑰
var rsaEnc = new RSACryptoServiceProvider(2048);
//匯出金鑰
var pubKey = rsaEnc.ToXmlString(false);
var rsaKeys = rsaEnc.ToXmlString(true);
//建立AES Managed時產生隨機Key及IV,不用另行指定
var aes = new AesManaged();
var encAesKeyIV = aes.Key.Concat(aes.IV).ToArray();
var aesKeyEncrypted = rsaEnc.Encrypt(encAesKeyIV, true);
byte[] signature;
Stopwatch sw = new Stopwatch();
sw.Start();
//準備加密Stream
using (var encFile =
new FileStream("D:\\Encrypted.bin", FileMode.Create))
{
using (var outStream = new
CryptoStream(
encFile, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
//讀取約500MB檔案寫入加密Stream
using (var fs = new FileStream("D:\\Source.zip",
FileMode.Open))
{
//REF: Buffer Size 64K CPU clock 較少
//https://goo.gl/UAuPyt
var buff = newbyte[65536];
int bytesRead = 0;
while ((bytesRead = fs.Read(buff, 0, buff.Length)) > 0)
{
outStream.Write(buff, 0, bytesRead);
}
}
}
}
sw.Stop();
Console.WriteLine($"加密耗時: {sw.ElapsedMilliseconds}ms");
byte[] srcHash = SHA1.Create().ComputeHash(
new FileStream("D:\\Source.zip", FileMode.Open));
signature = rsaEnc.SignHash(srcHash, CryptoConfig.MapNameToOID("SHA1"));
var rsaDec = new RSACryptoServiceProvider();
//從XML還原公私鑰
rsaDec.FromXmlString(rsaKeys);
//解密出AES Key
var aesKeyIV = rsaDec.Decrypt(aesKeyEncrypted, true);
aes = new AesManaged()
{
KeySize = 256,
Key = aesKeyIV.Take(32).ToArray(),
IV = aesKeyIV.Skip(32).Take(16).ToArray(),
BlockSize = 128
};
sw.Restart();
//準備解密Stream
using (var decFile = new FileStream("D:\\Decrypted.zip", FileMode.Create))
{
using (var encFile = new FileStream("D:\\Encrypted.bin", FileMode.Open))
{
using (var decStream = new CryptoStream(encFile,
aes.CreateDecryptor(), CryptoStreamMode.Read))
{
var buff = newbyte[65536];
int bytesRead = 0;
while ((bytesRead = decStream.Read(buff, 0, buff.Length)) > 0)
{
decFile.Write(buff, 0, bytesRead);
}
}
}
}
sw.Stop();
Console.WriteLine($"解密耗時: {sw.ElapsedMilliseconds}ms");
//印出解密檔案Hash與原始檔比對是否相同
byte[] decHash = SHA1.Create().ComputeHash(
new FileStream("D:\\Decrypted.zip", FileMode.Open));
Console.WriteLine($"Source SHA1={BitConverter.ToString(srcHash)}");
Console.WriteLine($"Decrypted SHA1={BitConverter.ToString(decHash)}");
//檢驗數位簽章
var valid =
rsaDec.VerifyHash(decHash, CryptoConfig.MapNameToOID("SHA1"), signature);
Console.WriteLine($"數位簽章: {(valid ? "PASS" : "FAILED")}");
}
實測 AES 加密 488MB 檔案需 12.3 秒,解密需 13.5 秒,速度蠻快的。
加密耗時: 12251ms
解密耗時: 13522ms
Source SHA1=34-F4-75-C1-81-7E-F4-25-4F-97-28-43-0C-7A-9D-5F-10-BD-2F-11
Decrypted SHA1=34-F4-75-C1-81-7E-F4-25-4F-97-28-43-0C-7A-9D-5F-10-BD-2F-11
數位簽章: PASS
另外,實務上不建議讓金鑰以文字檔形式曝露在外,多半會用金鑰容器保存 RSA 金鑰,詳情可參考官方文件,以下是簡單筆記:
staticvoid RsaKeyContainer()
{
//在個人RSA容器區建立金鑰容器並存入RSA金鑰
//一個KeyContainerName對應一把金鑰
var csp1 = new CspParameters();
csp1.KeyContainerName = "RSALab";
var rsa1 = new RSACryptoServiceProvider(csp1);
//如果要從外部匯入金鑰,先建立RSA再FromXmlString()
var csp2 = new CspParameters()
{
KeyContainerName = "RSALab"
};
var rsa2 = new RSACryptoServiceProvider(csp2);
rsa2.PersistKeyInCsp = true;
rsa2.FromXmlString("...");
//若同名金鑰容器已存在,自動取回上次存入的金鑰
var csp3 = new CspParameters()
{
KeyContainerName = "RSALab"
};
var rsa3 = new RSACryptoServiceProvider(csp3);
//要刪除金鑰,先取消PersistKeyInCsp再Clear()
//金鑰容器也會一併被刪除
var csp4 = new CspParameters()
{
KeyContainerName = "RSALab"
};
var rsa4 = new RSACryptoServiceProvider(csp4);
rsa4.PersistKeyInCsp = false;
rsa4.Clear();
}