go 中的 AES 加密

最近接触到了 AES 加密算法上的一个使用问题,由于不太理解该算法的原理,导致排查问题过程中带来了一些困扰。因此打算学习记录一下 AES 加密的原理和 golang 中的实现与加密包的正确使用方式。

什么是分块加密算法 (Block Ciphers)

按字面理解即将加解密内容进行分块处理之后对每一块数据进行加密。通常情况下,加密内容需要划分成指定分块大小的多个块(如 AES 方式中通常是 16 位),并且分块加密算法通常是对称加密算法。

初始化向量 (Initialization Vector, IV):

在分块加密算法中,为了增加加密的随机性,通常会使用一个随机的初始化向量,该向量在加密和解密时需要保持一致。

填充方式 (Padding):

由于分块加密算法要求加密内容长度必须是分块大小的整数倍,因此在加密时需要对内容进行填充,填充方式有很多种,如 PKCS7零填充 等。

  • PKCS7 填充方式:在填充的字节中填充填充的字节数,如填充 3 个字节,则填充 0x03 0x03 0x03。该方式便于解密时去除填充。
  • 零填充 填充方式:在填充的字节中填充 0x00

AES 加密

AES (Rijndael) 是常用的分组加密算法,该算法秘钥大小可以是 16、24、32 字节,分别对应 AES-128、AES-192、AES-256。

AES 的一轮加密有四个流程:

  1. 字节替换 (SubBytes):将每个字节替换为 sbox (一个 16*16 字节的替换表) 中的对应字节。
  2. 行移位 (ShiftRows):将每一行循环左移不同的位数。
  3. 列混淆 (MixColumns):将每一列进行混淆。
  4. 轮密钥加 (AddRoundKey):将轮密钥与当前状态进行异或操作。

ECB 和 CBC 模式

AES 加密算法有多种工作模式,其中最常见的是 ECB 和 CBC 模式。

  • ECB (Electronic Codebook) 模式:将明文分成块,每一块独立加密,然后合并加密后的块。该模式不适合加密大量数据,因为相同的明文块会加密成相同的密文块。
  • CBC (Cipher Block Chaining) 模式:在加密前,将明文与上一个密文块进行异或操作,然后再加密。该模式适合加密大量数据,因为每一块的加密都依赖上一块的密文。

crypto/cipher 包注意点

可以看到 crypto/cipher 包中的 CryptBlocks 方法对入参 dstsrc 做了说明,因此在使用时需要注意 dstsrc 的长度是否合法,否则可能会导致程序崩溃。

  • dst : 是加密后的数据存储位置,长度必须大于等于 src 的长度。
  • src : 是需要加密的数据,长度必须是块大小的整数倍。
  • 如果传入 dstsrc 相同,则会在 src 上进行加密。
    块解密函数
    校验

总结

了解完算法后,还是对源码中 CryptBlocks 函数中参数校验失败直接 panic 设计方式有些不解, 违背了 “错误即值” 的哲学。
但咨询了某 go 交流群里的大佬后,大佬后给出了一些解释: 非系统边界校验失败直接 panic 是为了避免错误的传递,而是在系统边界校验失败时才返回错误。

DEMO (AES-CBC)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
)

func EncryptAESCBC(key, text string) (string, error) {
	block, err := aes.NewCipher([]byte(key))
	if err != nil {
		return "", err
	}

	plaintext := padCBC([]byte(text))
	ciphertext := make([]byte, aes.BlockSize+len(plaintext))
	iv := ciphertext[:aes.BlockSize]
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return "", err
	}

	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)

	return base64.StdEncoding.EncodeToString(ciphertext), nil
}

func DecryptAESCBC(key, text string) (string, error) {
	block, err := aes.NewCipher([]byte(key))
	if err != nil {
		return "", err
	}

	ciphertext, err := base64.StdEncoding.DecodeString(text)
	if err != nil {
		return "", err
	}

	if len(ciphertext) < aes.BlockSize {
		return "", errors.New("ciphertext too short")
	}

	iv := ciphertext[:aes.BlockSize]
	ciphertext = ciphertext[aes.BlockSize:]

	if len(ciphertext)%aes.BlockSize != 0 {
		return "", errors.New("ciphertext is not a multiple of the block size")
	}

	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(ciphertext, ciphertext)

	return string(unpadCBC(ciphertext)), nil
}

// PKCS7 padding
func padCBC(in []byte) []byte {
	padding := aes.BlockSize - (len(in) % aes.BlockSize)
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(in, padtext...)
}

func unpadCBC(in []byte) []byte {
	padding := int(in[len(in)-1])
	return in[:len(in)-padding]
}

func main() {

	key := "example key 1234"
	text := "Hello, world!"

	encrypted, err := EncryptAESCBC(key, text)
	if err != nil {
		fmt.Println("Error encrypting:", err)
		return
	}

	fmt.Println("Encrypted:", encrypted)

	decrypted, err := DecryptAESCBC(key, encrypted)
	if err != nil {
		fmt.Println("Error decrypting:", err)
		return
	}

	fmt.Println("Decrypted:", decrypted)
}

引用

wiki 分组加密工作模式
Understanding how AES encryption works

0%