[TOC]
常用密码技术
1 密码
1.1 发送者、接收者和窃听者
请想象一个Alice向Bob发送电子邮件的场景。在这个场景中,发出邮件的Alice称为 发送者(sender),而收到邮件的Bob则称为 接收者(receiver)。
在讲解发送者、接收者的概念时,用邮件这个例子会比较便于理解,但实际上发送者和接收者这两个术语的使用范围并不仅仅局限于邮件。当某个人向另一个人发送信息时,发出信息的人称为发送者,而收到信息的人称为接收者。另外,被发送的信息有时也统称为 消息(message)。
-
Alice向Bob发送邮件
1538728596743.png
邮件是通过互联网从Alice的计算机发送到Bob的计算机的。在发送邮件时,邮件会经过许多台计算机和通信设备进行中转,在这个过程中,就存在被恶意窃听者(eavesdropper)偷看到的可能性。
- Eve(窃听者)看到邮件的内容
窃听者Eve并不一定是人类,有可能是安装在通信设备上的某种窃听器,也可能是安装在邮件软件和邮件服务器上的某些程序。
尽管邮件内容原本应该只有发送者和接收者两个人知道,但如果不采取相应的对策,就存在被第三方知道的风险。
1.2 加密和解密
Alice不想让别人看到邮件的内容,于是她决定将邮件进行加密(encrypt)后再发送出去。
加密之前的消息称为明文(plaintext),加密之后的消息称为密文(cipher-text)。
我们看到明文可以理解其中的含义,而看到密文则无法理解其中的含义。
- 明文加密之后就会变成看不懂的密文
Bob收到了来自Alice的加密邮件,但作为接收者的Bob也是无法直接阅读密文的,于是
Bob需要对密文进行解密(decrypt)之后再阅读。解密就是将密文恢复成明文的过程。
- 密文解密之后就变成了原来的明文
将消息加密后发送的话,即使消息被窃听,窃听者得到的也只是密文,而无法得知加密前的明文内容
- 将消息加密后发送, 窃听者只能得到密文
在上述场景中,Alice将邮件进行加密,而Bob则进行解密,这样做的目的,是为了不让窃听者Eve读取邮件的内容Alice和Bob通过运用密码(cryptography)技术,保证了邮件的机密性(confidentiality)。
1.3 秘钥
1.3.1 密码算法
用于解决复杂问题的步骤,通常称为算法(algorithm)。从明文生成密文的步骤,也就是加密的步骤,称为“加密算法",而解密的步骤则称为“解密算法"。加密、解密的算法合在一起统称为密码算法。
1.3.2 秘钥
密码算法中需要密钥(key)。现实世界中的“钥'',是像 :key: 这样的形状微妙而复杂的小金属片。然而,密码算法中的密钥,则是像203554728568477650354673080689430768这样的一串非常大的数字。
- 加密、解密与秘钥
无论是在加密时还是在解密时,都需要知道密钥。
正如保险柜的钥匙可以保护保险柜中存放的贵重物品一样,密码中的密钥可以保护你的重要数据。即使保险箱再坚固,如果钥匙被盗, 里面的贵重物品也会被盗。同样地我们也必须注意不要让密码的密钥被他人窃取。
1.4 凯撒密码
恺撒密码(Caesar cipher)是一种相传尤利乌斯·恺撒曾使用过的密码。恺撒于公元前100年左右诞生于古罗马,是一位著名的军事统帅。
<font color="red">恺撤密码是通过将明文中所使用的字母表按照一定的字数“平移”来进行加密的</font>。比如在日语(例如平假名)或者汉语(例如汉语拼音)或者英文字母表中都可以用同样的思路来实现恺撒密码。
为了讲解方便,我们用小写字母(a,b,c,…)来表小明文,用大写字母(A,B,C,...)来表示密文。
现在我们将字母表平移3个字母,于是,明文中的a在加密后就变成了与其相隔3个字母的D,以此类推。b变成E,c变成F,d变成G......v变成Y,w变成Z,而x则会回到字母表的开头而变成A,相应地,y变成B,z变成C。通过下图我们可以很容易地理解“平移"的具体工作方式。
1.4.1 凯撒密码的加密
这里,我们假设要保密的信息为monkey d luffy这个男孩的名字。我们暂且不管这个名字到底代表一位真实的男性,还是只是一种暗号,只考虑将它在保密的状态下发送给接收者。
此时,明文包含下列12个字母:monkey d luffy, 接下来我们对明文中的字母逐一加密:
m ---> P
o ---> R
n ---> Q
k ---> N
e ---> H
y ---> B
d ---> G
l ---> O
u ---> X
f ---> I
f ---> I
y ---> B
这样,明文 monkey d luffy 就被转换成了密文PRQNHB G OXIIB,monkey d luffy这个词我们能够看懂,但
PRQNHB G OXIIB就看不懂了。
恺撒密码中,将字母表中的字母平移这个操作就是密码的算法,而平移的字母数量则相当于密钥。在上面的例子中,密钥为3(如下图)。
1.4.2 凯撒密码的解密
现在,假设接收者已经收到了密文PRQNHB G OXIIB,由于密文本身是看不懂的,因此必须将它解密成明文。
恺撒密码的解密过程是使用与加密时相同的密钥进行反向的平移操作。用刚才的例子来说,只要反向平移3个字母就可以解密了。
P ---> m
R ---> o
Q ---> n
N ---> k
H ---> e
B ---> y
G ---> d
O ---> l
X ---> u
I ---> f
I ---> f
B ---> y
这样我们就得到了明文monkey d luffy。
在这个场景中, 秘钥3必须由发送者和接收者事先约定好。
1.5 密码信息安全常识与威胁
1.5.1 密码信息安全常识
在继续下面的内容之前,我们先来介绍一些关于密码的常识。刚刚开始学习密码的人常常会对以下这几条感到不可思议,因为它们有悖于我们的一般性常识。
- 不要使用保密的密码算法
- 使用低强度的密码比不进行任何加密更危险
- 任何密码总有一天都会被破解
- 密码只是信息安全的一部分
不要使用保密的密码算法
很多企业都有下面这样的想法:
“由公司自己开发一种密码算法,并将这种算法保密,这样就能保证安全。然而,这样的想法却是大错特错,使用保密的密码算法是无法获得高安全性的。我们不应该制作或使用任何保密的密码算法,而是应该使用那些已经公开的、被公认为强度较高的密码算法。
这样做的原因主要有以下两点:
-
密码算法的秘密早晚会公诸于世
从历史上看,密码算法的秘密最终无一例外地都会被暴露出来。例如: RSA公司开发的RC4密码算法曾经也是保密的,但最终还是有一位匿名人士开发并公开了与其等效的程序。
一旦密码算法的详细信息被暴露,依靠对密码算法本身进行保密来确保机密性的密码系统也就土崩瓦解了。反之,那些公开的算法从一开始就没有设想过要保密,因此算法的暴露丝毫不会削弱它们的强度。
-
开发高强度的密码算法是非常困难的
- 要比较密码算法的强弱是极其困难的,因为密码算法的强度并不像数学那样可以进行严密的证明。密码算法的强度只能通过事实来证明,如果专业密码破译者经过数年的尝试仍然没有破解某个密码算法,则说明这种算法的强度较高。
- 稍微聪明一点的程序员很容易就能够编写出“自己的密码系统"。这样的密码在外行看来貌似牢不可破,但在专业密码破译者的眼里,要破解这样的密码几乎是手到擒来。
- 现在世界上公开的被认为强度较高的密码算法,几乎都是经过密码破译者长期尝试破解未果而存活下来的。因此,如果认为“公司自己开发的密码系统比那些公开的密码系统更强”,那只能说是过于高估自己公司的能力了。
- 试图通过对密码算法本身进行保密来确保安全性的行为,一般称为隐蔽式安全性(securitybyobscurity),这种行为是危险且愚蠢的。
- 反过来说,将密码算法的详细信息以及程序源代码全部交给专业密码破译者,并且为其提供大量的明文和密文样本,如果在这样的情况下破译一段新的密文依然需要花上相当长的时间,就说明这是高强度的密码。
使用低强度的密码比不进行任何加密更危险
一般人们会认为.就算密码的强度再低,也比完全不加密要强吧?其实这样的想法是非常危险的。
正确的想法应该是:与其使用低强度的密码,还不如从一开始就不使用任何密码这主要是由于用户容易通过“密码”这个词获得一种“错误的安全感”。对于用户来说,安全感与密码的强度无关,而只是由“信息已经被加密了”这一事实产生的,而这通常会导致用户在处理一些机密信息的时候麻痹大意。
任何密码总有一天会被破译
如果某种密码产品宣称“本产品使用了绝对不会被破解的密码算法”,那么你就要对这个产品的安全性打个问号了,这是因为绝对不会被破解的密码是不存在的。
无论使用任何密码算法所生成的密文,只要将所有可能的密钥全部尝试一遍,就总有一天可以破译出来。因此,破译密文所需要花费的时间,与要保密的明文的价值之间的权衡就显得非常重要。
密码只是信息安全的一部分
我们还是回到Alice给Bob发送加密邮件的例子。即便不去破解密码算法,也依然有很多方法能够知道Alice所发送的邮件内容, 例如:
攻击者可以不去试图破译经过加密的邮件,而是转而攻击Alice的电脑以获取加密之前的邮件明文。
上面提到的攻击手段,都与密码的强度毫无关系。要保证良好的安全性,就需要理解“系统”这一概念本身的性质复杂的系统就像一根由无数个环节相连组成的链条,如果用力拉,链条就会从其中最脆弱的环节处断开。因此,系统的强度取决于其中最脆弱的环节的强度。
最脆弱的环节并不是密码,而是人类自己。
1.5.2 密码信息威胁
我们将信息安全所面临的威胁与用来用对这些威胁的密码技术直接的关系用一张图标来表示出来。
2. 对称加密
"对称加密: 也称为对称密码, 是指在加密和解码时使用同一秘钥的加密方式"
2.1 编码
现代的密码都是建立在计算机的基础之上的,这是因为现代的密码所处理的数据量非常大,而且密码算法也非常复杂,不借助计算机的力量就无法完成加密和解密的操作。
计算机的操作对象并不是文字,而是由0和1排列而成的比特序列。无论是文字、图像、声音、视频还是程序,在计算机中都是用比特序列来表示的。执行加密操作的程序,就是将表示明文的比特序列转换为表示密文的比特序列。
将现实世界中的东西映射为比特序列的操作称为编码(encoding)。例如midnight(深夜)这个词,我们可以对其中的每个字母逐一进行编码,这种编码规则叫作ASCII。
注意这里的m --> 01101101这一转换并不是加密而是编码。尽管在人类看来0和1的序列跟密码没什么两样,但计算机却可以“看懂"这些比特序列,并很快地反应出其所对应的字符 midnight
2.2 DES
2.2.1 什么是DES
DES(Data Encryption Standard)是1977年美国联邦信息处理标准(FIPS)中所采用的一种对称密码(FIPS46.3)。DES一直以来被美国以及其他国家的政府和银行等广泛使用。然而,随着计算机的进步,现在DES已经能够被暴力破解,强度大不如前了。
RSA公司举办过破泽DES密钥的比赛(DESChallenge),我们可以看一看RSA公司官方公布的比赛结果:
- 1997年的DES Challenge1中用了96天破译密钥
- 1998年的DES ChallengeIl-I中用了41天破译密钥
- 1998年的DES ChallengeII-2中用了56小时破译密钥
- 1999年的DES ChallengeIll中只用了22小时15分钟破译密钥
由于DES的密文可以在短时间内被破译,因此除了用它来解密以前的密文以外,现在我们不应该再使用DES了。
2.2.2 加密和解密
DES是一种将64比特的明文加密成64比特的密文的对称密码算法,==它的密钥长度是56比特==。尽管<font color="red">从规格上来说,DES的密钥长度是64比特,但由于每隔7比特会设置一个用于错误检查的比特,因此实质上其密钥长度是56比特</font>。
<font color="red">DES是以64比特的明文(比特序列)为一个单位来进行加密的</font>,这个64比特的单位称为分组。一般来说,以分组为单位进行处理的密码算法称为分组密码(blockcipher),DES就是分组密码的一种。
DES每次只能加密64比特的数据,如果要加密的明文比较长,就需要对DES加密进行迭代(反复),而迭代的具体方式就称为模式(mode)。
大B -> bit
小b -> byte
秘钥长度(56bit + 8bit)/8 = 8byte 12345678
- DES的加密与解密 - 图例
2.2.3 Go中对DES的操作
加解密实现思路
-
加密 - CBC分组模式
- 创建并返回一个使用DES算法的cipher.Block接口
- 秘钥长度为64bit, 即 64/8 = 8字节(byte)
- 对最后一个明文分组进行数据填充
- DES是以64比特的明文(比特序列)为一个单位来进行加密的
- 最后一组不够64bit, 则需要进行数据填充( 参考第三章)
- 创建一个密码分组为链接模式的, 底层使用DES加密的BlockMode接口
- 加密连续的数据块
- 创建并返回一个使用DES算法的cipher.Block接口
-
解密
- 创建并返回一个使用DES算法的cipher.Block接口
- 创建一个密码分组为链接模式的, 底层使用DES解密的BlockMode接口
- 数据块解密
- 去掉最后一组的填充数据
加解密的代码实现
在Go中使用DES需要导入的包:
import (
"crypto/des"
"crypto/cipher"
"fmt"
"bytes"
)
DES加密代码:
// src -> 要加密的明文
// key -> 秘钥, 大小为: 8byte
func DesEncrypt_CBC(src, key []byte) []byte{
// 1. 创建并返回一个使用DES算法的cipher.Block接口
block, err := des.NewCipher(key)
// 2. 判断是否创建成功
if err != nil{
panic(err)
}
// 3. 对最后一个明文分组进行数据填充
src = PKCS5Padding(src, block.BlockSize())
// 4. 创建一个密码分组为链接模式的, 底层使用DES加密的BlockMode接口
// 参数iv的长度, 必须等于b的块尺寸
tmp := []byte("helloAAA")
blackMode := cipher.NewCBCEncrypter(block, tmp)
// 5. 加密连续的数据块
dst := make([]byte, len(src))
blackMode.CryptBlocks(dst, src)
fmt.Println("加密之后的数据: ", dst)
// 6. 将加密数据返回
return dst
}
DES解密代码
// src -> 要解密的密文
// key -> 秘钥, 和加密秘钥相同, 大小为: 8byte
func DesDecrypt_CBC(src, key []byte) []byte {
// 1. 创建并返回一个使用DES算法的cipher.Block接口
block, err := des.NewCipher(key)
// 2. 判断是否创建成功
if err != nil{
panic(err)
}
// 3. 创建一个密码分组为链接模式的, 底层使用DES解密的BlockMode接口
tmp := []byte("helloAAA")
blockMode := cipher.NewCBCDecrypter(block, tmp)
// 4. 解密数据
dst := src
blockMode.CryptBlocks(src, dst)
// 5. 去掉最后一组填充的数据
dst = PKCS5UnPadding(dst)
// 6. 返回结果
return dst
}
最后一个分组添加填充数据和移除添加数据代码
// 使用pks5的方式填充
func PKCS5Padding(ciphertext []byte, blockSize int) []byte{
// 1. 计算最后一个分组缺多少个字节
padding := blockSize - (len(ciphertext)%blockSize)
// 2. 创建一个大小为padding的切片, 每个字节的值为padding
padText := bytes.Repeat([]byte{byte(padding)}, padding)
// 3. 将padText添加到原始数据的后边, 将最后一个分组缺少的字节数补齐
newText := append(ciphertext, padText...)
return newText
}
// 删除pks5填充的尾部数据
func PKCS5UnPadding(origData []byte) []byte{
// 1. 计算数据的总长度
length := len(origData)
// 2. 根据填充的字节值得到填充的次数
number := int(origData[length-1])
// 3. 将尾部填充的number个字节去掉
return origData[:(length-number)]
}
测试函数
func DESText() {
// 加密
key := []byte("11111111")
result := DesEncrypt_CBC([]byte("床前明月光, 疑是地上霜. 举头望明月, 低头思故乡."), key)
fmt.Println(base64.StdEncoding.EncodeToString(result))
// 解密
result = DesDecrypt_CBC(result, key)
fmt.Println("解密之后的数据: ", string(result))
}
重要的函数说明
-
生成一个底层使用DES加/解密的Block接口对象
函数对应的包: import "crypto/des" func NewCipher(key []byte) (cipher.Block, error) - 参数 key: des对称加密使用的密码, 密码长度为64bit, 即8byte - 返回值 cipher.Block: 创建出的使用DES加/解密的Block接口对象
-
创建一个密码分组为CBC模式, 底层使用b加密的BlockMode接口对象
函数对应的包: import "crypto/cipher" func NewCBCEncrypter(b Block, iv []byte) BlockMode - 参数 b: 使用des.NewCipher函数创建出的Block接口对象 - 参数 iv: 事先准备好的一个长度为一个分组长度的比特序列, 每个分组为64bit, 即8byte - 返回值: 得到的BlockMode接口对象
-
使用cipher包的BlockMode接口对象对数据进行加/解密
接口对应的包: import "crypto/cipher" type BlockMode interface { // 返回加密字节块的大小 BlockSize() int // 加密或解密连续的数据块,src的尺寸必须是块大小的整数倍,src和dst可指向同一内存地址 CryptBlocks(dst, src []byte) } 接口中的 CryptBlocks(dst, src []byte) 方法: - 参数 dst: 传出参数, 存储加密或解密运算之后的结果 - 参数 src: 传入参数, 需要进行加密或解密的数据切片(字符串)
-
创建一个密码分组为CBC模式, 底层使用b解密的BlockMode接口对象
函数对应的包: import "crypto/cipher" func NewCBCDecrypter(b Block, iv []byte) BlockMode - 参数 b: 使用des.NewCipher函数创建出的Block接口对象 - 参数 iv: 事先准备好的一个长度为一个分组长度的比特序列, 每个分组为64bit, 即8byte, 该序列的值需要和NewCBCEncrypter函数的第二个参数iv值相同 - 返回值: 得到的BlockMode接口对象
-
自定义函数介绍
对称加密加密需要对数据进行分组, 保证每个分组的数据长度相等, 如果最后一个分组长度不够, 需要进行填充 func PKCS5Padding(ciphertext []byte, blockSize int) []byte - 参数 ciphertext: 需要加密的原始数据 - 参数 blockSize: 每个分组的长度, 跟使用的加密算法有关系 * des:64bit, 8byte * 3des:64bit, 8byte * aes: 128bit, 16byte
2.3 三重DES
现在DES已经可以在现实的时间内被暴力破解,因此我们需要一种用来替代DES的分组密码,三重DES就是出于这个目的被开发出来的。
三重DES(triple-DES)是为了增加DES的强度,==将DES重复3次所得到的一种密码算法==,通常缩写为3DES。
2.3.1 三重DES的加密
三重DES的加解密机制如图所示:
加->解->加 -> 目的是为了兼容des
3des秘钥长度24字节 = 1234567a 1234567b 1234567a
明文: 10
秘钥1: 2
秘钥2: 3
秘钥3: 4
加密算法: 明文+秘钥
解密算法: 密文-秘钥
10+2-3+4
明文经过三次DES处理才能变成最后的密文,由于DES密钥的长度实质上是56比特,因此<font color="red">三重DES的密钥长度就是56×3=168比特, 加上用于错误检测的标志位8x3, 共192bit</font>。
从上图我们可以发现,三重DES并不是进行三次DES加密(加密-->加密-->加密),而是<font color="red">加密-->解密-->加密</font>的过程。在加密算法中加人解密操作让人感觉很不可思议,实际上这个方法是IBM公司设计出来的,目的是为了让三重DES能够兼容普通的DES。
<font color="red">当三重DES中所有的密钥都相同时,三重DES也就等同于普通的DES了</font>。这是因为在前两步加密-->解密之后,得到的就是最初的明文。因此,以前用DES加密的密文,就可以通过这种方式用三重DES来进行解密。也就是说,三重DES对DES具备向下兼容性。
如果密钥1和密钥3使用相同的密钥,而密钥2使用不同的密钥(也就是只使用两个DES密钥),这种三重DES就称为DES-EDE2。EDE表示的是加密(Encryption) -->解密(Decryption)-->加密(Encryption)这个流程。
密钥1、密钥2、密钥3全部使用不同的比特序列的三重DES称为DES-EDE3。
尽管三重DES目前还被银行等机构使用,但其处理速度不高,而且在安全性方面也逐渐显现出了一些问题。
2.3.2 Go中对3DES的操作
加解密实现思路
-
加密 - CBC分组模式
- 创建并返回一个使用3DES算法的cipher.Block接口
- 秘钥长度为64bit3=192bit, 即 192/8 = 24字节(byte)*
- 对最后一个明文分组进行数据填充
- 3DES是以64比特的明文(比特序列)为一个单位来进行加密的
- 最后一组不够64bit, 则需要进行数据填充( 参考第三章)
- 创建一个密码分组为链接模式的, 底层使用3DES加密的BlockMode接口
- 加密连续的数据块
- 创建并返回一个使用3DES算法的cipher.Block接口
-
解密
- 创建并返回一个使用3DES算法的cipher.Block接口
- 创建一个密码分组为链接模式的, 底层使用3DES解密的BlockMode接口
- 数据块解密
- 去掉最后一组的填充数据
加解密的代码实现
3DES加密代码
// 3DES加密
func TripleDESEncrypt(src, key []byte) []byte {
// 1. 创建并返回一个使用3DES算法的cipher.Block接口
block, err := des.NewTripleDESCipher(key)
if err != nil{
panic(err)
}
// 2. 对最后一组明文进行填充
src = PKCS5Padding(src, block.BlockSize())
// 3. 创建一个密码分组为链接模式, 底层使用3DES加密的BlockMode模型
blockMode := cipher.NewCBCEncrypter(block, key[:8])
// 4. 加密数据
dst := src
blockMode.CryptBlocks(dst, src)
return dst
}
3DES解密代码
// 3DES解密
func TripleDESDecrypt(src, key []byte) []byte {
// 1. 创建3DES算法的Block接口对象
block, err := des.NewTripleDESCipher(key)
if err != nil{
panic(err)
}
// 2. 创建密码分组为链接模式, 底层使用3DES解密的BlockMode模型
blockMode := cipher.NewCBCDecrypter(block, key[:8])
// 3. 解密
dst := src
blockMode.CryptBlocks(dst, src)
// 4. 去掉尾部填充的数据
dst = PKCS5UnPadding(dst)
return dst
}
重要的函数说明
-
生成一个底层使用3DES加/解密的Block接口对象
函数对应的包: import "crypto/des" func NewTripleDESCipher(key []byte) (cipher.Block, error) - 参数 key: 3des对称加密使用的密码, 密码长度为(64*3)bit, 即(8*3)byte - 返回值 cipher.Block: 创建出的使用DES加/解密的Block接口对象
-
创建一个密码分组为CBC模式, 底层使用b加密的BlockMode接口对象
函数对应的包: import "crypto/cipher" func NewCBCEncrypter(b Block, iv []byte) BlockMode - 参数 b: 使用des.NewTripleDESCipher 函数创建出的Block接口对象 - 参数 iv: 事先准备好的一个长度为一个分组长度的比特序列, 每个分组为64bit, 即8byte - 返回值: 得到的BlockMode接口对象
-
使用cipher包的BlockMode接口对象对数据进行加/解密
接口对应的包: import "crypto/cipher" type BlockMode interface { // 返回加密字节块的大小 BlockSize() int // 加密或解密连续的数据块,src的尺寸必须是块大小的整数倍,src和dst可指向同一内存地址 CryptBlocks(dst, src []byte) } 接口中的 CryptBlocks(dst, src []byte) 方法: - 参数 dst: 传出参数, 存储加密或解密运算之后的结果 - 参数 src: 传入参数, 需要进行加密或解密的数据切片(字符串)
-
创建一个密码分组为CBC模式, 底层使用b解密的BlockMode接口对象
函数对应的包: import "crypto/cipher" func NewCBCDecrypter(b Block, iv []byte) BlockMode - 参数 b: 使用des.NewTripleDESCipher 函数创建出的Block接口对象 - 参数 iv: 事先准备好的一个长度为一个分组长度的比特序列, 每个分组为64bit, 即8byte, 该序列的值需要和NewCBCEncrypter函数的第二个参数iv值相同 - 返回值: 得到的BlockMode接口对象
2.4 AES
AES(Advanced Encryption Standard)是取代其前任标准(DES)而成为新标准的一种对称密码算法。全世界的企业和密码学家提交了多个对称密码算法作为AES的候选,最终在2000年从这些候选算法中选出了一种名为==Rijndael==的对称密码算法,并将其确定为了AES。
Rijndael是由比利时密码学家Joan Daemen和Vincent Rijmen设汁的分组密码算法,今后会有越来越多的密码软件支持这种算法。
==Rijndael的分组长度为128比特==,密钥长度可以以32比特为单位在128比特到256比特的范围内进行选择(不过==在AES的规格中,密钥长度只有128、192和256比特三种==)。
128bit = 16字节
192bit = 24字节
256bit = 32字节
在go提供的接口中秘钥长度只能是16字节
2.4.2 AES的加密和解密
和DES—样,AES算法也是由多个轮所构成的,下图展示了每一轮的大致计算步骤。DES使用Feistel网络作为其基本结构,而AES没有使用Feistel网络,而是使用了SPN Rijndael的输人分组为128比特,也就是16字节。首先,需要逐个字节地对16字节的输入数据进行SubBytes处理。所谓SubBytes,就是以每个字节的值(0~255中的任意值)为索引,从一张拥有256个值的替换表(S-Box)中查找出对应值的处理,也是说,将一个1字节的值替换成另一个1字节的值。
SubBytes之后需要进行ShiftRows处理,即将SubBytes的输出以字节为单位进行打乱处理。从下图的线我们可以看出,这种打乱处理是有规律的。
ShiftRows之后需要进行MixCo1umns处理,即对一个4字节的值进行比特运算,将其变为另外一个4字节值。
最后,需要将MixColumns的输出与轮密钥进行XOR,即进行AddRoundKey处理。到这里,AES的一轮就结東了。实际上,在AES中需要重复进行10 ~ 14轮计算。
通过上面的结构我们可以发现输入的所有比特在一轮中都会被加密。和每一轮都只加密一半输人的比特的Feistel网络相比,这种方式的优势在于加密所需要的轮数更少。此外,这种方式还有一个优势,即SubBytes,ShiftRows和MixColumns可以分别按字节、行和列为单位进行并行计算。
<font color="red">SubBytes -- 字节代换</font>
<font color="red">ShiftRows -- 行移位代换</font>
<font color="red">MixColumns -- 列混淆 </font>
<font color="red">AddRoundKey -- 轮密钥加</font>
下图展示了AES中一轮的解密过程。从图中我们可以看出,SubBytes、ShiftRows、MixColumns分别存在反向运算InvSubBytes、InvShiftRows、InvMixColumns,这是因为AES不像Feistel网络一样能够用同一种结构实现加密和解密。
1538731285324.png
<font color="red">InvSubBytes -- 逆字节替代</font>
<font color="red">InvShiftRows -- 逆行移位</font>
<font color="red">InvMixColumns -- 逆列混淆</font>
2.4.2 Go中对AES的使用
加解密实现思路
-
加密 - CBC分组模式
- 创建并返回一个使用AES算法的cipher.Block接口
- 秘钥长度为128bit, 即 128/8 = 16字节(byte)
- 对最后一个明文分组进行数据填充
- AES是以128比特的明文(比特序列)为一个单位来进行加密的
- 最后一组不够128bit, 则需要进行数据填充( 参考第三章)
- 创建一个密码分组为链接模式的, 底层使用AES加密的BlockMode接口
- 加密连续的数据块
- 创建并返回一个使用AES算法的cipher.Block接口
-
解密
- 创建并返回一个使用AES算法的cipher.Block接口
- 创建一个密码分组为链接模式的, 底层使用AES解密的BlockMode接口
- 数据块解密
- 去掉最后一组的填充数据
加解密的代码实现
AES加密代码
// AES加密
func AESEncrypt(src, key []byte) []byte{
// 1. 创建一个使用AES加密的块对象
block, err := aes.NewCipher(key)
if err != nil{
panic(err)
}
// 2. 最后一个分组进行数据填充
src = PKCS5Padding(src, block.BlockSize())
// 3. 创建一个分组为链接模式, 底层使用AES加密的块模型对象
blockMode := cipher.NewCBCEncrypter(block, key[:block.BlockSize()])
// 4. 加密
dst := src
blockMode.CryptBlocks(dst, src)
return dst
}
AES解密
// AES解密
func AESDecrypt(src, key []byte) []byte{
// 1. 创建一个使用AES解密的块对象
block, err := aes.NewCipher(key)
if err != nil{
panic(err)
}
// 2. 创建分组为链接模式, 底层使用AES的解密模型对象
blockMode := cipher.NewCBCDecrypter(block, key[:block.BlockSize()])
// 3. 解密
dst := src
blockMode.CryptBlocks(dst, src)
// 4. 去掉尾部填充的字
dst = PKCS5UnPadding(dst)
return dst
}
重要的函数说明
-
生成一个底层使用AES加/解密的Block接口对象
函数对应的包: import "crypto/aes" func NewCipher(key []byte) (cipher.Block, error) - 参数 key: aes对称加密使用的密码, 密码长度为128bit, 即16byte - 返回值 cipher.Block: 创建出的使用AES加/解密的Block接口对象
-
创建一个密码分组为CBC模式, 底层使用b加密的BlockMode接口对象
函数对应的包: import "crypto/cipher" func NewCBCEncrypter(b Block, iv []byte) BlockMode - 参数 b: 使用aes.NewCipher函数创建出的Block接口对象 - 参数 iv: 事先准备好的一个长度为一个分组长度的比特序列, 每个分组为64bit, 即8byte - 返回值: 得到的BlockMode接口对象
-
使用cipher包的BlockMode接口对象对数据进行加/解密
接口对应的包: import "crypto/cipher" type BlockMode interface { // 返回加密字节块的大小 BlockSize() int // 加密或解密连续的数据块,src的尺寸必须是块大小的整数倍,src和dst可指向同一内存地址 CryptBlocks(dst, src []byte) } 接口中的 CryptBlocks(dst, src []byte) 方法: - 参数 dst: 传出参数, 存储加密或解密运算之后的结果 - 参数 src: 传入参数, 需要进行加密或解密的数据切片(字符串)
-
创建一个密码分组为CBC模式, 底层使用b解密的BlockMode接口对象
函数对应的包: import "crypto/cipher" func NewCBCDecrypter(b Block, iv []byte) BlockMode - 参数 b: 使用des.NewCipher函数创建出的Block接口对象 - 参数 iv: 事先准备好的一个长度为一个分组长度的比特序列, 每个分组为128bit, 即16byte, 该序列的值需要和NewCBCEncrypter函数的第二个参数iv值相同 - 返回值: 得到的BlockMode接口对象
2.5 应选择哪种对称加密
前面我们介绍了DES、三重DES和AES等对称密码,那么我们到底应该使用哪一种对称密码算法呢?
- 今后最好不要将DES用于新的用途,因为随着计算机技术的进步,现在用暴力破解法已经能够在现实的时间内完成对DES的破译。但是,在某些情况下也需要保持与旧版本软件的兼容性。
- 出于兼容性的因素三重DES在今后还会使用一段时间,但会逐渐被AES所取代。
- 今后大家应该使用的算法是AES(Rijndael),因为它安全、快速,而且能够在各种平台上工作。此外,由于全世界的密码学家都在对AES进行不断的验证,因此即便万一发现它有什么缺陷,也会立刻告知全世界并修复这些缺陷。
一般来说,我们不应该使用任何自制的密码算法,而是应该使用AES。因为AES在其选定过程中,经过了全世界密码学家所进行的高品质的验证工作,而对于自制的密码算法则很难进行这样的验证。
本章小结
本章中我们介绍了对称密码,以及DES、三重DES、AES和其他一些密码算法。
使用一种密钥空间巨大,且在算法上没有弱点的对称密码,就可以通过密文来确保明文的机密性。巨大的密钥空间能够抵御暴力破解,算法上没有弱点可以抵御其他类型的攻击。
然而,用对称密码进行通信时,还会出现密钥的配送问题,即如何将密钥安全地发送给接收者。为了解决密钥配送问题,我们需要非对称加密技术。非对称加密,我们将在第四章进行讲解。
本章所介绍的几乎所有的密码算法,都只能将一个固定长度的分组进行加密当需要加密的明文长度超过分组长度时,就需要对密码算法进行迭代下一章我们将探讨对分组密码进行迭代的方法。
3. 分组密码的模式
"分组密码的模式 -- 分组密码是如何迭代的"
本章中我们将探讨一下分组密码的模式
我们在上一章中介绍的DES和AES都属于分组密码,它们只能加密固定长度的明文。如果需要加密任意长度的明文,就需要对分组密码进行迭代,而分组密码的迭代方法就称为分组密码的“模式”。
分组密码有很多种模式,如果模式的选择不恰当,就无法保证机密性。例如,如果使用ECB模式,明文中的一些规律就可以通过密文被识别出来。
分组密码的主要模式(ECB、CBC、CFB、OFB、CTR),最后再来考察一下到底应该使用哪一种模式。
3.1 分组密码
分组密码(blockcipher)是每次只能处理特定长度的一块数据的一类密码算法,这里的一块"就称为分组(block)。此外,一个分组的比特数就称为分组长度(blocklength)。
例如,DES和三重DES的分组长度都是64比特。这些密码算法一次只能加密64比特的明文.并生成64比特的密文。
AES的分组长度可以从128比特、192比特和256比特中进行选择。当选择128比特的分组长度时,AES一次可加密128比特的明文,并生成128比特的密文。
3.2 模式
分组密码算法只能加密固定长度的分组,但是我们需要加密的明文长度可能会超过分组密码的分组长度,这时就需要对分组密码算法进行迭代,以便将一段很长的明文全部加密。而迭代的方法就称为分组密码的模式(mode)。
话说到这里,很多读者可能会说:“如果明文很长的话,将明文分割成若干个分组再逐个加密不就好了吗?”事实上可没有那么简单。将明文分割成多个分组并逐个加密的方法称为ECB模式,这种模式具有很大的弱点(稍后讲解)。对密码不是很了解的程序员在编写加密软件时经常会使用ECB模式,但这样做会在不经意间产生安全漏洞,因此大家要记住千万不能使用ECB模式。
模式有很多种类,分组密码的主要模式有以下5种:
- ECB模式:Electronic Code Book mode(电子密码本模式)
- CBC模式:Cipher Block Chaining mode(密码分组链接模式)
- CFB模式:Cipher FeedBack mode(密文反馈模式)
- OFB模式:Output FeedBack mode(输出反馈模式)
- CTR模式:CounTeR mode(计数器模式)
明文分组和密文分组
在介绍模式之前,我们先来学习两个术语。
**明文分组: **是指分组密码算法中作为加密对象的明文。明文分组的长度与分组密码算法的分组长度是相等的。
**密文分组: **是指使用分组密码算法将明文分组加密之后所生成的密文。
为了避免图示变得复杂,以后我们将“用分组密码算法加密"简写为“加密",并省略对密钥的描述。
3.3 ECB 模式
ECB(Electronic Code Book, 电子密码本)模式是最简单的加密模式,<font color="red">明文消息被分成固定大小的块(分组),并且每个块被单独加密。</font> 每个块的加密和解密都是独立的,且使用相同的方法进行加密,所以可以进行并行计算,但是这种方法一旦有一个块被破解,使用相同的方法可以解密所有的明文数据,<font color="red">安全性比较差。 适用于数据较少的情形,加密前需要把明文数据填充到块大小的整倍数。</font>
使用ECB模式加密时,相同的明文分组会被转换为相同的密文分组,也就是说,我们可以将其理解为是一个巨大的“明文分组-->密文分组"的对应表,因此ECB模式也称为电子密码本模式当最后一个明文分组的内容小于分组长度时,需要用一特定的数据进行填充(padding),让值一个分组长度等于分组长度。
ECB模式是所有模式中最简单的一种。ECB模式中,明文分组与密文分组是一一对应的关系,因此,如果明文中存在多个相同的明文分组,则这些明文分组最终都将被转换为相同的密文分组。这样一来,只要观察一下密文,就可以知道明文中存在怎样的重复组合,并可以以此为线索来破译密码,因此ECB模式是存在一定风险的。
3.3 CBC模式
XOR
为了让大家理解比特序列运算的概念,我们来介绍一下XOR运算。XOR的全称是exclusive or,在中文里叫作异或。尽管名字看起来很复杂,但这种运算本身一点都不难。
1个比特(bit)的位运算规则如下:
如果将0理解为偶数, 1理解为奇数,就可以将XOR和一般的加法运算等同起来。
由于XOR和加法运算很相似,因此一般用+和O组合而成的符号⊕来表示XOR。
为了更加直观地理解XOR,大家可以想象一下黑白棋(奥赛罗棋)中的棋子。
将一个棋子保持原状(不翻转)看做0
将一个棋子翻转到另一面看做1
那么XOR运算就相当于将黑白棋的一个棋子进行翻转的操作。
通过上述场景,大家应该能够理解这样一个规律,<font color="red">即两个相同的数进行XOR运算的结果一定为0,因为棋子翻转两次和一次都没有翻转的结果是一样的</font>。
上面我们介绍了1个比特之间的XOR运算,而如果是长比特序列之间的运算,则只要对其中每个相对应的比特进行XOR运算就可以了。假设我们将01001100这个比特序列称为A,将10101010这个比特序列称为B,那么A与B的XOR运算就可以像下面这样逐一对各个比特进行计算。和加法运算不同的是,XOR中不需要进位。
由于两个相同的数进行XOR运算的结果一定为0,因此如果将A⊕B的结果再与B进行XOR运算,则结果会变回A。也就是说,两个公式中的B会相互抵消。
可能大家已经发现了,上面的计算和加密、解密的步骤非常相似。
将明文A用密钥B进行加密,得到密文A⊕B
将密文A⊕B用密钥B进行解密,得到明文A
实际上,只要选择一个合适的B,仅仅使用XOR就可以实现一个高强度的密码。
对同一个比特序列进行两次XOR之后就会回到最初的状态。
CBC模式
CBC(Cipher Block Chaining, 密码块链)<font color="red">模式中每一个分组要先和前一个分组加密后的数据进行XOR异或操作,然后再进行加密</font>。 这样每个密文块依赖该块之前的所有明文块,为了保持每条消息都具有唯一性,<font color="red">第一个数据块进行加密之前需要用初始化向量IV进行异或操作</font>。 <font color="blue">CBC模式是一种最常用的加密模式,它主要缺点是加密是连续的,不能并行处理,并且与ECB一样消息块必须填充到块大小的整倍数。</font>
如果将一个分组的加密过程分离出来,我们就可以很容易地比较出ECB模式和CBC模式的区别 。ECB模式只进行了加密,而CBC模式则在加密之前进行了一次XOR。
初始化向量
当加密第一个明文分组时,由于不存在“前一个密文分组",因此<font color="red">需要事先准备一个长度为一个分组的比特序列来代替“前一个密文分组</font>",这个比特序列称为初始化向量(initialization vector)
通常缩写为 IV 一般来说,每次加密时都会随机产生一个不同的比特序列来作为初始化向量。
明文分组在加密之前一定会与“前一个密文分组"进行 XOR 运算,因此即便明文分组1和2的值是相等的,密文分组1和2的值也不一定是相等的。这样一来,ECB模式的缺陷在CBC模式中就不存在了。
3.4 CFB 模式
CFB模式的全称是Cipher FeedBack模式(密文反馈模式)。在CFB模式中,<font color="red">前一个分组的密文加密后和当前分组的明文XOR异或操作生成当前分组的密文</font>。
所谓反馈,这里指的就是返回输人端的意思,即前一个密文分组会被送回到密码算法的输入端。
CFB模式的解密和CBC模式的加密在流程上其实是非常相似的。
在ECB模式和CBC模式中,明文分组都是通过密码算法进行加密的,然而,在CFB模式中,明文分组并没有通过密码算法来直接进行加密。
从上图可以看出,明文分组和密文分组之间并没有经过"加密"这一步骤。在CFB模式中,明文分和密文分组之间只有一个XOR。
我们将CBC模式与CFB模式对比一下,就可以看出其中的差异了(如下图)。在CBC模式中,明文分组和密文分组之间有XOR和密码算法两个步骤,而在CFB模式中,明文分组和密文分组之间则只有XOR。
初始化向量
<font color="red">在生成第一个密文分组时,由于不存在前一个输出的数据,因此需要使用初始化向量(IV)来代替</font>,这一点和CBC模式是相同的。一般来说,我们需要在每次加密时生成一个不同的随机比特序列用作初始化向量。
CFB模式与流密码
CFB模式是通过将“明文分组”与“密码算法的输出"进行XOR运算来生成“密文分组”的。
在CFB模式中,密码算法的输出相当于一个随机比特序列。由于密码算法的输出是通过计算得到的,并不是真正的随机数,因此CFB模式不可能具各理论上不可破译的性质。
<font color="red">CFB模式中由密算法所生成的比特序列称为密钥流(key stream)</font>。在CFB模式中,密码算法就相当于用来生成密钥流的伪随机数生成器,而初始化向量相当于伪随机数生成器的“种子“。
在CFB模式中,<font color="red">明文数据可以被逐比特加密</font>,因此我们<font color="red">可以将CFB模式看做是一种使用分组密码来实现流密码的方式</font>。
3.5 OFB 模式
OFB式的全称是Output-Feedback模式(输出反馈模式)。在OFB模式中,密码算法的输出会反馈到密码算法的输入中, 即上一个分组密码算法的输出是当前分组密码算法的输入(下图)。
OFB模式并不是通过密码算法对明文直接进行加密的,而是通过将 “明文分组" 和 “密码算法的输出” 进行XOR来产生 “密文分组” 的,在这一点上OFB模式和CFB模式非常相似。
初始化向量
和CBC模式、CFB模式一样,OFB模式中也需要使用初始化向量(IV)。一般来说,我们需要在每次加密时生成一个不同的随机比特序列用作初始化向量。
CFB模式和OFB模式对比
OFB模式和CFB模式的区别仅仅在于密码算法的输入。
CFB式中,密码算法的输人是前一个密文分组,也就是将密文分组反馈到密算法中,因此就有了“密文反馈模式”这个名字。
相对地,OFB模式中,密码算法的输入则是密码算法的前一个输出,也就是将输出反馈给密码算法,因此就有了“输出反馈模式"这个名字。
如果将一个分组抽出来对CFB模式和OFB模式进行一个对比.就可以很容易看出它们之间的差异(下图)。
由于CFB模式中是对密文分组进行反馈的,因此必须从第一个明文分组开始按顺序进行加密,也就是说无法跳过明文分组1而先对明文分组2进行加密。
相对地,在OFB模式中,XOR所需要的比特序列(密钥流)可以事先通过密码算法生成,和明文分组无关。只要提前准备好所需的密钥流,则在实际从明文生成密文的过程中,就完全不需要动用密码算法了。只要将明文与密钥流进行XOR就可以了。和AES等密码算法相比,XOR运算的速度是非常快的。这就意味着只要提前准备好密钥流就可以快速完成加密。换个角度来看,生成密钥流的操作和进行XOR运算的操作是可以并行的。
3.6 CTR 模式
CTR模式的全称是CounTeR模式(计数器模式)。<font color="red">CTR摸式是一种通过将逐次累加的计数器进行加密来生成密钥流的流密码</font>(下图)。
CTR模式中,每个分组对应一个逐次累加的计数器,并通过对计数器进行加密来生成密钥流。也就是说,最终的密文分组是通过将计数器加密得到的比特序列,与明文分组进行XOR而得到的。
计数器的生成方法
每次加密时都会生成一个不同的值(nonce)来作为计数器的初始值。当分组长度为128比特(16字节)时,计数器的初始值可能是像下面这样的形式。
其中前8个字节为nonce(随机数),这个值在每次加密时必须都是不同的,后8个字节为分组序号,这个部分是会逐次累加的。在加密的过程中,计数器的值会产生如下变化:
按照上述生成方法,可以保证计数器的值每次都不同。由于计数器的值每次都不同,因此每个分组中将计数器进行加密所得到的密钥流也是不同的。也是说,这种方法就是用分组密码来模拟生成随机的比特序列。
OFB模式与CTR模式对比
CTR模式和OFB模式一样,都属于流密码。如果我们将单个分组的加密过程拿出来,那么OFB模式和CTR模式之间的差异还是很容易理解的(下图)。OFB模式是将加密的输出反愦到输入,而CTR模式则是将计数器的值用作输入。
1538712242324.png
CTR模式的特点
CTR模式的加密和解密使用了完全相同的结构,因此在程序实现上比较容易。这一特点和同为流密码的OFB模式是一样的。
此外,CTR模式中可以以任意顺序对分组进行加密和解密,因此在加密和解密时需要用到的“计数器"的值可以由nonce和分组序号直接计算出来。这一性质是OFB模式所不具备的。
能够以任意顺序处理分组,就意味着能够实现并行计算。在支持并行计算的系统中,CTR模式的速度是非常快的。
3.7 总结
我们已经介绍了ECB、CBC、CFB、OFB和CTR模式,下面我们对这些模式的特点做一下整理。