用 .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

Post a comment