密碼學03--go語言與非對稱加密RSA演算法的實現
目錄
1.對稱加密的弊端
對稱加密的弊端在於金鑰分發困難,無法保證金鑰的傳送是絕對安全,因此一旦金鑰丟失會導致資料資訊極易被攻破。一般會通過非對稱加密的方式來對對稱加密的金鑰進行分發保護。例如:
在圖中步驟1、2、3都是為了能夠將對稱加密的key以一種相對安全的方式傳輸給Alex的一種手段。因為與Alex公鑰對應的私鑰僅僅在Alex本地才持有,這就意味著Frank處通過Alex公鑰加密的內容其他人即便截獲也無法讀取。這樣就保證了對稱加密key的安全性。
2.非對稱加密
非對稱加密不存在金鑰分發困難的問題,但需要對非對稱加密的公私鑰作出一個簡單的說明:公鑰和私鑰都能夠加密資料,公鑰和私鑰也都能解密資料
2.1 非對稱加密使用場景
資料對誰更重要,誰就持有私鑰。
- (1)通訊過程:A將資料傳輸給B,資訊顯然只允許B讀取(A持有公鑰加密資料,B持有私鑰解密資料)
- (2)登入認證:客戶端需要向伺服器傳送登入請求(伺服器持有公鑰加密登入請求響應,客戶端持有私鑰解密響應資料)
- (3)網銀U盾:銀行向個人發起認證資訊只能允許個人讀取(銀行中持有公鑰加密,個人持有的U盾中持有私鑰解密)
- (4)數字簽名:附在資訊原文後面的表明資訊傳輸過程中是否受到偽造、用來確認的確由資訊擁有者發出的驗證資訊
- (比較特殊,由傳送者持有私鑰,接受者持有公鑰。)
2.2 區分公私鑰
- 直接觀察區別區分:一般情況下,私鑰長度大於公鑰長度。
- 第三方工具生成的:公鑰結構都是xxx.pub帶有.pub的字尾,而私鑰則是xxx這種不帶有後綴的結構。
2.3 非對稱加密通訊流程
具體細節參見1中的圖示內部步驟1、2、3即可。
2.4 非對稱加密與對稱加密
非對稱加密儘管看起來簡潔清晰、安全性高、似乎各方面都完爆對稱加密,但是非對稱加密無法替代對稱加密!因為非對稱加密儘管加密過程簡單,但是非對稱加密也存在很大的侷限性
- 非對稱加密的執行效率要低於對稱加密,同樣數量級的資料加密操作對稱加密速度更快
- 非對稱加密無法對大結構資料進行加密,因為非對稱加密不存在分組加密操作,因此金鑰的長度決定了加密資料長度有限
所以在使用加密的時候都會將對稱加密的key通過非對稱加密先行傳送,畢竟對稱加密的key都不會很大。然後在將本體資料經過對稱加密後傳送。也就是1中圖示的內容。
3.非對稱加密RSA演算法
3.1 RSA演算法
RSA是一種非對稱加密演算法,它的名字是由它的三位開發者,即RonRivest、AdiShamir和LeonardAdleman 的姓氏的首字母組成的,即Rivest-Shamir-Leonard(來威斯特-夏米爾-萊昂納德)。它是目前使用最廣泛的一種非對稱加密演算法,RSA不僅可以被用於非對稱加密還能夠用於生成數字簽名。1983年,RSA公司為RSA演算法在美國取得了專利,但現在該專利於2000年9月21日過期。
再次強調:非對稱加密的金鑰分為加密金鑰和解密金鑰兩部分
3.2 RSA原理
與邏輯原理非常複雜的對稱加密相比較,非對稱加密中的RSA邏輯原理簡單到讓人不可思議,因為RSA的邏輯原理僅僅只用到了簡單的乘方與取模操作。在RSA演算法中明文、密文還有金鑰都是數字,那麼他的邏輯原理就能夠用下面兩個公式來表示:
其中E代表單詞Encryption加密、D代表Decryption解密、N代表Number數字,而{E,N}組合就是公鑰,{D,N}組合就是私鑰。
3.3 RSA生成金鑰對流程
3.3.1 RSA生成私鑰
(1)使用rsa包中的GenerateKey方法生成私鑰物件
/*
引數random是隨機數(就是私鑰本身),bits是金鑰位數
引數一:random不再使用Math中的方法,而是選擇crypto/rand包中的一個io.Reader型別的Reader固有物件。
引數二:最好是1024的整數倍
返回值:返回一個PrivateKey型別的私鑰結構體
type PrivateKey struct {
PublicKey // 公鑰--公鑰是被包含在私鑰中,一起生成的
D *big.Int // 私有的指數
Primes []*big.Int // (暫時忽略)N的素因子,至少有兩個
Precomputed PrecomputedValues // (暫時忽略)包含預先計算好的值,可在某些情況下加速私鑰的操作
}
type PublicKey struct {
N *big.Int // 模
E int // 公開的指數
}
*/
func GenerateKey(random io.Reader, bits int) (priv *PrivateKey, err error)
(2)通過x509標準包將得到的rsa私鑰物件序列化為的ASN.1的DER編碼字串
- x509:是一種本地儲存的證書規範,一般規範要求公鑰存放在數字簽名之中。
- ASN.1抽象語法標記:是一種獨立於計算機架構和語言的描述資料結構的語法標記
- ASN.1的編碼規範有:BER、CER、DER、PER、XER等,其中DER是一種對資料的編碼規範(二進位制編碼)
/*
使用crypto/x509包中的MarshalPKCS1PrivateKey方法,生成一個ASN.1標準的DER編碼字串
而需要的引數就是(1)中生成的私鑰物件*PrivateKey
*/
func MarshalPKCS1PrivateKey(key *rsa.PrivateKey) []byte
(3)構建一個pem.Block資料塊,儲存DER編碼字串
此時作出一個宣告:DER編碼字串中的內容並不是麼一個字元都對使用者是可見的,在ASCII表中有相當一部分字元是沒有顯示方式的因此直接儲存或顯示DER編碼字串會造成不可預知的麻煩
那麼顯然就需要一種辦法將內容並不都可見的DER編碼字串轉換成均可見內容的字串,這時我們找到了pem編碼規範。由於pem包中提供的Encode編碼方法能夠用來對字串序列進行編碼操作,並且會最終將序列化的內容以base64的編碼格式呈現,所以選擇使用pem規範對生成的DER編碼字串進行base64編碼序列化。但是由於pem包中的Encode方法需要一個pemBlock型別的引數,因此需要先構建一個pem.Block資料塊,然後將待序列化的DER編碼字串存入。
/*
Block結構是存在於encoding/pem包中定義的結構體
*/
type Block struct {
Type string //相當於是標題(例如:私鑰就寫”RSA PRIVATE KEY”/公鑰就寫”RSA PUBLICK KEY“)
Headers map[string]string //可選的頭項,省略即可
Bytes []byte //內容解碼後的資料,一般是ASN.1標準的DER編碼字串,即(2)中生成的字串
}
(4)通過encoding/pem包中的包的Encode方法將構建好的pem.Block資料塊base64編碼,並寫入本地檔案中。
/*
第一個引數就是隨便一個檔案指標,file就可以
第二個引數是(3)中生成的pem.Block塊。
*/
func Encode(out io.Writer, b *Block) error
3.3.2 RSA生成公鑰
(1)從生成的私鑰物件中取出公鑰物件,原因參考3.3.1(1)程式碼註釋
publicKey := PrivateKey.PublicKey
(2)通過x509標準包將得到的rsa私鑰物件序列化為的ASN.1的DER編碼字串
/*
注意私鑰的x509方法是PKCS1Private
func MarshalPKCS1PrivateKey(key *rsa.PrivateKey) []byte
而公鑰的x509方法是PKIXPublic
func MarshalPKIXPublicKey(pub interface{}) ([]byte, error)
特別的是公鑰這個方法的返回值並不是*PublicKey,而是一個interface{}
事實上其實是因為x509包內對於*PublicKey有*rsa.PublicK和*ecdsa.PublicKey兩種規格
所以這個位置的引數在傳遞的時候,新增一個&符號即可。具體歸屬哪一種型別x509包方法會自行判斷
*/
func MarshalPKIXPublicKey(pub interface{}) ([]byte, error)
(3)構建一個pem.Block資料塊,儲存DER編碼字串
/*
這玩意和生成私鑰的Block一樣的,都是pem標準的Block資料型別
*/
type Block struct {
Type string //相當於是標題(例如:私鑰就寫”RSA PRIVATE KEY”/公鑰就寫”RSA PUBLICK KEY“)
Headers map[string]string //可選的頭項,省略即可
Bytes []byte //內容解碼後的資料,一般是ASN.1標準的DER編碼字串,即(2)中生成的字串
}
(4)通過encoding/pem包中的Encode方法將構建好的pem.Block資料塊base64編碼,並寫入本地檔案中。
/*
同理,與私鑰生成方法一致
第一個引數就是隨便一個檔案指標,file就可以
第二個引數是(3)中生成的pem.Block塊。
*/
func Encode(out io.Writer, b *Block) error
3.4 RSA生成金鑰對模板
//建立工具函式生成RSA金鑰對,並儲存到本地
func GenerateRSAkey(keySize int){
//------------------------私鑰--------------------------
//1.使用rsa包中的GenerateKey方法來生成私鑰物件
privateKey,err := rsa.GenerateKey(rand.Reader, keySize)
if err!=nil{
panic(err)
}
//2.通過x509標準將得到的rsa私鑰物件序列化為ASN.1標準的DER編碼字串
derPrivateCode := x509.MarshalPKCS1PrivateKey(privateKey)
//3.構建一個pem.Block塊,儲存私鑰DER編碼字串
peoPrivateBlock := pem.Block{Type:"FRANK RSA PRIVATE KEY",Bytes:derPrivateCode}
//4.通過pem的Encode方法將構建好的pem.Block資料塊編碼(base64編碼格式),並儲存至本地
filePrivate,err := os.Create("frankPrivate.pem")
err = pem.Encode(filePrivate,&peoPrivateBlock)
if err!=nil{
panic(err)
}
//5.私鑰寫入檔案關閉
filePrivate.Close()
//------------------------公鑰--------------------------
//1.從得到的私鑰物件中取出公鑰物件取出
publickKey := privateKey.PublicKey
//2.通過x509標準將得到的rsa公鑰物件序列化為ASN.1標準的DER編碼字串
derPublicCode,pubErr := x509.MarshalPKIXPublicKey(&publickKey)
if pubErr!=nil{
panic(pubErr)
}
//3.構建一個pem.Block塊,儲存公鑰DER編碼字串
peoPublicBlock := pem.Block{Type:"FRANK RSA PUBLIC KEY",Bytes:derPublicCode}
//4.通過pem的Encode方法將構建好的pem.Block資料塊編碼(base64編碼格式),並儲存至本地
filePublic,err := os.Create("frankPublic.pem")
err = pem.Encode(filePublic,&peoPublicBlock)
if err!=nil{
panic(err)
}
//5.公鑰寫入檔案關閉
filePublic.Close()
}
3.5 使用RSA金鑰對加密-解密流程
3.5.1 使用RSA公鑰加密流程
此時作出一個宣告:不管是對稱加密金鑰也好還是非對稱加密金鑰、亦或者是RSA演算法也好還是ECDSA橢圓演算法也好,最終使用金鑰並對檔案進行加密操作時金鑰必須是一個已知的明文字串序列!所以我們在3.4中的確將公鑰和私鑰生成並編碼存入了本地檔案,但是如果需要使用他們則必須在使用的時候先將公私鑰檔案反向解密為明文字串序列,然後使用這個明文字串序列對真實內容加密。由此可知如果想要使用RSA金鑰的公鑰對資料加密,則必須先將生成公鑰檔案的操作反向才行。
(1)將公鑰base64編碼字串從本地公鑰檔案中取出
(2)通過encoding/pem包中的Decode方法解碼,將pem構建的base64編碼反向解密,得到pem編碼之前的pem.Block塊
(3)使用x509包中提供的資料解析方法,將pem.Block中Byte欄位內的公鑰物件解析出來
func ParsePKIXPublicKey(derBytes []byte) (pub interface{}, err error)
//不要忘記本方法的返回值是interface{}介面型別,因為存在*rsa.PublicKey和*ecdsa.PublicKey兩種公鑰型別
publickKey := pub.(*rsa.PublicKey)
//使用斷言,將公鑰型別確定為RSA型別
(4)核心加密程式碼:使用rsa包中提供的加密方法,藉助得到的公鑰來對資訊加密
/*
第一個引數就是rand.Reader那個密碼隨機數生成器
第二個引數就是解密公鑰檔案得到的【公鑰明文字體】
第三個引數就是需要加密的明文資訊
當然返回的就是密文資訊
*/
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) (out []byte, err error)
3.5.2 使用RSA私鑰解密流程
顯然使用RSA公鑰去加密資料的流程和使用RSA私鑰去解密資料的流程及戶完全一樣。
(1)將私鑰base64編碼字串從本地私鑰檔案中取出
(2)通過encoding/pem包中的Decode方法解碼,將pem構建的base64編碼反向解密,得到pem編碼之前的pem.Block塊
(3)使用x509包中提供的資料解析方法,將pem.Block中Byte欄位內的私鑰物件解析出來
func ParsePKCS1PrivateKey(der []byte) (key *rsa.PrivateKey, err error)
//私鑰的解密方法返回值就是rsa.PrivateKey型別,不再需要斷言。
(4)核心解密程式碼:使用rsa包中提供的解密方法,藉助得到的私鑰來對資訊解密
/*
第一個引數就是rand.Reader那個密碼隨機數生成器
第二個引數就是解密私鑰檔案得到的【公鑰明文字體】
第三個引數就是需要解密的密文資訊
當然返回的就是解密後的明文資訊
*/
func DecryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (out []byte, err error)
3.6 使用RSA金鑰對加密-解密模板
/*
使用RSA公鑰加密工具函式
引數一:就是需要加密的明文內容
引數二:就是公鑰檔名
返回值自然就是RSA公鑰檔案加密後的加密字串
*/
func RSAUsePublicKeyEncrypt(plainText []byte, publickeyFileName string) []byte{
//1.將公鑰檔案當中的公鑰base64編碼讀出存入緩衝區
publicFile,err := os.Open(publickeyFileName)
if err!=nil{
panic(err)
}
defer publicFile.Close()
//獲取檔案長度,檔案多長就建立多長緩衝區
fileInfo,err := publicFile.Stat()
if err!=nil{
panic(err)
}
buffer := make([]byte, fileInfo.Size())
publicFile.Read(buffer)
//2.pem解碼,將公鑰base64解碼為一個存有公鑰DER字串的pem.Block資料塊
block,_ := pem.Decode(buffer)
//3.使用x509包提供的公鑰解析方法,對公鑰DER字串解析
pubInterface,err := x509.ParsePKIXPublicKey(block.Bytes)
if err!=nil{
panic(err)
}
publickKey := pubInterface.(*rsa.PublicKey)//將公鑰斷言為rsa型別
//4.使用rsa包中的公鑰加密方法加密資料
cipher,err := rsa.EncryptPKCS1v15(rand.Reader, publickKey, plainText)
if err!=nil{
panic(err)
}
return cipher
}
/*
使用RSA私鑰解密的工具函式
引數一:就是需要解密的密文內容
引數二:就是私鑰檔名
返回值自然就是RSA私鑰檔案解密後的原本明文字串
*/
func RSAUsePrivateKeyDecrypt(cipherText []byte, privateKeyFileName string)[]byte{
//1.將私鑰檔案當中的私鑰base64編碼讀出存入緩衝區
privateFile,err := os.Open(privateKeyFileName)
if err!=nil{
panic(err)
}
defer privateFile.Close()
//仍然是檔案多大建立多大緩衝區
fileInfo,err := privateFile.Stat()
if err!=nil{
panic(err)
}
buffer := make([]byte, fileInfo.Size())
privateFile.Read(buffer)
//2.pem解碼,將私鑰base64編碼解碼成一個存有私鑰DER字串的pem.Block資料塊
block,_ := pem.Decode(buffer)
//3.使用x509包提供的私鑰解析方法,對私鑰DER字串解析
//注意本方法返回的就直接是res.PrivateKey型別物件,不用斷言
privateKey,err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err!=nil{
panic(err)
}
//4.使用rsa包中提供的私鑰解密方法解密資料
plainText,err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, cipherText)
if err!=nil{
panic(err)
}
return plainText
}