背景
最近因为工作需要,要处理一种第三方的特别消息 。其内容经过AES加密,又通过Base64编码,由于官方文档没有Golang版本的示例,解析也费了一番功夫。正好借此机会,把Base64和AES全面了解一下。
什么是Base64
Base64是一种用于将二进制数据编码为ASCII字符的编码方法 。它的诞生最早是用于解决电子邮件中二进制数据的传输问题。基于历史原因,早期电子邮件只被允许传送ASCII字符,如果直接发送了一封带有非ASCII字符的电子邮件通过一些古老的网关时可能出现问题。Base64编码通过将二进制数据转换为ASCII字符,确保数据的可靠传输。Base64编码的名称中的 "64" 指的是它使用了64个字符的字符集,包括大小写字母、数字,以及一些特殊字符。这样的字符集可以用更少的字符表示更多的二进制数据,从而实现了数据的紧凑编码。虽然Base64最初是为了在电子邮件中传输二进制数据而设计的,但它后来被广泛应用于各种场景,成为在文本环境中安全、可靠地传输二进制数据的通用标准。值得注意的是:Base64编码不是一种加密算法,而是一种编码算法。
Base64的编码原理
Base64标准码表基于RFC4648,详情如下:
十进制 | 二进制 | 字符 | 十进制 | 二进制 | 字符 | 十进制 | 二进制 | 字符 | 十进制 | 二进制 | 字符 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 000000 | A | 16 | 010000 | Q | 32 | 100000 | g | 48 | 110000 | w |
1 | 000001 | B | 17 | 010001 | R | 33 | 100001 | h | 49 | 110001 | x |
2 | 000010 | C | 18 | 010010 | S | 34 | 100010 | i | 50 | 110010 | y |
3 | 000011 | D | 19 | 010011 | T | 35 | 100011 | j | 51 | 110011 | z |
4 | 000100 | E | 20 | 010100 | U | 36 | 100100 | k | 52 | 110100 | 0 |
5 | 000101 | F | 21 | 010101 | V | 37 | 100101 | l | 53 | 110101 | 1 |
6 | 000110 | G | 22 | 010110 | W | 38 | 100110 | m | 54 | 110110 | 2 |
7 | 000111 | H | 23 | 010111 | X | 39 | 100111 | n | 55 | 110111 | 3 |
8 | 001000 | I | 24 | 011000 | Y | 40 | 101000 | o | 56 | 111000 | 4 |
9 | 001001 | J | 25 | 011001 | Z | 41 | 101001 | p | 57 | 111001 | 5 |
10 | 001010 | K | 26 | 011010 | a | 42 | 101010 | q | 58 | 111010 | 6 |
11 | 001011 | L | 27 | 011011 | b | 43 | 101011 | r | 59 | 111011 | 7 |
12 | 001100 | M | 28 | 011100 | c | 44 | 101100 | s | 60 | 111100 | 8 |
13 | 001101 | N | 29 | 011101 | d | 45 | 101101 | t | 61 | 111101 | 9 |
14 | 001110 | O | 30 | 011110 | e | 46 | 101110 | u | 62 | 111110 | + |
15 | 001111 | P | 31 | 011111 | f | 47 | 101111 | v | 63 | 111111 | / |
填充 | = |
编码规则是:将3个字节编码为4个字符。3个字节共24位,正好可以拆成4个6位,而每6位可以对应一个码表中的字符。以Yes
举例,转换过程如下:
文本 | Y | e | s | |
---|---|---|---|---|
ASCII编码 | 89 | 101 | 115 | |
二进制位 | 0 1 0 1 1 0 0 1 | 0 1 1 0 0 1 0 1 | 0 1 1 1 0 0 1 1 | |
转换后二进制位 | 0 1 0 1 1 0 | 0 1 0 1 1 0 | 0 1 0 1 0 1 | 1 1 0 0 1 1 |
索引 | 22 | 22 | 21 | 51 |
Base64编码 | W | W | V | z |
当字节不足3时,通过0
来补足不全的位数,使其能被3整除。然后在后面补上1个或2个=
,代表缺少的字符。
文本 | N | o | ||
---|---|---|---|---|
二进制位 | 0 1 0 0 1 1 1 0 | 0 1 1 0 1 1 1 1 | ||
转换后二进制位 | 0 1 0 0 1 1 | 1 0 0 1 1 0 | 1 1 1 1 0 0 | = |
Base64编码 | T | m | 8 | = |
由于3个字符变成了4个字符,因此Base64编码会导致内容大小增大33%。
有哪些变种
Base64并不只有标准码表,还有各种各样的变种,常见的用于MIME、UTF-7、IMAP的不同RFC标准码表都各有不同,这里举个最为常见的例子是URL中用到的Base64编码。
URL中有时候需要传输一些中文字符、图片、二进制文件等。按照标准Base64编码,编码后会出现+
、/
等字符,而这些字符在URL中会被替换成%XX
的形式。为了兼容这种情况,URL中的Base64编码将+
、/
对应替换成了-
和_
。
因此Golang的encoding/base64包中才默认有两种不同的Encoding实现。

什么AES
进阶加密标准(英语:Advanced Encryption Standard,缩写:AES)是美国联邦政府采用的一种区块加密标准。由美国国家标准与技术研究院 (NIST)于2001年11月26日发布于FIPS PUB 197。并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。
AES的加解密原理由于比较复杂,我也不是专门研究密码学的,因此这里简单介绍一下。
密钥扩展
根据AES密钥长度进行密钥扩展,生成多个轮密钥。
初始轮
将明文数据分成128位块,并与第一个轮密钥进行异或操作。
多轮加密
各轮AES加密除最后一轮外均包含4个步骤:
字节替换(SubBytes):将每个字节映射到另一个字节,使用S-box进行替换。
行移位(ShiftRows):对每个128位块的行进行循环左移,第一行不移动,第二行左移1个字节,第三行左移2个字节,第四行左移3个字节。
列混淆(MixColumns):对每个128位块的列进行混淆,使用固定矩阵进行乘法运算。最后一轮加密中省略MixColumns步骤,而以另一个AddRoundKey取代。
轮密钥加(AddRoundKe):将每个128位块与下一个轮密钥进行异或操作。

最终轮
最后一轮加密后,将128位块与最后一个轮密钥进行异或操作。
我们需要知道些什么
我们接触AES,并不会按照加密原理进行代码实现。所以工作中我们需要知道的更多是AES中可以被使用者选择的部分。
密钥长度
AES支持三种长度的密钥:128位,192位,256位。平时所说的AES128,AES192,AES256,实际上就是指的AES算法对不同长度密钥的使用
分组模式
对明文进行分组的方式有很多像:计算器模式(Counter (CTR))、密码反馈模式(Cipher FeedBack (CFB))、输出反馈模式(Output FeedBack (OFB))等,这里列出两种常见的模式:
- 电码本模式(Electronic Codebook Book (ECB)): 将整个明文分成若干段相同的小段,然后对每一小段进行加密。
- 密码分组链接模式(Cipher Block Chaining (CBC)): 先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。
填充模式
由于明文是按固定大小进行分组的,因此对于最后一段不足大小的明文,需要进行填充。常见的有下面三种方式:
- NoPadding:不填充,但要求明文必须是16的整数倍
- PKCS7(Padding Cryptography System 7):在明文末尾添加填充字节,填充字节的值等于需要填充的字节数。
- PKCS5(Padding Cryptography System 5):与PKCS7填充类似,但用于8字节分组的加密算法。
回到原来的问题上
我最开始困惑于怎么解析这类加密消息。经过一番学习,已知当加密算法:AES-128
,分组方式:ECB
,填充方式:PKCS5
,编码方式:Base64 URL
。则可以用如下方式解析:
go
// 解码
func Decode(msg string, aesKey string) (Message, error) {
// 我自己的消息结构
message := Message{}
aesByte, err := Base64Decode(msg)
if err != nil {
return message, errors.Wrap(err, "base64 decode err")
}
aesKeyByte, _ := Base64Decode(aesKey)
msgByte, err := AesDecrypt(aesByte, aesKeyByte)
if err != nil {
return message, errors.Wrap(err, "aes decrypt err")
}
_ = json.Unmarshal(msgByte, &message)
return message, nil
}
// Base64解码
func Base64Decode(data string) ([]byte, error) {
// "="补齐不足的字符
var missing = (4 - len(data)%4) % 4
data += strings.Repeat("=", missing)
return base64.URLEncoding.DecodeString(data)
}
// 去除填充
func PKCS5UnPadding(data []byte) []byte {
l := len(data)
unpadding := int(data[l-1])
// 获取最后一个8位,代表填充了几个空字符
// 去掉填充的空字符
return data[:(l - unpadding)]
}
// 解密
func AesDecrypt(data, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return []byte{}, err
}
// 出于安全考虑,Golang不支持ECB模式,需要自己实现
buffer := bytes.NewBufferString("")
tmp := make([]byte, block.BlockSize())
for idx := 0; idx < len(data); idx += block.BlockSize() {
block.Decrypt(tmp, data[idx:idx+block.BlockSize()])
buffer.Write(tmp)
}
ret := PKCS5UnPadding(buffer.Bytes())
return ret, nil
}