一、非對稱加密
在對稱密碼中,由於加密和解密都需要使用相同祕鑰。假如向接收者配送密鑰過程中發生祕鑰泄露,就可能導致泄的情況出現。而非對稱加密的出現可以很好解決該問題。
非對稱加密的密鑰分爲加密密鑰和解密密鑰兩種。發送者用加密密鑰對消息進行加密,接收者用解密密鑰對密文進行解密。解密密鑰從一開始就是由接收者自己保管的,這樣就解決了上面祕鑰配送過程中可能遇到的泄密問題。
非對稱加密中,加密密鑰一般是公開的,任意用戶都可以獲得。正是由於加密密鑰是公開的,因此加密祕鑰也被稱爲公鑰(PublicKey)。相對地,解密祕鑰是絕對不能夠公開的,由用戶自己負責保管,因此稱爲私鑰(PrivateKey)。公鑰和私鑰統稱爲密鑰對(keypair)。
1.1 非對稱加密通訊流程
假設Alice要給Bob發送一條消息。由於消息的重要性,需要對消息進行加密處理。在非對稱加密通訊中,通訊過程是由消息接收方Bob負責發起的。
- Bob生成一個包含公鑰和私鑰的祕鑰對;
- Bob將公鑰發送給Alice,私鑰自己保管;
- Alice接收到Bob發送過來的公鑰後,使用該公鑰對發送消息進行加密處理後,再把加密數據發送給Bob;
- Bob接收到Alice發送過來的加密數據後,使用私鑰進行解密,得到原文;
如果在通訊過程中Tracker竊取了數據,但是隻要Bob的祕鑰數據沒有泄漏, Tracker就無法完成解密操作,數據還是安全的。
下面介紹兩種常見的非對稱加密算法:RSA和橢圓曲線。
1.2 RSA
RSA加密算法是一種非對稱加密算法,在公開密鑰加密和電子商業中RSA被廣泛使用。RSA是1977年由RonRivest、AdiShamir和LeonardAdleman 一起提出的。RSA就是它們三人的首字母所組成的。
1.2.1 RSA加密原理
RSA的加密過程可以用以下公式來表達:
也就是說,RSA的密文是對代表明文的數字執行E次方運算後,將運算結果對N取模得到的,這個餘數就是密文。因此,E和N就是RSA加密的密鑰,E和N的組合就是公鑰。一般把公鑰簡寫成 “(E,N)” 或者 “{E, N}"的形式。
簡單來說,只要知道E和N就可以完成加密運算。但是,E和N並不是隨便什麼數都可以的,它們必須經過嚴密的計算得到的。
1.2.2 RSA解密原理
RSA的解密過程可以用以下公式來表達:
也就是說,對代表密文數字執行D次方運算後,將運算結果對N取模,這個餘數就是明文。因此,D和N就是RSA解密的密鑰,D和N的組合就是私鑰。只有知道D和N兩個數的人才能夠完成解密的運算。
值得注意的是,解密公式的數字N和加密公式的數字N是相同的。
簡單來說,只要知道D和N就可以完成解密運算。當然,D也並不是隨便什麼數都可以的,作爲解密密鑰的D,和數字E有着相當緊密的聯繫。
1.2.3 如何生成祕鑰對
- 生成私鑰的操作步驟:
第一步:使用rsa中的GenerateKey方法,該方法使用隨機數據生成器random生成一對具有指定字位數的RSA密鑰;
func GenerateKey(random io.Reader, bits int) (priv *PrivateKey, err error)
第二步:通過x509標準將得到的ras私鑰序列化爲ASN.1的DER編碼字符串;
func MarshalPKCS1PrivateKey(key *rsa.PrivateKey) []byte
第三步:將私鑰字符串設置到pem格式塊中;
type Block struct {
Type string // 類型,私鑰爲"RSA PRIVATE KEY",公鑰爲“RSA PUBLIC KEY”
Headers map[string]string // 可選的頭信息
Bytes []byte // 解碼後的數據
}
第四步:通過pem將設置好的數據進行編碼, 並寫入磁盤文件中;
func Encode(out io.Writer, b *Block) error
- 生成公鑰的操作步驟:
第一步:從得到的私鑰對象中將公鑰信息取出;
publicKey := privateKey.PublicKey // privateKey爲私鑰對象
第二步:通過x509標準將得到 的rsa公鑰序列化爲字符串;
func MarshalPKIXPublicKey(pub interface{}) ([]byte, error)
第三步:將公鑰字符串設置到pem格式塊中;
第四步:通過pem將設置好的數據進行編碼, 並寫入磁盤文件;
代碼實現:
func generateKey() {
//============== 生成私鑰 =============
// 使用rsa中的GenerateKey方法生成私鑰
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
// 通過x509標準將得到的ras私鑰序列化爲ASN.1 的 DER編碼字符串
derText := x509.MarshalPKCS1PrivateKey(privateKey)
// 將私鑰字符串設置到pem格式塊中
block := pem.Block{
Type: "rsa private key",
Bytes: derText,
}
// 創建文件,用於保存私鑰數據
file, err := os.Create("private.pem")
if err != nil {
panic(err)
}
// 釋放資源
defer file.Close()
// 通過pem將設置好的數據進行編碼, 並寫入磁盤文件中
pem.Encode(file, &block)
//============== 生成公鑰 =============
// 從得到的私鑰對象中將公鑰信息取出
publicKey := privateKey.PublicKey
// 通過x509標準將得到 的rsa公鑰序列化爲字符串
derText, err = x509.MarshalPKIXPublicKey(&publicKey)
if err != nil {
panic(err)
}
// 將公鑰字符串設置到pem格式塊中
block = pem.Block {
Type: "rsa public key",
Bytes: derText,
}
// 創建文件,用於存儲公鑰數據
file, err = os.Create("public.pem")
if err != nil {
panic(err)
}
// 釋放資源
defer file.Close()
// 通過pem將設置好的數據進行編碼, 並寫入磁盤文件
pem.Encode(file, &block)
}
1.2.4 如何使用RSA
- 使用公鑰加密的操作步驟:
第一步:將公鑰文件中的公鑰讀出, 得到使用pem編碼的字符串;
第二步:將得到的字符串進行解碼操作;
第三步:使用x509將編碼之後的公鑰解析出來;
第四步:使用得到的公鑰通過rsa進行數據加密;
- 使用私鑰解密的操作步驟:
第一步:將私鑰文件中的私鑰讀出, 得到使用pem編碼的字符串;
第二步:將得到的字符串進行解碼操作;
第三步:使用x509將編碼之後的私鑰解析出來;
第四步:使用得到的私鑰通過rsa進行數據解密;
代碼實現:
// rsa加密
func RsaEncrypt(publicKeyFile string, plainText string) []byte {
// 將公鑰文件中的公鑰讀出, 得到使用pem編碼的字符串
file, err := os.Open(publicKeyFile)
if err != nil {
panic(err)
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
panic(err)
}
buf := make([]byte, fileInfo.Size())
file.Read(buf)
// 使用pem.Decode將得到的字符串解碼
block, _ := pem.Decode(buf)
// 使用x509將解碼後的公鑰解析出來
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}
publicKey, ok := pubInterface.(*rsa.PublicKey)
if !ok {
panic("publicKey is not rsa.PublicKey.")
}
// 使用公鑰進行數據加密
cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(plainText))
if err != nil {
panic(err)
}
return cipherText
}
// rsa解密
func RsaDecrypt(privateKeyFile string, cypherText []byte) []byte {
// 將私鑰文件中的私鑰讀出, 得到使用pem編碼的字符串
file, err := os.Open(privateKeyFile)
if err != nil {
panic(err)
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
panic(err)
}
buf := make([]byte, fileInfo.Size())
file.Read(buf)
// 使用pem將得到的字符串解碼
block, _ := pem.Decode(buf)
// 使用x509將解碼後的私鑰解析出來
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
panic(err)
}
// 使用私鑰進行數據解密
plainText, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, cypherText)
if err != nil {
panic(err)
}
return plainText
}
// 測試
func main() {
generateKey()
cypherText := RsaEncrypt("public.pem", "hello rsa")
fmt.Println("加密後的字符串:", cypherText)
plainText := RsaDecrypt("private.pem", cypherText)
fmt.Println("解密後的字符串:", string(plainText))
}
1.3 ECC橢圓曲線
橢圓曲線(英語:Elliptic curve cryptography,縮寫爲 ECC)在密碼學中的使用是在1985年由Neal Koblitz和Victor Miller分別獨立提出的。ECC的主要優勢是在某些情況下,它比其他加密算法使用更小的密鑰,提供更高的安全級別。ECC 164位的密鑰相當於RSA 1024位密鑰提供的保密強度,而且計算量更少,處理速度更快。目前我國居民二代身份證正在使用 256 位的橢圓曲線密碼,虛擬貨幣比特幣也選擇ECC作爲加密算法。
1.4 對稱加密和非對稱加密的疑惑
問題1:非對稱加密比對稱加密的機密性更高嗎?
這個問題無法回答, 以爲機密性高低是根據祕鑰長度而變化的。
問題2:採用1024bit 祕鑰長度的非對稱加密, 和採用128bit祕鑰長度的對稱加密中, 是祕鑰更長的非對稱加密更安全嗎?
不是。從下表看出,1024比特的公鑰密碼與128比特的對稱密碼相比,反而是128比特對稱密碼的抵禦暴力破解的能力更強。
對稱加密的祕鑰長度 | 非對稱加密的祕鑰長度 |
---|---|
128比特 | 2304比特 |
112比特 | 1792比特 |
80比特 | 768比特 |
64比特 | 512比特 |
56比特 | 384比特 |
問題3:有了非對稱加密, 以後對稱加密會被替代嗎?
不會,一般來說,在採用具備同等機密性的密鑰長度的情況下,非對稱加密的處理速度只有對稱加密的幾百分之一。因此,非對稱加密並不適合用來對很長的消息內容進行加密。根據目的的不同,還可能會配合使用對稱加密和非對稱加密,例如,混合密碼系統就是將這兩種密碼組合而成的。
二、單向散列函數
2.1 什麼是單項散列函數
單向散列函數(One-way hash function),又稱單向Hash函數或雜湊函數。單向散列函數就是把任意長的輸入消息串變化成固定長的輸出串且由輸出串難以得到輸入串的一種函數。這個輸出串稱爲該消息的散列值。一般用於產生消息摘要,密鑰加密等。
這裏的消息不一定是文字,也可以是圖像、聲音等文件。無論任何消息,單向散列函數都會將它作爲單純的比特序列來處理,計算出散列值。散列值的長度和消息的長度無關。無論消息是1比特,還是1Mb,甚至是I00Mb,單向散列函數都會計算出固定長度的散列值。
2.2 單向散列函數的特性
- 散列值的長度是固定的
- 能夠快速計算出散列值
- 具有單向性
- 消息不同散列值也不同
兩個不同的消息產生同一個散列值的情況稱爲碰撞(collision)。如果使用單向散列函數進行數據完整性的檢查,則需要確保不可能被人爲地發現碰撞。
2.3 單向散列函數應用
2.3.1 檢測軟件是否被篡改
很多軟件,尤其是安全相關的軟件都會把通過單向散列函數計算出的散列值公佈在自己的官方網站上。用戶在下載到軟件之後,可以自行計算散列值,然後與官方網站上公佈的散列值進行對比。通過散列值,用戶可以確認自己所下載到的文件與軟件作者所提供的文件是否一致。
這樣的方法,在可以通過多種途徑得到軟件的情況下非常有用。爲了減輕服務器的壓力,很多軟件作者都會藉助多個網站(鏡像站點)來發布軟件,在這種情況下,單向散列函數就會在檢測軟件是否被篡改方面發揮重要作用。
2.3.2 消息認證碼
消息認證碼是將“發送者和接收者之間的共享密鑰”和“消息,進行混合後計算出的散列值。使用消息認證碼可以檢測並防止通信過程中的錯誤、篡改以及僞裝。
2.3.3 數字簽名
數字簽名是現實社會中的簽名(sign)和蓋章這樣的行爲在數字世界中的實現。數字簽名的處理過程非常耗時,因此一般不會對整個消息內容直接施加數字簽名,而是先通過單向散列函數計算出消息的散列值,然後再對這個散列值施加數字簽名。
2.3.4 僞隨機生成器
密碼技術中所使用的隨機數需要具備“事實上不可能根據過去的隨機數列預測未來的隨機數列”這樣的性質。爲了保證不可預測性,可以利用單向散列函數的單向性。
2.3.5 一次性口令
一次性口令經常被用於服務器對客戶端的合法性認證。在這種方式中,通過使用單向散列函數可以保證口令只在通信鏈路上傳送一次。因此,即使竊聽者竊取了口令,也無法使用。
2.4 常見的單向散列函數
2.4.1 MD5
MD5是由Rwest於1991年設計的單項散列函數,全稱是Message Digest Algorithm 5,譯爲“消息摘要算法第5版”。
- MD5的特點:
1)對輸入信息生成唯一的128比特的散列值;
2)明文不同,則散列值一定不同,明文相同,則散列值一定相同;
3)根據輸出值,不能得到原始的明文,即其過程不可逆;
MD5的強抗碰撞性已經被攻破,也就是說,現在已經能夠產生具備相同散列值的兩條不同的消息,因此它也已經不安全了。
示例代碼:
// 方式一
func GetMD5(plainText []byte) string {
// 1. 創建一個使用MD5校驗的Hash對象`
myHash := md5.New()
// 2. 通過io操作將數據寫入hash對象中
myHash.Write(plainText )
// 3. 計算結果
result := myHash.Sum(nil)
fmt.Println(result)
// 4. 將結果轉換爲16進制格式字符串
res := hex.EncodeToString(result)
return res
}
// 方式二
func GetMD5(plainText []byte) string {
// 執行md5加密
result := md5.Sum(plainText)
// 數據格式化爲16進制格式字符串
res := hex.EncodeToString(result[:])
return res
}
2.4.2 SHA-1、SHA-256、SHA-384、SHA-512
SHA-1是由NIST(NationalInstituteOfStandardsandTechnology,美國國家標準技術研究所)設計的一種能夠產生160比特散列值的單向散列函數。SHA-1的消息長度存在上限,但這個值接近於264比特,是個非常巨大的數值,因此在實際應用中沒有問題。
SHA-256、SHA-384和SHA-512都是由NIST設計的單向散列函數,它們的散列值長度分別爲256比特、384比特和512比特。這些單向散列函數合起來統稱SHA-2,它們的消息長度也存在上限(SHA-256的上限接近於 264 比特,SHA-384 和 SHA-512的上限接近於 2128 比特)。SHA-1的強抗碰撞性已於2005年被攻破, 也就是說,現在已經能夠產生具備相同散列值的兩條不同的消息。不過,SHA-2還尚未被攻破。
示例代碼:
// 方式一
func GetSha() string {
// 1.創建哈希接口對象
myHash := sha256.New()
// 2.添加數據
myHash.Write([]byte("hello "))
myHash.Write([]byte("world "))
// 3.計算結果
result := myHash.Sum(nil)
// 4.格式化爲16進制
res := hex.EncodeToString(result)
return res
}
// 方式二
func GetSha2() string {
// 直接調用Sum256方法生成散列值
result := sha256.Sum256([]byte("hello world "))
res := hex.EncodeToString(result[0:len(result)])
return res
}
三、消息認證碼
3.1 什麼是消息認證碼
消息認證碼(Message Authentication Code)是一種確認完整性並進行認證的技術,取三個單詞的首字母,簡稱爲MAC。
消息認證碼的輸入包括任意長度的消息和一個發送者與接收者之間共享的密鑰,它可以輸出固定長度的數據,這個數據稱爲MAC值。根據任意長度的消息輸出固定長度的數據,這一點和單向散列函數很類似。但是單向散列函數中計算散列值時不需要密鑰,而消息認證碼中則需要使用發送者與接收者之間共享的密鑰。
要計算MAC必須持有共享密鑰,沒有共享密鑰的人就無法計算MAC值,消息認證碼正是利用這一性質來完成認證的。此外,和單向散列函數的散列值一樣,哪怕消息中發生1比特的變化,MAC值也會產生變化,消息認證碼正是利用這一性質來確認完整性的。
3.2 消息認證碼的使用步驟
下面以銀行匯款爲例,介紹消息認證碼的使用過程。
- 發送者Alice和接收者Bob事先共享祕鑰;
- Alice根據匯款請求消息使用共享祕鑰計算出MAC值;
- Alice將匯款請求消息與MAC值一起發送給Bob;
- Bob根據接收到的匯款請求信息使用共享祕鑰計算出MAC值;
- Bob將自己計算的MAC值與Alice發送過來的MAC值進行比較,如果兩個MAC值一致,則Bob就可以斷定匯款請求消息是由Alice發送過來;如果不一致,則可以斷定消息不是來自於Alice;
3.3 HMAC
HMAC是一種使用單向散列函數來構造消息認證碼的方法(RFC2104規範),其中HMAC的H就是Hash的意思。HMAC中所使用的單向散列函數並不僅限於一種,任何高強度的單向散列函數都可以被用於HMAC,如果將來設計出新的單向散列函數,也同樣可以使用。使用SHA-I、MD5、RIPEMD-160所構造的HMAC,分別稱爲HMAC-SHA-1、HMAC-MD5和HMAC-RlPEMD。
-
消息認證碼的內部實現:
從上圖可以看出,通過單向散列函數生成的MAC值,一定是一個與輸入消息和密鑰相關的長度固定的比特序列。 -
使用單向散列函數生成HMAC:
func GenerateHmac(plainText, key []byte) []byte {
// 1.創建哈希接口,需要指定使用的哈希算法和密鑰
myHash := hmac.New(sha1.New, key)
// 2.給哈希對象添加數據
myHash.Write(plainText)
// 3.計算散列值
hashText := myHash.Sum(nil)
return hashText
}
func VerifyHmac(plainText, key, hashText []byte) bool {
// 1.創建哈希接口,需要指定使用的哈希算法和密鑰
myHash := hmac.New(sha1.New, key)
// 2.給哈希對象添加數據
myHash.Write(plainText)
// 3.計算散列值
hashText2 := myHash.Sum(nil)
// 4.比較散列值是否相等
return hmac.Equal(hashText, hashText2)
}
// 測試
func main() {
plainText := []byte("hello world") // 明文
key := []byte("123456") // 共享密鑰
hmac := GenerateHmac(plainText, key) // 生成消息認證碼
isEq := VerifyHmac(plainText, key, hmac)
fmt.Printf("校驗結果:%t\n", isEq)
}
注意:使用消息認證碼執行認證所使用的祕鑰和哈希函數必須相同。
- 消息認證碼的弊端:
1)無法通過第三方證明,不能讓大家信服;
2)無法阻止對方反悔;