对称加密 : AES算法原理,常见工作模式 与 Go示例代码分享

最近项目接入中需要用到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)
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值