RSA 非對稱金鑰加解密與數位簽章筆記
| | | 0 | |
用 .NET 加解密已是老生常談,.NET 內建 MD5、SHA1、RSA、AES、DES... 等雜湊及加密演算法,寫來易如反掌,網路上的文章也很多。但沒有自己整理過一次,每回要用都要爬文找半天。有些基本功不能省就是不能省,所以,我的 RSA 私房筆記來了。
程式範例 1 包含:產生隨機 RSA 金鑰、匯出公私鑰、對一小段文字加密、產生數位簽章。第二階段則包含匯入私鑰、解密加密內容、驗證數位簽章,並試著偷改內容驗證簽章是否因此失效。
static void 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 檔的加解密以及數位簽章:
static void 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 = new byte[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 = new byte[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 金鑰,詳情可參考官方文件,以下是簡單筆記:
static void RsaKeyContainer()
{ //在個人RSA容器區建立金鑰容器並存入RSA金鑰 //一個KeyContainerName對應一把金鑰 var csp1 = new CspParameters(); csp1.KeyContainerName = "RSALab"; var rsa1 = new RSACryptoServiceProvider(csp1); //註: 金鑰容器被設計用於保存本機產生的金鑰,不適用保存外部匯入金鑰 //如要匯入公開金鑰加密及驗章,請參考上面FromXmlString()範例 //補充參考 https://stackoverflow.com/a/2287561/288936 //若同名金鑰容器已存在,自動取回上次存入的金鑰 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();
}
Comments
Be the first to post a comment