對(duì)稱加密算法和 AEAD
對(duì)稱加密
對(duì)稱加密算法就是加密和解密都使用相同的密鑰的算法恩沛。它的加解密流程圖采用的是下面的形式來完成的:
明文 --> 密鑰 --> 密文
解密過程就是:
密文 --> 密鑰 --> 明文
當(dāng)明文完成加密過程之后就會(huì)形成密文亚兄。根據(jù)使用場(chǎng)景的不同账千,密文可以發(fā)送給其它處理對(duì)象券躁,或者是保存到存儲(chǔ)介質(zhì)中凤类,當(dāng)需要使用的時(shí)候扛稽,通過解密步驟就可以得到明文柴灯。
我們所熟悉的 DES物延、3DES宣旱、AES 都是對(duì)稱加密算法,相信稍微有點(diǎn)計(jì)算機(jī)方面知識(shí)和經(jīng)驗(yàn)的同學(xué)都知道在選擇對(duì)稱加密算法的時(shí)候叛薯,首選就是 AES 算法浑吟。一是 AES 是所有程序開發(fā)語言中都會(huì)包含在標(biāo)準(zhǔn)庫(kù)中的算法,同時(shí)耗溜,也是這些算法中推出時(shí)間靠后的密碼算法了组力。
DES 出現(xiàn)在 1974 年,AES 的出現(xiàn)在 2001 年抖拴。
如果沒有什么特殊情況的話燎字,在軟件領(lǐng)域,往往推出時(shí)間越靠后就代表越安全阿宅,比如軟件的版本候衍,后推出的版本就有可能會(huì)解決前期版本中的安全性問題。在密碼算法也大致如此家夺。但這并不是絕對(duì)的脱柱,不過 AES 確實(shí)目前還是安全的、也是首選的對(duì)稱密碼算法拉馋。
但選擇了 AES 作為加解密算法之后榨为,你還將面臨一個(gè)難題,如何在實(shí)際的生產(chǎn)環(huán)境中安全的運(yùn)用它煌茴。作為對(duì)稱加密算法随闺,它屬于塊(block)密碼,所以你還要考慮以下的問題:
- 加密模式怎么選蔓腐?
- 加密模式使用的 IV 或 nonce 怎么選矩乐?
當(dāng)然這些最基本的內(nèi)容我在這里不展開了,以后再補(bǔ)寫點(diǎn)基礎(chǔ)知識(shí)。這就是 AES 一次只能處理 128 比特即 16 字節(jié)的數(shù)據(jù)散罕,而需要處理信息有 1024 字節(jié)這么大分歇,怎么處理?
然后需要考慮的問題是對(duì)密文進(jìn)行認(rèn)證欧漱。也就是對(duì)明文使用完對(duì)稱加密算法后會(huì)得到密文职抡,在你不確定生成的密文最終處于什么樣的保護(hù)情況,對(duì)密文進(jìn)行認(rèn)證是一個(gè)非常值得考慮的做法误甚。
什么是保護(hù)情況呢缚甩?舉個(gè)例子,比方說你有套別墅窑邦、有輛幾百萬的保時(shí)捷擅威,每天回家后將車停在自家別墅的車庫(kù)里,鎖不鎖車無所謂冈钦,鎖匙放不放車上也無所謂郊丛,因?yàn)檫@個(gè)時(shí)候環(huán)境是安全的。而你開車上班派继,把車停在了辦公樓下的地下車庫(kù)宾袜,這時(shí)安全的做法就是鎖車拿走鎖匙。
所以驾窟,我們不能只用到對(duì)稱加密算法就行了庆猫,還需要用到簽名算法,對(duì)密文添加完整性和認(rèn)證的檢測(cè)能力绅络,這就是 Authenticated Encryption月培,簡(jiǎn)稱 AE。
Authenticated Encryption
那這個(gè)對(duì)密文簽名需要怎么做恩急?雖然大至有幾種方法杉畜,但最常用的作法就是對(duì)明文加密完成之后的密文運(yùn)用 MAC (消息認(rèn)證碼)算法進(jìn)行簽名,然后將密文+簽名返回衷恭。
后面進(jìn)一步的發(fā)展此叠,我們可以添加一些明文的信息到密文中去,這些明文的信息不需要加密随珠,但也需要保證它們的完整性和認(rèn)證能力灭袁,這就是 Authenticated Encryption with Associated Data,簡(jiǎn)稱 AEAD(關(guān)聯(lián)數(shù)據(jù)的認(rèn)證加密)窗看。
上面的這些知識(shí)又需要學(xué)習(xí)大量的密碼學(xué)相關(guān)理論了茸歧,但有了 Tink,上面這些問題你就不用考慮了显沈,Tink 能幫你搞定软瞎。
Tink 的 AEAD primitives 就是為這個(gè)目標(biāo)而誕生的逢唤,讓你不用和這些低層的加密算法 API 打交道也能很好的處理密碼學(xué)相關(guān)的問題。
Tink 將對(duì)稱加密算法包裝成了 AEAD primitives涤浇,是一種對(duì)稱加密的標(biāo)準(zhǔn)實(shí)踐鳖藕,也是目前安全的實(shí)踐。
AEAD 實(shí)現(xiàn)了對(duì)密文的機(jī)密性只锭、完整性和認(rèn)證的功能吊奢,能有效的防止針對(duì)密文的攻擊。比如說選擇密文攻擊纹烹,攻擊者通過修改密文后,提交給加密算法召边,來觀察加密算法的反應(yīng)铺呵,從而通過精心構(gòu)建的密文破解密鑰。
你可以這樣想 AE:
- DES隧熙、3DES片挂、AES 提供了加密功能;
- MAC 實(shí)現(xiàn)了完整性和認(rèn)證的功能贞盯。
而 AEAD 則是在上面 AE 的基礎(chǔ)上音念,讓你可以加入一些額外的認(rèn)證數(shù)據(jù),它是 AE 的另一種用法躏敢。
我們來看一下具體使用 Tink AEAD primitives 的方法闷愤。
AEAD 官方示例
準(zhǔn)備及運(yùn)行
目前官方給出的 AEAD 示例代碼如下,創(chuàng)建一個(gè) main.go 的文件件余,然后運(yùn)行看看結(jié)果:
package main
import (
"fmt"
"log"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
if err != nil {
log.Fatal(err)
}
a, err := aead.New(kh)
if err != nil {
log.Fatal(err)
}
ct, err := a.Encrypt([]byte("this data needs to be encrypted"), []byte("associated data"))
if err != nil {
log.Fatal(err)
}
pt, err := a.Decrypt(ct, []byte("associated data"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Cipher text: %s\nPlain text: %s\n", ct, pt)
}
運(yùn)行 main.go 之后讥脐,會(huì)輸出以下的內(nèi)容,你的密文確定不長(zhǎng)成這個(gè)樣子的:
$ go run main.go
Cipher text: ╔E????? c?1?L:?>?Yz??G ??M???ǖP???╗?<V.?L?\?D"?pC$e`?5?D
Plain text: this data needs to be encrypted
一般情況下我不太喜歡將密文直接輸出進(jìn)為字符串的形式啼器,因?yàn)槔锩姘刑嗟牟豢梢娮址蛴〕鰜砀袷角姘俟帧?/p>
所以,我喜歡將包含不可見字符的密文輸出為 HEX(十六進(jìn)制串)或 Base64端壳,所以告丢,這里可以將最后一行語句改成:
fmt.Printf("Cipher text: %x\nPlain text: %s\n", ct, pt)
修改之后,再次運(yùn)行下 main.go 程序:
$ go run main.go
Cipher text: 0187574a607e5a6e321cd16ea125546ef145d0f9e250a7ae4ce5ef259a715a7025cacdeb8e004c1bb98b2db96de439ed9023e8c5636b05f462dd298c597789c7
Plain text: this data needs to be encrypted
總算是沒有看到 '?' 這樣的不可見字符了损谦,這樣看起來就舒服多了岖免。
示例執(zhí)行流程
從上面的示例代碼中,也就是通過該加解密過程來分析成翩,在代碼的處理過程中觅捆,一共包含的幾個(gè)步驟,分別用創(chuàng)建的對(duì)象來分析麻敌,結(jié)果如下:
創(chuàng)建 KeyTemplate栅炒;
語句aead.AES256GCMKeyTemplate()
創(chuàng)建了一個(gè)密鑰模板,該模板是定義如何生成密鑰,包含加解密算法赢赊、密鑰長(zhǎng)度乙漓、IV 長(zhǎng)度等值。比如這里定義了一個(gè)基于 AES 的 32 位算法释移,采用 GCM 加密模式生成密鑰叭披;通過 KeyTemplate 創(chuàng)建 KeysetHandle;
根據(jù) KeyTemplate 創(chuàng)建的密鑰(Key)存放在鍵集(Keyset玩讳,也可稱為密鑰集)中涩蜘,KeysetHandle 就是操作密鑰集的,它用來執(zhí)行對(duì)密鑰的打印熏纯、保存和讀取操作同诫,但它會(huì)對(duì)敏感的密鑰信息進(jìn)行保護(hù),比方說你不能直接打印密鑰的具體內(nèi)容樟澜,但可以看到密鑰的元信息(ID號(hào)误窖、狀態(tài)、密鑰前綴)秩贰。通過 KeysetHandle 創(chuàng)建 AEAD primitive霹俺;
現(xiàn)在,就可以通過鍵集(Keyset)中保存的密鑰來創(chuàng)建 AEAD 加密使用的 primitive毒费,它將提供加密(Encrypt)和解密(Decrypt)方法用于后繼的加解密操作丙唧。使用 AEAD primitive 執(zhí)行加密或解密操作。
加密和解密接口
上面我們使用 AEAD primitive 的加密(Encrypt)和解密(Decrypt)方法蝗罗,它們的接口定義如下:
type AEAD interface {
// Encrypt encrypts plaintext with additionalData as additional
// authenticated data. The resulting ciphertext allows for checking
// authenticity and integrity of additional data additionalData,
// but there are no guarantees wrt. secrecy of that data.
Encrypt(plaintext, additionalData []byte) ([]byte, error)
// Decrypt decrypts ciphertext with {@code additionalData} as additional
// authenticated data. The decryption verifies the authenticity and integrity
// of the additional data, but there are no guarantees wrt. secrecy of that data.
Decrypt(ciphertext, additionalData []byte) ([]byte, error)
}
-
Encrypt
Encrypt(plaintext, additionalData []byte) ([]byte, error)
- 輸入:明文(plaintext)和額外的驗(yàn)證數(shù)據(jù)(additionalData)艇棕;
- 返回:密文(ciphertext),或者錯(cuò)誤信息串塑;
-
Decrypt
Decrypt(ciphertext, additionalData []byte) ([]byte, error)
- 輸入:密文(ciphertext)和額外的驗(yàn)證數(shù)據(jù)(additionalData)沼琉;
- 返回:明文(plaintext),或者錯(cuò)誤信息桩匪;
所有的處理操作都是以 []byte 的形式來處理的打瘪,而現(xiàn)實(shí)世界中一般都是字符串,但在 Go 語言中傻昙,字符串轉(zhuǎn)換成 []byte 是相當(dāng)?shù)暮?jiǎn)單闺骚,只需要 []byte("這是字符串")
就可以了。
你會(huì)發(fā)現(xiàn)妆档,這里有一個(gè)重要的內(nèi)容沒有看到僻爽,就是加解密使用的密鑰。這可以是個(gè)對(duì)密碼體系來說非常關(guān)鍵部分贾惦,怎么就沒有出現(xiàn)了胸梆?難道不需要使用么敦捧?
這就是 Tink 設(shè)計(jì)該加密庫(kù)的一個(gè)要點(diǎn),就是因?yàn)槊荑€相當(dāng)重要碰镜,所以必須通過密鑰系統(tǒng)來管理兢卵。后面文章中會(huì)提到如何使用密鑰管理系統(tǒng)來進(jìn)行密鑰管理的內(nèi)容。
額外認(rèn)證數(shù)據(jù)是什么绪颖?
額外認(rèn)證數(shù)據(jù) (additional authenticated data, AAD)秽荤,是在使用 AEAD primitive 時(shí),加密和解密中需要傳入的柠横。按照定義它是可以被用來提供一些包括版本或是以密文相關(guān)內(nèi)容的信息窃款,加密算法不會(huì)加密這類的數(shù)據(jù),但會(huì)對(duì)該數(shù)據(jù)進(jìn)行誰牍氛。
現(xiàn)在雁乡,我們來看看額外誰數(shù)據(jù)的特點(diǎn)。
額外認(rèn)證數(shù)據(jù)是否被加密糜俗?
額外認(rèn)證數(shù)據(jù)不是說以明文的形式存在于密文之中的么,那這些數(shù)據(jù)是插入到密文的前面曲饱?還是后面悠抹?或者說是中間?那現(xiàn)在采用一段代碼來觀察一下效果扩淀。以傳入大量的小寫字母 “a”楔敌,它的十六進(jìn)制數(shù)是 0x61,來看看密文的變化驻谆,有沒有這么多的 0x61 出現(xiàn)卵凑。
package main
import (
"fmt"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, _ := keyset.NewHandle(aead.AES256GCMKeyTemplate())
a, _ := aead.New(kh)
ct, _ := a.Encrypt([]byte("this data needs to be encrypted"), []byte("aaaaaaaaaaaaaaaa"))
pt, _ := a.Decrypt(ct, []byte("aaaaaaaaaaaaaaaa"))
fmt.Printf("Cipher text: %x\nPlain text: %s\n", ct, pt)
}
輸出為:
Cipher text: 0177d8f90b3c1ebab79afcb179e564762f71dcf65f98b022cce5c55099deda197dac1d33cd15d6b858050b2a9e153f8cb2997d28c87e79076c3d62761b211ad1
Plain text: this data needs to be encrypted
通過輸出的結(jié)果可以發(fā)現(xiàn),密文中并沒有那么多的 0x61 出現(xiàn)胜臊,那是不是這些數(shù)據(jù)被加密呢勺卢?
其實(shí)這些額外認(rèn)證數(shù)據(jù)并沒有被加密,它們只是和密文做了當(dāng)應(yīng)的運(yùn)算象对,主要是和密文進(jìn)行異或運(yùn)算黑忱。
額外認(rèn)證數(shù)據(jù)不一致能解密么?
如果輸入了錯(cuò)誤的額外認(rèn)證數(shù)據(jù)勒魔,密文是否能正常的解密甫煞?我們使用代碼測(cè)試下:
package main
import (
"fmt"
"log"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, _ := keyset.NewHandle(aead.AES256GCMKeyTemplate())
a, _ := aead.New(kh)
ct, _ := a.Encrypt([]byte("this data needs to be encrypted"), []byte("associated data"))
pt, err := a.Decrypt(ct, []byte("changed associated data"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Cipher text: %x\nPlain text: %s\n", ct, pt)
}
由于提供了不一樣的額外認(rèn)證數(shù)據(jù),運(yùn)行該代碼之后冠绢,會(huì)得到信息:
aead_factory: decryption failed
現(xiàn)在的結(jié)論就是加密時(shí)抚吠,如果使用和加密不一樣的額外認(rèn)證數(shù)據(jù)是無法將密文還原成明文的。
關(guān)于安全性的問題
雖然了解到上面的兩點(diǎn)內(nèi)容弟胀,包括不知道額外認(rèn)證數(shù)據(jù)不能解密密文楷力,但最終我們需要知道的是喊式,AEAD 的安全性還是會(huì)體現(xiàn)在密鑰的安全性上,如果密鑰被他人獲取弥雹,你是無法通過額外認(rèn)證數(shù)據(jù)獲得任何安全上的保障的垃帅。
在對(duì)稱加密體系中,安全保障是通過密鑰來提供的剪勿,這點(diǎn)是需要牢記的贸诚。
你能正常的解密么?
通過上面的示例和對(duì)該示例主要步驟的分解厕吉,讓你可以掌握基礎(chǔ)的 Tink 在對(duì)稱加密方面的使用酱固。
但是這還遠(yuǎn)遠(yuǎn)不夠。你可以運(yùn)行下面的兩段代碼來體驗(yàn)下在使用過程中遇到的問題头朱。也就是加解密分離运悲,因?yàn)樯厦婺嵌问纠a只是示例使用,不可能用到任何環(huán)境项钮,因?yàn)槟悴豢赡芸吹郊用芎篑R上又解密的班眯。
一般來說,我們的應(yīng)用場(chǎng)景可能是下面這樣:
- 一個(gè)文件或字符串烁巫,通過加密算法加密后署隘,保存到文件系統(tǒng)或數(shù)據(jù)庫(kù)中,等到使用的時(shí)候再讀出來解密后使用亚隙;
- 一段消息磁餐,通過加密算法加密后發(fā)送到指定的授受者,然后授受者使用事先約定好的密鑰解密后查看阿弃;
為了展示起來清晰诊霹,我將加解密的過程分開到兩段不同的代碼中,這樣你可以完全的了解代碼所需要做的事情渣淳。
加密信息
首先脾还,過程 Encrypt 接口對(duì)一段明文信息 “this data needs to be encrypted” 進(jìn)行加密:
package main
import (
"fmt"
"log"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
if err != nil {
log.Fatal(err)
}
a, err := aead.New(kh)
if err != nil {
log.Fatal(err)
}
ct, err := a.Encrypt([]byte("this data needs to be encrypted"), []byte("associated data"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Cipher text: %x\n", ct)
}
運(yùn)行后可以得到密文:
$ go run main.go
Cipher text: 01ec5977d49fd2565fa00dd241b1d8dcbd073b497cdf4d8454dcc15dbf5e9705831125e99ac523d4f787e5e7a2b7bb51248ea4459ef6db749b2cbd0b321dcd40
解密密文
當(dāng)?shù)玫矫芪闹螅梢酝ㄟ^使用 Decrypt 進(jìn)行解密入愧。由于得到的是二進(jìn)制字符串荠呐,在 Go 中需要使用 hex.DecodeString 方法將其轉(zhuǎn)換成 []byte 類型:
package main
import (
"encoding/hex"
"fmt"
"log"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
if err != nil {
log.Fatal(err)
}
a, err := aead.New(kh)
if err != nil {
log.Fatal(err)
}
pt := "01ec5977d49fd2565fa00dd241b1d8dcbd073b497cdf4d8454dcc15dbf5e9705831125e99ac523d4f787e5e7a2b7bb51248ea4459ef6db749b2cbd0b321dcd40"
ptByte, err := hex.DecodeString(pt)
if err != nil {
log.Fatal(err)
}
ct, err := a.Decrypt(ptByte, []byte("associated data"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Cipher text: %x\n", ct)
}
運(yùn)行代碼對(duì)上面的密文進(jìn)行解密:
$ go run main.go
aead_factory: decryption failed
exit status 1
然后,我們發(fā)現(xiàn)解密失敗砂客。這是為什么呢泥张?
其實(shí)這是因?yàn)?strong>解密使用的密鑰和加密使用的密鑰不一樣,加密使用的密鑰其實(shí)是隨機(jī)生成的鞠值,而并沒有保存下來媚创,加密代碼運(yùn)行完成之后,密鑰也就消失了彤恶。
后面會(huì)提到使用密鑰管理系統(tǒng)來處理密鑰問題钞钙。
重要的概念
- KeysetHandle:用來處理鍵集(Keyset)的對(duì)象鳄橘;
- Keyset:鍵集,由密鑰(Key)構(gòu)成的一個(gè)集合芒炼;
- Key:密鑰瘫怜,在對(duì)稱加密算法中,加密和解密密鑰是一樣的本刽;
- 密鑰管理系統(tǒng)(KMS):一個(gè)用來保存和管理密鑰生命周期的系統(tǒng)鲸湃;
- Primitives(原語):這里指定的是與密碼學(xué)處理相關(guān)的接口;
總結(jié)
通過上面的 AEAD 加解密流程和注意事項(xiàng)等內(nèi)容子寓,我想你也應(yīng)該能了解到 Tink 里加解密相關(guān)設(shè)計(jì)和實(shí)現(xiàn)的思路暗挑。這樣的設(shè)計(jì)和實(shí)現(xiàn)的方式可以大大的減化你編寫對(duì)稱加密實(shí)現(xiàn)自身業(yè)務(wù)的難度。
在這里我們了解到使用 AEAD Primitives 來處理對(duì)稱加解密的過程斜友,并了解到了額外認(rèn)證數(shù)據(jù)(add)使用的注意事項(xiàng)炸裆。
不過 Tink 不是萬能的,它解決了這方面的問題鲜屏,但密鑰管理的問題還是留給你了烹看,因?yàn)樗吘怪皇且粋€(gè)密碼庫(kù)。Tink 提供了相應(yīng)的 API 連接 KMS (密鑰管理系統(tǒng))洛史,但密鑰管理的問題還是需要你去規(guī)劃和實(shí)施的部分听系,以后會(huì)介紹。
現(xiàn)在虹菲,你可以自己嘗試一下,如果加密時(shí)不使用額外認(rèn)證數(shù)據(jù)掉瞳,比如使用 nil 或 []byte("")
毕源,得到密文后,能不能使用 nil 或 []byte("")
正解解密密文陕习。