缘起
前阵子在用 golang 重构一段 php 逻辑, 那段代码中用到了 mcrypt_encrypt 和 base64_encode 方法, 大致如下:
base64_encode(mcrypt_encrypt(self::MCRYPT_CIPHER, self::MCRYPT_KEY, $str, self::MCRYPT_MODE, self::MCRYPT_IV))
看起来挺简单的 php 调用, 使用 golang 重构时发现没那么简单
无论是 mcrypt_encrypt 还是 base64_encode, 在 golang 中都无法很简单的用一个现成的方法调用, 且看官方包文档时很多概念都搞不懂
网上查看了一些资料, 感觉理解的还不是很清楚, 最终我从这两个方法的基本概念入手, 简单学习了下, 感觉清晰了很多, 完成了重构
本篇文章就是把这次学到的关于对称加密和 base64 编码的基本概念在这里做个简单的讲解, 让大家能从基本原理中理解这两个方法的正确使用方式
对称加密
简单说: 使用相同密钥进行加密解密叫做对称加密
带着的问题
加密和解密操作是如何实现的?
DESAES 是什么意思? AES-128AES-256 又是什么东西? 如何选择?
加密模式是什么? 多种加密模式应当如何选择?
初始向量是什么?
加密和解密的运算本质(XOR)
现代加解密运算都是计算机运算, 也就是 0 和 1 的位运算, 对称加密中, 最重要的当属 XOR 运算
XOR 也叫做异或运算, 规则如下:
- 0 XOR 0 = 0
- 0 XOR 1 = 1
- 1 XOR 0 = 1
- 1 XOR 1 = 0
如果是比特序列之间的 XOR 运算, 只需要对其中每个对应的比特进行 XOR 运算即可, 例:
- 0 1 0 0 1 1 0 0 A
- 1 0 1 0 1 0 1 0 B
- ---------------------------------
- 1 1 1 0 0 1 1 0 A XOR B
这里就有了个有趣的地方:
- 0 1 0 0 1 1 0 0 A
- 1 0 1 0 1 0 1 0 B
- ---------------------------------
- 1 1 1 0 0 1 1 0 A XOR B
- 1 0 1 0 1 0 1 0 B
- ---------------------------------
- 0 1 0 0 1 1 0 0 A
即:
A XOR B XOR B = A
如果 A 是明文, B 是密钥, 那么 A XOR B = crypted 即实现了加密, crypted XOR B = A 即实现了解密
分组加密的概念
这里用一个最简单的示例来说明基本概念
上面大家看到了, 加密需要进行 XOR 运算, 这就需要运算双方的长度相同, 也就是明文和密钥的长度是相同的
但是, 实际通常要加密的明文长度很长, 密钥通常是相对固定的, 那么如何做呢?
这就需要把明文按照密钥的长度进行分组, 即分成多个
明文块(block)
, 然后对每个 block 分别和密钥进行迭代的 XOR 运算, 形成最终的密文, 其中迭代的方式就称为 模式(mode)
上面描述的就是最简单的分组加密的概念, 实际中的算法会复杂很多, 这些
算法, 就是所谓的 DES3DESAES
等
阶段小结
通过上面的解释, 相信大家已经了解了对称加密的基本原理
DES 本质上就是一种
将 64bit 的明文加密成 64bit 的密文
的对称加密算法, 但这种算法现在已经被认为是不安全的, 所以后来又出现了 3DES , 它对 DES 具备向下兼容性
AES 则是为了取代 DES 而生的, 它通过公开精选, 最终选定了一个叫做 Rijndael 的分组密码算法上面提到的 AES-128AES-256 , 即是指密钥的长度
模式
模式指的就是分组加密的迭代方式, 主要有如下几种:
- ECB
- CBC
- CFB
- OFB
- CTR
先给个结论:
绝对不要使用 ECB , 这个模式是非常不安全的
现在使用最多的, 就是 CBC , 这也是 TLS 中在使用的模式
ECB
ECB 模式将明文分组加密之后的结果直接作为密文结果, 如下图:
这个模式最大的问题, 就是相同的明文分组会转换为相同的密文分组因为只要观察一下密文, 就可以知道明文中存在怎样的重复组合, 并可以借此攻击
举个例子, 假如 A 要向 B 转账 100 元, 数据由 3 个分组构成:
付款人 A 的账号
收款人 B 的账号
转账金额
ECB 加密后的密文简单示例如下:
- 59 7D DE CC
- DF 49 2A 1C
- CD AF D5 9E
假如攻击者将 1 和 2 的内容进行调换, 则变为:
- DF 49 2A 1C
- 59 7D DE CC
- CD AF D5 9E
这样一来, 就变为了 B 向 A 转账 100 元
ECB 模式的一大漏洞, 就是攻击者可以在不破解密文的情况下操纵明文
CBC
CBC 的全称是
Cipher Block Chaining
(密文分组链接), 正如其名, 密文分组会像链条一样相互连接起来
CBC 模式首先将明文分组与前一个密文分组进行 XOR 运算, 然后再进行加密, 就这样一直加密完所有分组
这里出现了
初始化向量(IV)
这个概念: 当加密第一个明文分组时, 由于不存在前一个密文分组, 因此就需要一个长度为一个分组的比特序列来代替, 这个比特序列就叫做初始化向量(IV),ECB 中不需要
CBC 模式现在是使用最广泛的对称加密模式, TLS 协议 (https 中使用) 中即使用此模式
使用 golang 重构 php 的 mcrypt_encode
我们这里使用 AES 的 CBC 模式, 这里会涉及到 3 个重要的值: 密钥(key)
初始化向量(iv)
要加密的明文(data)
密钥 key
key 的长度, 可选 162432 字节 , 这是为了对应
AES-128AES-192AES-256
AES-256 的复杂度最高, 所以我们最好选择这个, 这样就需要
key 的长度是 32 字节
, 有个最好的办法就是对自定义的 key 做 md5 得到的字符串值即可
初始化向量 iv
iv 的长度, 需要和分组大小相同, AES 的分组大小是 128bit , 即 16 字节 我们这里可以对自定义的 iv 做 md5 后得到的字符串值取前 16 个字节即可
明文 data
明文会被自动分组, 为了能让每个分组的长度固定, 就需要先对明文进行数据填充, 再进行分组加密操作这里我们使用最常用的一种数据分组填充方式 PKCS5Padding
AES 代码
PKCS5Padding 代码
- Demo
- package main
- import (
- "github.com/goinbox/crypto"
- )
- func main() {
- key := crypto.Md5String([]byte("gobox")) //The key argument should be the AES key, either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
- iv := crypto.Md5String([]byte("goinbox"))[:crypto.AES_BLOCK_SIZE] //The length of iv must be the same as the Block's block size
- data := []byte("abc")
- acc, _ := crypto.NewAesCBCCrypter([]byte(key), []byte(iv))
- crypted := acc.Encrypt(data)
- d := acc.Decrypt(crypted)
- if string(d) != string(data) {
- println("crypto error")
- } else {
- println("crypto success")
- }
- }
是不是发现原本 php 一个方法做的事情, 到了 golang 中需要做这么多的事
很多高级语言都做了很多的封装, 让大家不必了解过多的细节即可使用, 但同时我们也失去了学习更多东西的机会
现在大家可以再回过头看看之前自己用过的对称加密方法, 对其中的参数是不是就理解了, 再想想看是否之前有使用不当的地方, 笔者就发现了之前一个重要的业务中使用了 ECB 模式, 赶紧改了, 呵呵
Base64 编码
再来说下 Base64 编码
上面加密后的密文, 通常会包含很多不可见字符, 这样通常会对加密后的结果做一次编码的工作, 这样就可以在各处使用了
带着的问题
这里我遇到的一个问题, 就是为什么要用 Base64 进行编码, 我看还有 Base32 啊, 为什么不用那个呢?
有了前面的经验, 我认识到还是要去了解下 Base64 编码和 Base32 编码的本质是什么, 才能判断
详解
Base64 编码使用 64 个字符来对任意数据进行编码, 同理 Base32 编码会使用 32 个字符对任意数据进行编码
Base64 编码的 64 个字符为:
编码的过程, 先将数据转成二进制形式, 然后每
6bit(2 的 6 次方 = 64)
计算十进制值, 再在上面的对应表中转换为对应字符, 最终得到一个字符串, 示例:
最后的那些 0, 是需要进行补齐填充的 bit 另外, 标准 Base64 会使用 = 来替代 A , 这是因为 = 不在索引表中, 可以作为结束符存在
小结
使用 Base64 编码后的数据长度会增加 1/3,Base32 因为要使用更少的字符, 所以编码后的长度要增加 3/5
有此可见, Base64 在某种程度上来说兼顾了字符集大小和编码后数据长度, 所以它的应用场景也更加广泛
参考
图解密码技术
Base64 编码原理与应用
http://blog.7rule.com/2018/03/09/crypto-base64.html
来源: http://www.tuicool.com/articles/26ZFZ37