最近项目接入中需要用到aes对称加密算法,深入go官方提供的算法库,发现缺少ecb模式的封装,完善算法库并深入讲解aes算法,与分享常见的三种分组模式示例。
一、AES
1.1、原理
AES是高级加密标准,在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,目前已经被全世界广泛使用,同时AES已经成为对称密钥加密中最流行的算法之一。AES支持三种长度的密钥:128位,192位,256位。
- AES算法基于四个核心加密模块:SubBytes模块、ShiftRows模块、MixColumns模块和AddRoundKey模块。其中,SubBytes使用S盒对每一个字节进行替换,ShiftRows对每一行进行位移,MixColumns使用矩阵乘法对每一列进行混淆,AddRoundKey将轮密钥和每一个数据块进行异或。
- 整个加密流程由多个轮次组成,每个轮次使用一个轮密钥和相应的核心加密模块对数据块进行操作,最后一轮不进行MixColumns操作。
1.1.1 密钥
密钥是AES算法实现加密和解密的根本。对称加密算法之所以对称,是因为这类算法对明文的加密和解密需要使用同一个密钥。
AES支持三种长度的密钥:
128位,192位,256位
平时大家所说的AES128,AES192,AES256,实际上就是指的AES算法对不同长度密钥的使用。
1.1.2 填充
要想了解填充的概念,我们先要了解AES的分组加密特性。什么是分组加密呢?我们来看看下面这张图:AES算法在对明文加密的时候,并不是把整个明文一股脑加密成一整段密文,而是把明文拆分成一个个独立的明文块,每一个明文块长度128bit。
这些明文块经过AES加密器的复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密结果。
假如一段明文长度是192bit,如果按每128bit一个明文块来拆分的话,第二个明文块只有64bit,不足128bit。这时候怎么办呢?就需要对明文块进行填充(Padding)。
填充涉及以下三种填充模式:
- NoPadding:
不做任何填充,但是要求明文必须是16字节的整数倍。
- PKCS5Padding(默认),PKCS7Padding(aes的5与7相同):
如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。
比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则补全为{1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6}
- ISO10126Padding:
如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。
比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则可能补全为{1,2,3,4,5,a,b,c,d,e,5,c,3,G,$,6}
二、AES算法流程
AES加密算法涉及4种操作:字节替代(SubBytes)、行移位(ShiftRows)、列混淆(MixColumns)和轮密钥加(AddRoundKey)。下图给出了AES加解密的流程,从图中可以看出:
1、解密算法的每一步分别对应加密算法的逆操作;
2、加解密所有操作的顺序正好是相反的。正是由于这几点(再加上加密算法与解密算法每步的操作互逆)保证了算法的正确性。加解密中每轮的密钥分别由种子密钥经过密钥扩展算法得到。算法中16字节的明文、密文和轮子密钥都以一个4x4的矩阵表示。
AddRoundKey (轮密钥加)— 矩阵中的每一个字节都与该次轮密钥(round key)做XOR运算;每个子密钥由密钥生成方案产生。
SubBytes(字节替代) — 通过非线性的替换函数,用查找表的方式把每个字节替换成对应的字节。
ShiftRows(行移位) — 将矩阵中的每个横列进行循环式移位。
MixColumns (列混淆)— 为了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每列的四个字节。
三、AES工作模式
分组密码算法只能加密固定长度为 N 比特的分组数据(DES 和 3DES 算法中 N=64,AES 算法中 N=128),若待加密数据长度 != N ,则待加密数据需要被分组或填充至长度为 N 比特的数据块用以加密,至于如何分组及填充则取决于使用的工作模式和填充方式。
早在 1981 年,DES 算法公布之后,NIST 在标准文献 FIPS 81 中公布了 4 种工作模式:
- 电子密码本:Electronic Code Book Mode (ECB)
- 密码分组链接:Cipher Block Chaining Mode (CBC)
- 密文反馈:Cipher Feedback Mode (CFB)
- 输出反馈:Output Feedback Mode (OFB)
后面又新增了一些工作模式:
- 计数器模式:Counter Mode (CTR)
- 填充密码分组链接:Propagating Cipher Block Chaining Mode(PCBC)
3.1 ECB 模式
ECB 加密所需数据:明文 P、加密密钥 Key、数据填充模式 M。
ECB 加密步骤如下:
- 将 P 分为 P0、P1、P2、...、Pn,Px 长度为 = 128,长度不足需填充
- 用 Key 将 P0 加密得出 C0
- 用 Key 将 P1 加密得出 C1
- ...
- 用 Key 将 Pn 加密得出 Cn
- 拼接 C0、C1、...Cn 得到密文 C
-
-
ECB 的理想应用场景是短数据(如加密密钥)的加密。ECB 模式中明文和密文是一一对应的,相同的明文分组加密将会得到相应的密文分组,因此它不能很好的隐藏模式。
3.2 CBC 模式
CBC 加密所需数据:明文 P、加密密钥 Key、初始向量 IV、数据填充模式 M。
CBC 加密步骤如下:
- 将 P 分为 P0、P1、P2、...、Pn,Px 长度 = 128,长度不足需填充
- 将 P0、IV 做异或运算得到 P0_IV,用 Key 将 P0_IV 加密得到 C0
- 将 P1、C0 做异或运算得到 P1_IV,用 Key 将 P1_IV 加密得到 C1
- ...
- 将 Pn、C(n-1) 做异或运算得到 Pn_IV,用 Key 将 Pn_IV 加密得到 Cn
- 拼接 C0、C1、...Cn 得到密文 C
-
-
CBC 模式相比 ECB 实现了更好的模式隐藏,但因为其将密文引入运算,加解密操作无法并行操作。同时引入的 IV 向量,还需要加、解密双方共同知晓方可。
3.3 CFB 模式
CFB 加密所需数据:明文 P、加密密钥 Key、初始向量 IV。
CFB 加密步骤如下:
- 将 P 分为 P0、P1、P2、...、Pn,Px 长度 <= 128
- 用 Key 加密 IV 得到 IV0,将 IV0、P0 做异或运算得到 C0
- 用 Key 加密 C0 得到 IV1,将 IV1、P1 做异或运算得到 C1
- ...
- 用 Key 加密 C(n-1) 得到 IVn,将 IVn、Pn 做异或运算得到 Cn
- 拼接 C0、C1、...Cn 得到密文 C,加密结束
-
-
CFB 模式是用分组算法实现流算法,明文数据不需要按分组大小对齐。
四、Go语言代码示例分享
go语言官方库"crypto/aes"中已经提供了CBC与CFB,但是官方库中没有ECB的模式,至于为什么没有ECB模式,可以查看 官方issue,意思就是不安全,但是我们确实要使用的话,怎么去实现呢,下面进入正题。
4.1 Go ECB
ECB的实现,可以参照标准库中cbc的实现方式,首先我们看CBC的代码实现
NewCBCEncrypter函数返回了一个BlockMode接口,说明cbcEncrypter是实现了BlockMode接口
type cbc struct {
b Block
blockSize int
iv []byte
tmp []byte
}
func newCBC(b Block, iv []byte) *cbc {
return &cbc{
b: b,
blockSize: b.BlockSize(),
iv: dup(iv),
tmp: make([]byte, b.BlockSize()),
}
}
type cbcEncrypter cbc
// NewCBCEncrypter函数返回了一个BlockMode接口,说明cbcEncrypter是实现了BlockMode接口
func NewCBCEncrypter(b Block, iv []byte) BlockMode {
...
return (*cbcEncrypter)(newCBC(b, iv))
}
type BlockMode interface {
BlockSize() int
CryptBlocks(dst, src []byte)
}
我们也可以仿照cbc,用ecb也去实现BlockMode接口
/**
* ecb
* @Description:
**/
type Ecb struct {
b cipher.Block
blockSize int
//iv []byte ecb 模式無需iv向量
}
func newECB(b cipher.Block) *Ecb {
return &Ecb{
b: b,
blockSize: b.BlockSize(),
}
}
type ecbEncrypter Ecb
type ecbDecrypter Ecb
/**
* @Description: Ecb encrypter
* @param: b
* @return: cipher.BlockMode
* @Author: Iori <yuanzhuang@mini1.cn>
* @Date: 2023-03-06 17:41:46
**/
func NewECBEncrypter(b cipher.Block) cipher.BlockMode {
return (*ecbEncrypter)(newECB(b))
}
func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
if len(src)%x.blockSize != 0 {
panic("crypto/cipher: input not full blocks")
}
if len(dst) < len(src) {
panic("crypto/cipher: output smaller than input")
}
for len(src) > 0 {
x.b.Encrypt(dst, src[:x.blockSize])
src = src[x.blockSize:]
dst = dst[x.blockSize:]
}
}
ecbEncrypter也实现了BlockMode的接口,就可以按照cbc的方式进行解密了
然后就是封装方法,写入自己的算法库
/**
* @Description: Ecb encryption 128bit
* @param: encryptStr
* @param: key
* @param: iv
* @return: string
* @return: error
* @Author: Iori <yuanzhuang@mini1.cn>
* @Date: 2023-03-06 17:02:27
**/
func EcbEncrypt(encrypt, key string) (string, error) {
encryptBytes := convert.Str2bytes(encrypt)
block, err := aes.NewCipher(convert.Str2bytes(key))
if err != nil {
return "", err
}
encryptBytes = pkcs7Padding(encryptBytes, block.BlockSize())
blockMode := NewECBEncrypter(block)
encrypted := make([]byte, len(encryptBytes))
blockMode.CryptBlocks(encrypted, encryptBytes)
return crypto.Base64EncodeStr(encrypted), nil
}
/**
* @Description: Ecb decrypt
* @param: decryptStr
* @param: key
* @param: iv
* @return: string
* @return: error
* @Author: Iori <yuanzhuang@mini1.cn>
* @Date: 2023-03-06 17:27:21
**/
func EcbDecrypt(decryptStr, key string) (string, error) {
decryptBytes, err := crypto.Base64DecodeStr(decryptStr)
if err != nil {
return "", err
}
block, err := aes.NewCipher(convert.Str2bytes(key))
if err != nil {
return "", err
}
blockMode := NewECBDecrypter(block)
decrypted := make([]byte, len(decryptBytes))
blockMode.CryptBlocks(decrypted, decryptBytes)
decrypted = pkcs7UnPadding(decrypted)
return convert.Bytes2str(decrypted), nil
}
demo示例:
func TestAes(b *testing.T) {
origData := "Hello World" // 待加密的数据
key := "ABCDEFGHIJKLMNOP" // 加密的密钥
iv := "!xxxxx20wbxxxx#z" // 偏移的向量iv
log.Println("原文:", string(origData))
log.Println("------------------ CBC模式 --------------------")
encrypted, _ := aes.CbcEncrypt(origData, key, iv)
//log.Println("密文(hex):", hex.EncodeToString([]byte(encrypted)))
log.Println("密文(base64):", encrypted)
decrypted, _ := aes.CbcDecrypt(encrypted, key, iv)
log.Println("解密结果:", decrypted)
log.Println("------------------ ECB模式 --------------------")
encrypted, _ = aes.EcbEncrypt(origData, key)
//log.Println("密文(hex):", hex.EncodeToString(encrypted))
log.Println("密文(base64):", encrypted)
decrypted, _ = aes.EcbDecrypt(encrypted, key)
log.Println("解密结果:", string(decrypted))
log.Println("------------------ CFB模式 --------------------")
encrypted, _ = aes.CfbEncrypt(origData, key, iv)
//log.Println("密文(hex):", hex.EncodeToString(encrypted))
log.Println("密文(base64):", encrypted)
decrypted, _ = aes.CfbDecrypt(encrypted, key, iv)
log.Println("解密结果:", decrypted)
}