本小结的密码学术语如填充,模式,等等,均以AES为例进行说明。举一反三,其他算法涉及的相同术语,大致也就理解是怎么回事了,,,吧。
https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
AES标准是神马鬼东西,USA的PDF写的非常详尽,在此个人用 ‘小白话术 + Python环境’ 理解一下。
AES算法,大致上:就是将一段16字节的数据(不妨将这一段数据取个代号叫 input),在另一段16字节(或24字节,或32字节)数据的辅助下(不妨叫 key),两者经过一系列骚操作,变成一段和原来不一样的,长度依然是16字节的数据(不妨叫 output)的过程,以及将output在同一个key的辅助下,经过一系列骚操作,又可以变回原来的input的过程:
加密:aes-encrypt(input,key)=>output
解密:aes-decrypt(output,key)=>input
关键词:AES-128,AES-192,AES-256 就是上述 key 的长度可以是16字节,24字节,32字节。有啥区别?越大越安全。
像AES算法这样,加密解密使用同一个key,大致即所谓’对称加密‘。’非对称加密‘,则使用不同的key:(input,key1)=>output,(output,key2)=>input
AES加密步骤:(演示动画:https://formaestudio.com/rijndaelinspector/archivos/Rijndael_Animation_v4_eng-html5.html):
(0)一排摆放16个元素与矩阵摆放元素位置关系
一排摆放:⓿①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮
矩阵摆放ㅤ0 1 2 3 c
ㅤㅤㅤㅤ0 ⓿ ④ ⑧ ⑫
ㅤㅤㅤㅤ1 ① ⑤ ⑨ ⑬
ㅤㅤㅤㅤ2 ② ⑥ ⑩ ⑭
ㅤㅤㅤㅤ3 ③ ⑦ ⑪ ⑮
ㅤㅤㅤㅤr
20个 4行x5列,24个 4行x6列 …依此类推。
矩阵摆放转回一排摆放,同理类推。
一排摆放:⓿①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮ 视作列表,则:
矩阵坐标(r,c),列表索引:r+4*c。
列表索引i,矩阵坐标:(i取余4,i取模4)。
比如
input:32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34(字节hex形式)
key: 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c
矩阵摆放形式
`input`ㅤㅤㅤㅤㅤ`Key`
32 88 31 e0ㅤㅤㅤ2b 28 ab 09
43 5a 31 37ㅤㅤㅤ7e ae f7 cf
f6 30 98 07ㅤㅤㅤ15 d2 15 4f
a8 8d a2 34ㅤㅤㅤ16 a6 88 3c
(1)对于 Key
矩阵,pdf说:
AES-128(本身 4行x4列),用它来独立生产一些列(与Input无关)。
第5-8列,通过1-4列某些列产生…依次类推。
AES-192(本身 4行x6列),用它来独立生产一些列(与Input无关)。
第7-12列,通过1-6列某些列产生…依次类推。
AES-256(本身 4行x8列),用它来独立生产一些列(与Input无关)。
第9-16列,通过1-8列某些列产生…依次类推。
于是,本身的列加上生产的列就有很多列 ,从第1列开始,每4列组装出一个4行x4列矩阵,不妨命名:K₀,K₁,K₂…
(2)对于input
矩阵,pdf说:
I₀=input
I₁=ᵐⁱˣ(ˢʰⁱ(ˢᵘᵇ(ˣᵒʳ(I₀,K₀))))
...
Iᵣ=ᵐⁱˣ(ˢʰⁱ(ˢᵘᵇ(ˣᵒʳ(Iᵣ₋₁,Kᵣ₋₁))))
比如
input:32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34(字节hex形式)
key: 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c
`input`ㅤㅤㅤㅤㅤ`Key`
32 88 31 e0ㅤㅤㅤ2b 28 ab 09
43 5a 31 37ㅤㅤㅤ7e ae f7 cf
f6 30 98 07ㅤㅤㅤ 15 d2 15 4f
a8 8d a2 34ㅤㅤㅤ16 a6 88 3c
则:I₀= `input`, K₀=`Key` ,I₀,K₀变换出I₁的过程
32 88 31 e0
I₀ 43 5a 31 37 I₁
f6 30 98 07 2b 28 ab 09 d4 e0 b8 1e d4 e0 b8 1e 04 e0 48 28
a8 8d a2 34 7e ae f7 cf ˢᵘᵇ 27 bf b4 41 ˢʰⁱ bf b4 41 27 ᵐⁱˣ 66 cb f8 06
ˣᵒʳ 15 d2 15 4f 11 98 5d 52 5d 52 11 98 81 19 d3 26
19 a0 9a e9 16 a6 88 3c ae f1 e5 30 30 ae f1 e5 e5 9a 7a 4c
K₀ 3d f4 c6 f8
e3 e2 8d 48
be 2b 2a 08
(3) Key
生产组装何时停止?input
Key
联合变换何时停止?pdf说:
AES-128得做10轮,第10轮不做ᵐⁱˣ,output=ˣᵒʳ(I₁₀,K₁₀)。因此,需要用至K₁₀,即Key
工厂中要生产11个4x4矩阵。11个4x4矩阵共计44列,本身4x4,有4列,所以,得生产40列。
AES-192得做12轮,第12轮不做ᵐⁱˣ,output=ˣᵒʳ(I₁₂,K₁₂)。因此,需要用至K₁₂,即Key
工厂要生产13个4x4矩阵。13个4x4矩阵共计52列,本身4x6,有6列,所以,得生产46列。
AES-256得做14轮,第14轮不做ᵐⁱˣ,output=ˣᵒʳ(I₁₄,K₁₄)。因此,需要用至K₁₄,即Key
工厂中要生产15个4x4矩阵。15个4x4矩阵共计60列,本身4x8,有8列,所以,得生产52列。
例如:AES-256
`Key`生产组装
{本身8列} {生产52列}
|||||||| + ||||||||||||||||||||||||||||||||||||||||||||||||||||
{组装K₀}{组装K₁} {组装Kx...} {组装K₁₄}
|||| |||| ................................................ ||||
得到矩阵:K₀,K₁,K₂...K₁₄
`input`联合变换
I₀=input
I₁=ᵐⁱˣ(ˢʰⁱ(ˢᵘᵇ(ˣᵒʳ(I₀,K₀))))
...
I₁₃=ᵐⁱˣ(ˢʰⁱ(ˢᵘᵇ(ˣᵒʳ(I₁₂,K₁₂))))
I₁₄= ˢʰⁱ(ˢᵘᵇ(ˣᵒʳ(I₁₃,K₁₃)))
出货
output=ˣᵒʳ(I₁₄,K₁₄)
填充,似乎就是指:将额外的数据添加到目标数据中任何位置满足某种需求,显然有无数种方式。
为什么要填充?
1.可能有长度需求。
比如,‘我爱你’是一段爱的数据,可是,现在女朋友对爱有长度要求,要7个字。那么,就要额外填充4个字。4个字可以加到后面’我爱你一生一世’,可以加到前面’一生一世我爱你’,可以各种混插’我想好好的爱你’,等等
2.可能有保密需求。
填充额外的数据,本身也是一种简单加密。比如,藏头诗,真正的信息只有头字,其他都是填充的’废话’。解密时,以句为单位,取每句第一个字,依次拼接。显然,可以各种藏头藏尾藏xx,只要解密时按既定规则能准确剥离废话。
《剑来》中国师为了给陈平安捎一句话,额外填充了一整本书的数据。陈平安没有事先知晓国师的填充规则,最后,还是成功剥离废话解密,敌人也做到了。
这不仅仅是小说情节,说明填充加密确实很弱鸡,因此,现代没有人这么干,而是依托数学,计算机开发出了很多专业加密/解密算法。
言归正传,aes加密解密算法中输入数据项必须16字节。显然,待加密数据字节数,不可能恰好16字节,怎么办?凑成16字节。以字节为最小单元的情况下,大佬们规定了很多的标准、规范之类的东西。
比如"我爱你",utf-8编码后,9个字节:
b’\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0’
ANSI X9.23 这么干:16-9差7个字节,先上6个\x00,最后在上一个表示填充了7个字节\x07:
b’\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0 + b’\x00’*6 + b’\x07’
ISO 10126 :16-9差7个,先上6个随机字节,最后在上一个表示填充了7个字节\x07:
b’\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0 + 随机字节6个 + b’\x07’
ISO / IEC 7816-4 :16-9差7个,第10个字节放\x80,后面6个\x00:
b’\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0 + b’\x80’ + b’\x00’*6
Zero padding 这么干:16-9差7个,放7个\x00完事。
b’\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0 + b’\x00’*7
PKCS#7(PKCS#5)这么干:16-9差7个,就放7个’\x07’。(如果差1个放1个’\x01’…依次类推,差15个放15个’\x0f’)
b’\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0 + b’\x07’*7
对于待填充数据刚好16倍数字节的情况,则在放一个16字节:
待填充数据(刚好16倍数字节) + b’\x10’*16
这些标准规范啥的,不仅仅是规定了’怎么填‘,也规定了‘怎么去’,可以说规定’怎么填‘的同时就考虑了‘怎么去‘,两者相辅相成。但是,如果我们自身对原始数据没有认识,胡乱套用标准’填充‘,"去填充"就会丢失原始数据。
比如,汉字’一’,采用UTF-16BE编码为:b’\x41\x00’,如果采用Zero padding填充’:b’\x41\x00’+b’\x00’*14。好了,因为Zero padding,那么去填充时也用Zero padding规定的原则,去掉所有\x00。得到:b’\x41。好了,UTF-16BE编码,那么用UTF-16BE解码得到字母:’A‘ 。
所以,要对原始待填充数据中的每个字节可能存在值有清晰的认识,尤其最后一个分的块的最后一个字节可能存在的值。清楚这些后,完全可以自定义填充,也不一定是末尾添加,便怎么凑都行,随便多少字节都行,只要"去填充"时不丢字节。结论:PKCS#7是完美的,面对任意二进制数据都不会懵逼,几乎就是"填充"的代名词。
具体到利用aes加密解密:
原始数据=>填充=>填充后的数据(16倍数长度)=>
aes-encrypt(填充后的数据,key)=> 密文=>aes-decrypt(密文,key)
=>填充后的数据(16倍数长度)=>去填充=>原始数据
还是以aes为例,比如,现在待加密数据:38字节,咋整?填充。将38字节,填充成16的倍数,一般就是填充成离待加密数据长度最近的那个16的倍数,这里是填充成48字节。当然,’填充‘成64字节,80字节,96字节,等也没毛病,但没必要。
现在’填充后的数据’:48字节。问题又来了,aes加密解密算法中输入数据项必须16字节,咱们这弄成了48字节,它也不是16字节啊,咋整?我们最容易想到的可能就是:不是48字节吗?那就每16字节一份,分成3份,分别加密,在组合在一起:
output=aes-encrypt(data[0:16],key) + aes-encrypt(data[16:24],key) + aes-encrypt(data[24:48],key)
可以吗?可以。这就是所谓"块加密模式之ECB模式"。但是,专家们说并不安全。但是,安全也是相对的。这种模式好处很明显,分块后,各块独立加密,可以并行执行,加密速度快啊。
不管怎样,还有一种叫"块加密模式之ECB模式"的做法。具体流程:
1、48字节还是分成3组:[data[:16],data[16:24],data[24:]]
2、额外引入一个16字节参数(不妨叫iv,所谓“初始化向量”)
3、最后处理流程是这样的:
R₀=iv
R₁=AES(R₀ ^ L₀,key)
R₂=AES(R₁ ^ L₁,key)
R₃=AES(R₂ ^ L₂,key)
output=R₁+R₂+R₃
显然,因为加密时多加入了一个参数iv,解密时也必须有这个iv。有时候,咱们可能觉得解密号称AES-CBC模式加密的密文时,并没有用iv,还有遇到过可以iv=‘0’,iv=‘\0’,iv=’‘的奇葩库,这只能说加密时偷懒搞了iv=’\x00‘*16,库也挺‘牛逼’,遇到不合法的iv值,盲猜了一波iv=’\x00‘*16。
"模式"不是AES特有的,其他"分块(组)加密模式/块(组)加密模式"大同小异。算法本身没变(input和key的要求,算法过程)变的只是前前后后可能增加了额外的参数或者额外的步骤来处理数据,使得最终加密出来的结果理论上安全。
python主要两个轮子,各有侧重:
(1)cryptography
opensll 壳子,大致 opensll 的效率,适合熟悉密码学概念的人士。
https://github.com/pyca/cryptography
https://cryptography.io/en/latest/
(2)pycryptodome
纯python(影响效率处c扩展),函数基本按密码学概念来的,适合学习。
pip install pycryptodomex (安装成Cryptodome package,推荐)
pip install pycryptodome(安装成兼容老pycrypto的Crypto package)
https://github.com/Legrandin/pycryptodome
https://www.pycryptodome.org/en/latest/index.html
***(3)pycrypto
过时的,但是N多python加密解密相关老文章使用的,,,
#https://github.com/dlitz/pycrypto
#https://www.dlitz.net/software/pycrypto/
# -*- coding=utf-8 -*-
# -*- coding=utf-8 -*-
"""
照搬pdf伪码的python代码,仅供理解,不存在效率。
MixColumns函数涉及的GF2^8域数学范畴不做细究,直接查表。
aes现在已经是cpu指令级别的支持,所以,咱们用成熟的轮子即可。
"""
def MYAES(Input,key):
AES_NkNbNr={
16:(4,4,10),24:(6,4,12),32:(8,4,14)} #(Nk,Nb,Nr)
AES_SBOX={
0x00: 0x63, 0x01: 0x7c, 0x02: 0x77, 0x03: 0x7b, 0x04: 0xf2, 0x05: 0x6b, 0x06: 0x6f, 0x07: 0xc5, 0x08: 0x30, 0x09: 0x01, 0x0a: 0x67, 0x0b: 0x2b, 0x0c: 0xfe, 0x0d: 0xd7, 0x0e: 0xab, 0x0f: 0x76,
0x10: 0xca, 0x11: 0x82, 0x12: 0xc9, 0x13: 0x7d, 0x14: 0xfa, 0x15: 0x59, 0x16: 0x47, 0x17: 0xf0, 0x18: 0xad, 0x19: 0xd4, 0x1a: 0xa2, 0x1b: 0xaf, 0x1c: 0x9c, 0x1d: 0xa4, 0x1e: 0x72, 0x1f: 0xc0,
0x20: 0xb7, 0x21: 0xfd, 0x22: 0x93, 0x23: 0x26, 0x24: 0x36, 0x25: 0x3f, 0x26: 0xf7, 0x27: 0xcc, 0x28: 0x34, 0x29: 0xa5, 0x2a: 0xe5, 0x2b: 0xf1