C# RSA 非對(duì)稱加密實(shí)踐

一、前提

背景: 項(xiàng)目的登錄頁(yè)面最初設(shè)計(jì)為使用前端 MD5 加密用戶密碼。后端服務(wù)通過(guò)匹配數(shù)據(jù)庫(kù)中存儲(chǔ)的 MD5 加密字符串來(lái)驗(yàn)證密碼的正確性。

加固前 登錄接口表單.png

安全漏洞: 安全審計(jì)報(bào)告指出,攻擊者能夠在局域網(wǎng)內(nèi)嗅探網(wǎng)絡(luò)流量,并捕獲請(qǐng)求數(shù)據(jù)包。由于 MD5 加密算法的安全性不足,攻擊者可以輕松解密并還原出明文密碼。

密碼 md5 加密后被還原.png

加固建議: 報(bào)告建議采用更安全的傳輸加密措施,例如使用 HTTPS 或升級(jí)至更安全的加密算法。建議的算法包括不可逆的 Hash 算法結(jié)合鹽值、安全對(duì)稱加密算法或非對(duì)稱加密算法。

實(shí)施方案: 為了保證用戶體驗(yàn),決定保留現(xiàn)有的前端 MD5 加密邏輯。在此基礎(chǔ)上,前端將對(duì) MD5 加密后的密碼字符串進(jìn)行一次 RSA 加密,然后傳輸至后端。后端在驗(yàn)證密碼前,將先對(duì) RSA 加密的字符串進(jìn)行解密。

加固后 登錄接口表單.png

二、概念

非對(duì)稱加密算法需要兩個(gè)密鑰:公開(kāi)密鑰(publickey)和私有密鑰(privatekey)。公開(kāi)密鑰與私有密鑰是一對(duì),如果用公開(kāi)密鑰對(duì)數(shù)據(jù)進(jìn)行加密,只有用對(duì)應(yīng)的私有密鑰才能解密;如果用私有密鑰對(duì)數(shù)據(jù)進(jìn)行加密,那么只有用對(duì)應(yīng)的公開(kāi)密鑰才能解密。

通過(guò)分析,當(dāng)前需求場(chǎng)景為發(fā)送者用接收者的公鑰加密,接受者用自己的私鑰解密。具體原理圖如下:


非對(duì)稱加密原理圖.png

三、實(shí)踐

使用到的工具類和第三方庫(kù)

具體操作步驟

  1. 后端生成密鑰:
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048))//2048 是指定密鑰加密位數(shù)
{
    // 公鑰 
    string pubkey = rsa.ToXmlString(false);
    // 私鑰 
    string prikey = rsa.ToXmlString(true);
}

注意:\color{red}{生成的密鑰是 xml 字符串!}
關(guān)于安全保留私鑰,微軟官方的建議是使用安全密鑰容器。為了方便存儲(chǔ)和讀取,我偷懶把私鑰和公鑰都保存在了配置文件里。

  1. 前端接收公鑰
    前端新增一個(gè)獲取公鑰的接口,加載頁(yè)面時(shí)自動(dòng)獲取 RAS 公鑰。
    注意:由于前端\color{red}{jsencrypt 庫(kù)支持的密鑰是 Pkcs8 格式},接口輸出的公鑰需先在后端將格式 xml 轉(zhuǎn)換為 Pkcs8 格式。進(jìn)行數(shù)據(jù)格式轉(zhuǎn)換后端需安裝 NuGet 包 XC.RSAUtil 。
    NuGet 程序包上的 XC.RSAUtil.png
//公鑰XML格式轉(zhuǎn)Pkcs8格式
RsaKeyConvert.PublicKeyXmlToPem(xmlPublicKeyString);
  1. 前端通過(guò)公鑰加密
    前端頁(yè)面引入 jsencrypt.js 庫(kù),大家可以根據(jù)需要自行選擇安裝方式,我這里用的是比較簡(jiǎn)單的文件引入。
<script src="~/Content/js/jsencrypt.min.js"></script>

在登錄接口中,使用在步驟 2 獲取到的公鑰,對(duì)登錄參數(shù)進(jìn)行一次 RSA 加密后,再傳輸?shù)胶蠖恕?/p>

// publickey 公鑰
// username 賬號(hào)
// password 密碼 md5 加密后字符串
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publickey);
var encryptedUsername = encrypt.encrypt(username);
var encryptedPassword = encrypt.encrypt(password);
  1. 后端通過(guò)私鑰解密
    在后端登錄驗(yàn)證方法中,讀取在步驟 1 時(shí)保存于配置文件的私鑰,先對(duì)登錄參數(shù)進(jìn)行一次 RSA 解密成明文,再驗(yàn)證登錄信息。
using (var rsa = new RSACryptoServiceProvider(2048))// 2048 是指定密鑰加密位數(shù)
{
    try
    {
        rsa.FromXmlString(privateKey);// 導(dǎo)入私鑰
        var encrypted = Convert.FromBase64String(encryptedText);// encryptedText 加密的密文
        var bytes = rsa.Decrypt(encrypted, false);
        return Encoding.UTF8.GetString(bytes);
    }
    finally
    {
        rsa.PersistKeyInCsp = false;
    }
}

輔助類

為方便使用,以上后端代碼統(tǒng)一整理成了一個(gè)輔助類 RSAHelper.cs ,使用時(shí)注意修改命名空間

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using XC.RSAUtil;

namespace xxxxxxx.Util
{
    /// <summary>
    /// 描 述:RSA 非對(duì)稱加密和解密輔助類
    /// </summary>
    public class RSAHelper
    {
        /// <summary>
        ///  RSA 解密文本
        /// </summary>
        /// <param name="encryptedText">加密的密文</param>
        /// <param name="privateKey">私鑰</param>
        /// <returns>未加密數(shù)據(jù)的字符串</returns>
        public static string RSADecrypt(string encryptedText, string privateKey)
        {
            using (var rsa = new RSACryptoServiceProvider(2048))//2048是加密位數(shù)
            {
                try
                {
                    rsa.FromXmlString(privateKey);
                    var encrypted = Convert.FromBase64String(encryptedText);
                    var bytes = rsa.Decrypt(encrypted, false);
                    return Encoding.UTF8.GetString(bytes);
                }
                finally
                {
                    rsa.PersistKeyInCsp = false;
                }
            }
        }

        /// <summary>
        /// RSA私鑰生成 (XML字符串)
        /// </summary>
        public static ValueTuple<string, string> GenerateKey()
        {
            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048))//2048是加密位數(shù)
            {
                // 公鑰 
                string pubkey = rsa.ToXmlString(false);
                // 私鑰 
                string prikey = rsa.ToXmlString(true);
                return new ValueTuple<string, string>(pubkey, prikey);
            }
        }

        /// <summary>
        /// 公鑰XML格式轉(zhuǎn)Pkcs8格式
        /// </summary>
        /// <param name="xmlPublicKeyString">xml格式公鑰字符串</param>
        /// <returns>Pkcs8格式公鑰字符串</returns>
        public static string PublicKeyXmlToPem(string xmlPublicKeyString)
        {
            return RsaKeyConvert.PublicKeyXmlToPem(xmlPublicKeyString);
        }

    }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容