Base64和AES那些事儿

背景

最近因为工作需要,要处理一种第三方的特别消息 。其内容经过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并不只有标准码表,还有各种各样的变种,常见的用于MIMEUTF-7IMAP的不同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
}
相关推荐
Aska_Lv19 分钟前
业务架构设计---硬件设备监控指标数据上报业务Java企业级架构
后端·架构
m0_7482552629 分钟前
Spring Boot 3.x 引入springdoc-openapi (内置Swagger UI、webmvc-api)
spring boot·后端·ui
小华同学ai34 分钟前
吊打中文合成!这款开源语音神器效果炸裂,逼真到离谱!
前端·后端·github
语落心生41 分钟前
算法计算与训练如何支持低开销流式计算? deepseek背后的smallpond需要些新改造
后端
uhakadotcom1 小时前
Python高并发实战:阿里云函数计算 + 异步编程高效处理万人请求
后端·面试·github
uhakadotcom1 小时前
Apache Flink:实时数据处理的强大工具
后端·面试·github
INSO1 小时前
Docker Compose
后端
uhakadotcom1 小时前
了解Nginx替代品:选择合适的Web服务器
后端·面试·github
RisingWave中文开源社区1 小时前
经验分享|用开源产品构建一个高性能实时推荐引擎
数据库·后端·开源
Asthenia04121 小时前
深入浅出聊聊 ElasticSearch 相关性算分在电商搜索场景应用:从简单销量排序到复杂优化
后端