03月09, 2018

对称加密和Base64编码

缘起

前阵子在用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编码的基本概念在这里做个简单的讲解,让大家能从基本原理中理解这两个方法的正确使用方式。

对称加密

简单说:使用相同密钥进行加密解密叫做对称加密。

带着的问题

  1. 加密和解密操作是如何实现的?
  2. DES、AES是什么意思?AES-128、AES-256又是什么东西?如何选择?
  3. 加密模式是什么?多种加密模式应当如何选择?
  4. 初始向量是什么?

加密和解密的运算本质(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)

上面描述的就是最简单的分组加密的概念,实际中的算法会复杂很多,这些算法,就是所谓的DES、3DES、AES等。

阶段小结

通过上面的解释,相信大家已经了解了对称加密的基本原理。

DES本质上就是一种将64bit的明文加密成64bit的密文的对称加密算法,但这种算法现在已经被认为是不安全的,所以后来又出现了3DES,它对DES具备向下兼容性。

AES则是为了取代DES而生的,它通过公开精选,最终选定了一个叫做Rijndael的分组密码算法。上面提到的AES-128、AES-256,即是指密钥的长度。

模式

模式指的就是分组加密的迭代方式,主要有如下几种:

  1. ECB
  2. CBC
  3. CFB
  4. OFB
  5. CTR

先给个结论:

  1. 绝对不要使用ECB ,这个模式是非常不安全的。
  2. 现在使用最多的,就是CBC,这也是TLS中在使用的模式。

ECB

ECB模式将明文分组加密之后的结果直接作为密文结果,如下图:

ECB

这个模式最大的问题,就是相同的明文分组会转换为相同的密文分组。因为只要观察一下密文,就可以知道明文中存在怎样的重复组合,并可以借此攻击。

举个例子,假如A要向B转账100元,数据由3个分组构成:

  1. 付款人A的账号
  2. 收款人B的账号
  3. 转账金额

ECB加密后的密文简单示例如下:

  1. 59 7D DE CC
  2. DF 49 2A 1C
  3. CD AF D5 9E

假如攻击者将1和2的内容进行调换,则变为:

  1. DF 49 2A 1C
  2. 59 7D DE CC
  3. CD AF D5 9E

这样一来,就变为了B向A转账100元。

ECB模式的一大漏洞,就是攻击者可以在不破解密文的情况下操纵明文。

CBC

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的长度,可选16、24、32字节,这是为了对应AES-128、AES-192、AES-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个字符为:

Base64-map

编码的过程,先将数据转成二进制形式,然后每6bit(2的6次方=64)计算十进制值,再在上面的对应表中转换为对应字符,最终得到一个字符串,示例:

Base64-example

最后的那些0,是需要进行补齐填充的bit。另外,标准Base64会使用=来替代A,这是因为=不在索引表中,可以作为结束符存在。

小结

使用Base64编码后的数据长度会增加1/3,Base32因为要使用更少的字符,所以编码后的长度要增加3/5。

有此可见,Base64在某种程度上来说兼顾了字符集大小和编码后数据长度,所以它的应用场景也更加广泛。

参考

图解密码技术

Base64编码原理与应用

http://blog.7rule.com/2018/03/09/crypto-base64.html

本文链接:https://www.opsdev.cn/post/crypto-base64.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。