1. 前言
AES是一種對稱加密,所謂對稱加密就是加密與解密使用的秘鑰是一個。
之前寫過一片關於python AES加密解密的文章,但是這里面細節實在很多,這次我從 參數類型、加密模式、編碼模式、補全模式、等等方面 系統的說明如何使用AES加密解密。
看文章不能急功近利,為了解決一個問題臨時查到一個代碼套用進去,或許可以迅速解決問題,但是遇到新的問題還需要再次查詢,這種我認為還是比較浪費時間的。我相信看完認真看完這篇文章的你會大有收獲。
2. 環境安裝
pip uninstall crypto
pip uninstall pycryptodome
pip install pycryptodome
前面兩個卸載命令是為了防止一些安裝環境問題,具體請看文章
3.加密模式
AES 加密最常用的模式就是 ECB模式 和 CBC 模式,當然還有很多其它模式,他們都屬於AES加密。ECB模式和CBC 模式倆者區別就是 ECB 不需要 iv偏移量,而CBC需要。
4.AES加密使用參數
以下參數都是在python中使用的。
參數 | 作用及數據類型 |
---|---|
秘鑰 | 加密的時候用秘鑰,解密的時候需要同樣的秘鑰才能解出來; 數據類型為bytes |
明文 | 需要加密的參數; 數據類型為bytes |
模式 | aes 加密常用的有 ECB 和 CBC 模式(我只用了這兩個模式,還有其他模式);數據類型為aes類內部的枚舉量 |
iv 偏移量 | 這個參數在 ECB 模式下不需要,在 CBC 模式下需要;數據類型為bytes |
下面簡單的一個例子ECB模式加密解密 :
from Crypto.Cipher import AES
password = b'1234567812345678' #秘鑰,b就是表示為bytes類型
text = b'abcdefghijklmnhi' #需要加密的內容,bytes類型
aes = AES.new(password,AES.MODE_ECB) #創建一個aes對象
# AES.MODE_ECB 表示模式是ECB模式
en_text = aes.encrypt(text) #加密明文
print("密文:",en_text) #加密明文,bytes類型
den_text = aes.decrypt(en_text) # 解密密文
print("明文:",den_text)
輸出:
密文: b'WU\xe0\x0e\xa3\x87\x12\x95\\]O\xd7\xe3\xd4 )'
明文: b'abcdefghijklmnhi'
以上是針對ECB模式的加密解密,從這個例子中可以看出參數中有幾個限制。
- 秘鑰必須為16字節或者16字節的倍數的字節型數據。
- 明文必須為16字節或者16字節的倍數的字節型數據,如果不夠16字節需要進行補全,關於補全規則,后面會在補全模式中具體介紹。
通過CBC模式例子:
from Crypto.Cipher import AES
password = b'1234567812345678' #秘鑰,b就是表示為bytes類型
iv = b'1234567812345678' # iv偏移量,bytes類型
text = b'abcdefghijklmnhi' #需要加密的內容,bytes類型
aes = AES.new(password,AES.MODE_CBC,iv) #創建一個aes對象
# AES.MODE_CBC 表示模式是CBC模式
en_text = aes.encrypt(text)
print("密文:",en_text) #加密明文,bytes類型
aes = AES.new(password,AES.MODE_CBC,iv) #CBC模式下解密需要重新創建一個aes對象
den_text = aes.decrypt(en_text)
print("明文:",den_text)
輸出:
密文: b'\x93\x8bN!\xe7~>\xb0M\xba\x91\xab74;0'
明文: b'abcdefghijklmnhi'
通過上面CBC模式的例子,可以簡單看出CBC模式與ECB模式的區別:AES.new() 解密和加密重新生成了aes對象,加密和解密不能調用同一個aes對象,否則會報錯TypeError: decrypt() cannot be called after encrypt()
。
總結:
1. 在Python中進行AES加密解密時,所傳入的密文、明文、秘鑰、iv偏移量、都需要是bytes(字節型)數據。python 在構建aes對象時也只能接受bytes類型數據。
2.當秘鑰,iv偏移量,待加密的明文,字節長度不夠16字節或者16字節倍數的時候需要進行補全。
3. CBC模式需要重新生成AES對象,為了防止這類錯誤,我寫代碼無論是什么模式都重新生成AES對象。
5. 編碼模式
前面說了,python中的 AES 加密解密,只能接受字節型(bytes)數據。而我們常見的 待加密的明文可能是中文
,或者待解密的密文經過base64編碼的
,這種都需要先進行編碼或者解碼,然后才能用AES進行加密或解密。反正無論是什么情況,在python使用AES進行加密或者解密時,都需要先轉換成bytes型數據。
我們以ECB模式針對中文明文進行加密解密舉例:
from Crypto.Cipher import AES
password = b'1234567812345678' #秘鑰,b就是表示為bytes類型
text = "好好學習天天向上".encode('gbk') #gbk編碼,是1個中文字符對應2個字節,8個中文正好16字節
aes = AES.new(password,AES.MODE_ECB) #創建一個aes對象
# AES.MODE_ECB 表示模式是ECB模式
print(len(text))
en_text = aes.encrypt(text) #加密明文
print("密文:",en_text) #加密明文,bytes類型
den_text = aes.decrypt(en_text) # 解密密文
print("明文:",den_text.decode("gbk")) # 解密后同樣需要進行解碼
輸出:
16
密文: b'=\xdd8k\x86\xed\xec\x17\x1f\xf7\xb2\x84~\x02\xc6C'
明文: 好好學習天天向上
對於中文明文,我們可以使用encode()
函數進行編碼,將字符串轉換成bytes類型數據,而這里我選擇gbk編碼,是為了正好能滿足16字節,utf8編碼是一個中文字符對應3個字節。這里為了舉例所以才選擇使用gbk編碼。
在解密后,同樣是需要decode()
函數進行解碼的,將字節型數據轉換回中文字符(字符串類型)。
現在我們來看另外一種情況,密文是經過base64編碼的(這種也是非常常見的,很多網站也是這樣使用的),我們用 http://tool.chacuo.net/cryptaes/ 這個網站舉例子:
模式:ECB
密碼: 1234567812345678
字符集:gbk編碼
輸出: base64
我們來寫一個python 進行aes解密:
from Crypto.Cipher import AES
import base64
password = b'1234567812345678'
aes = AES.new(password,AES.MODE_ECB)
en_text = b"Pd04a4bt7Bcf97KEfgLGQw=="
en_text = base64.decodebytes(en_text) #將進行base64解碼,返回值依然是bytes
den_text = aes.decrypt(en_text)
print("明文:",den_text.decode("gbk"))
輸出:
明文: 好好學習天天向上
這里的 b"Pd04a4bt7Bcf97KEfgLGQw=="
是一個bytes數據, 如果你傳遞的是一個字符串,你可以直接使用 encode()
函數 將其轉換為 bytes類型數據。
from Crypto.Cipher import AES
import base64
password = b'1234567812345678'
aes = AES.new(password,AES.MODE_ECB)
en_text = "Pd04a4bt7Bcf97KEfgLGQw==".encode() #將字符串轉換成bytes數據
en_text = base64.decodebytes(en_text) #將進行base64解碼,參數為bytes數據,返回值依然是bytes
den_text = aes.decrypt(en_text)
print("明文:",den_text.decode("gbk"))
因為無論是 utf8
和 gbk
編碼,針對英文字符編碼都是一個字符對應一個字節,所以這里encode()函數主要作用就是轉換成bytes數據,然后使用base64進行解碼。
hexstr,base64編碼解碼例子:
import base64
import binascii
data = "hello".encode()
data = base64.b64encode(data)
print("base64編碼:",data)
data = base64.b64decode(data)
print("base64解碼:",data)
data = binascii.b2a_hex(data)
print("hexstr編碼:",data)
data = binascii.a2b_hex(data)
print("hexstr解碼:",data)
輸出:
base64編碼: b'aGVsbG8='
base64解碼: b'hello'
hexstr編碼: b'68656c6c6f'
hexstr解碼: b'hello'
這里要說明一下,有一些AES加密,所用的秘鑰,或者IV向量是通過 base64編碼或者 hexstr編碼后的。針對這種,首先要進行的就是進行解碼,都轉換回 bytes數據,再次強調,python實現 AES加密解密傳遞的參數都是 bytes(字節型) 數據。
另外,我記得之前的 pycryptodome
庫,傳遞IV向量時,和明文時可以直接使用字符串類型數據,不過現在新的版本都必須為 字節型數據了,可能是為了統一好記。
6. 填充模式
前面我使用秘鑰,還有明文,包括IV向量,都是固定16字節,也就是數據塊對齊了。而填充模式就是為了解決數據塊不對齊的問題,使用什么字符進行填充就對應着不同的填充模式
AES補全模式常見有以下幾種:
模式 | 意義 |
---|---|
ZeroPadding | 用b'\x00'進行填充,這里的0可不是字符串0,而是字節型數據的b'\x00' |
PKCS7Padding | 當需要N個數據才能對齊時,填充字節型數據為N、並且填充N個 |
PKCS5Padding | 與PKCS7Padding相同,在AES加密解密填充方面我沒感到什么區別 |
no padding | 當為16字節數據時候,可以不進行填充,而不夠16字節數據時同ZeroPadding一樣 |
這里有一個細節問題,我發現很多文章說的也是不對的。
ZeroPadding填充模式的意義:很多文章解釋是當為16字節倍數時就不填充,然后當不夠16字節倍數時再用字節數據0填充,這個解釋是不對的,這解釋應該是no padding的,而ZeroPadding是不管數據是否對其,都進行填充,直到填充到下一次對齊為止,也就是說即使你夠了16字節數據,它會繼續填充16字節的0,然后一共數據就是32字節。
這里可能會有一個疑問,為什么是16字節 ,其實這個是 數據塊的大小,網站上也有對應設置,網站上對應的叫128位,也就是16字節對齊,當然也有192位(24字節),256位(32字節)。
本文在這個解釋之后,后面就說數據塊對齊問題了,而不會再說16字節倍數了。
除了no padding 填充模式,剩下的填充模式都會填充到下一次數據塊對齊為止,而不會出現不填充的問題。
PKCS7Padding和 PKCS5Padding需要填充字節對應表:
明文長度值(mod 16) | 添加的填充字節數 | 每個填充字節的值 |
---|---|---|
0 | 16 | 0x10 |
1 | 15 | 0x0F |
2 | 14 | 0x0E |
3 | 13 | 0x0D |
4 | 12 | 0x0C |
5 | 11 | 0x0B |
6 | 10 | 0x0A |
7 | 9 | 0x09 |
8 | 8 | 0x08 |
9 | 7 | 0x07 |
10 | 6 | 0x06 |
11 | 5 | 0x05 |
12 | 4 | 0x04 |
13 | 3 | 0x03 |
14 | 2 | 0x02 |
15 | 1 | 0x01 |
這里可以看到,當明文長度值已經對齊時(mod 16 = 0),還是需要進行填充,並且填充16個字節值為0x10。ZeroPadding填充邏輯也是類似的,只不過填充的字節值都為0x00,在python表示成 b'\x00'
。
填充完畢后,就可以使用 AES進行加密解密了,當然解密后,也需要剔除填充的數據,無奈Python這些步驟需要自己實現(如果有這樣的庫還請評論指出)。
7.python的完整實現
from Crypto.Cipher import AES
import base64
import binascii
# 數據類
class MData():
def __init__(self, data = b"",characterSet='utf-8'):
# data肯定為bytes
self.data = data
self.characterSet = characterSet
def saveData(self,FileName):
with open(FileName,'wb') as f:
f.write(self.data)
def fromString(self,data):
self.data = data.encode(self.characterSet)
return self.data
def fromBase64(self,data):
self.data = base64.b64decode(data.encode(self.characterSet))
return self.data
def fromHexStr(self,data):
self.data = binascii.a2b_hex(data)
return self.data
def toString(self):
return self.data.decode(self.characterSet)
def toBase64(self):
return base64.b64encode(self.data).decode()
def toHexStr(self):
return binascii.b2a_hex(self.data).decode()
def toBytes(self):
return self.data
def __str__(self):
try:
return self.toString()
except Exception:
return self.toBase64()
### 封裝類
class AEScryptor():
def __init__(self,key,mode,iv = '',paddingMode= "NoPadding",characterSet ="utf-8"):
'''
構建一個AES對象
key: 秘鑰,字節型數據
mode: 使用模式,只提供兩種,AES.MODE_CBC, AES.MODE_ECB
iv: iv偏移量,字節型數據
paddingMode: 填充模式,默認為NoPadding, 可選NoPadding,ZeroPadding,PKCS5Padding,PKCS7Padding
characterSet: 字符集編碼
'''
self.key = key
self.mode = mode
self.iv = iv
self.characterSet = characterSet
self.paddingMode = paddingMode
self.data = ""
def __ZeroPadding(self,data):
data += b'\x00'
while len(data) % 16 != 0:
data += b'\x00'
return data
def __StripZeroPadding(self,data):
data = data[:-1]
while len(data) % 16 != 0:
data = data.rstrip(b'\x00')
if data[-1] != b"\x00":
break
return data
def __PKCS5_7Padding(self,data):
needSize = 16-len(data) % 16
if needSize == 0:
needSize = 16
return data + needSize.to_bytes(1,'little')*needSize
def __StripPKCS5_7Padding(self,data):
paddingSize = data[-1]
return data.rstrip(paddingSize.to_bytes(1,'little'))
def __paddingData(self,data):
if self.paddingMode == "NoPadding":
if len(data) % 16 == 0:
return data
else:
return self.__ZeroPadding(data)
elif self.paddingMode == "ZeroPadding":
return self.__ZeroPadding(data)
elif self.paddingMode == "PKCS5Padding" or self.paddingMode == "PKCS7Padding":
return self.__PKCS5_7Padding(data)
else:
print("不支持Padding")
def __stripPaddingData(self,data):
if self.paddingMode == "NoPadding":
return self.__StripZeroPadding(data)
elif self.paddingMode == "ZeroPadding":
return self.__StripZeroPadding(data)
elif self.paddingMode == "PKCS5Padding" or self.paddingMode == "PKCS7Padding":
return self.__StripPKCS5_7Padding(data)
else:
print("不支持Padding")
def setCharacterSet(self,characterSet):
'''
設置字符集編碼
characterSet: 字符集編碼
'''
self.characterSet = characterSet
def setPaddingMode(self,mode):
'''
設置填充模式
mode: 可選NoPadding,ZeroPadding,PKCS5Padding,PKCS7Padding
'''
self.paddingMode = mode
def decryptFromBase64(self,entext):
'''
從base64編碼字符串編碼進行AES解密
entext: 數據類型str
'''
mData = MData(characterSet=self.characterSet)
self.data = mData.fromBase64(entext)
return self.__decrypt()
def decryptFromHexStr(self,entext):
'''
從hexstr編碼字符串編碼進行AES解密
entext: 數據類型str
'''
mData = MData(characterSet=self.characterSet)
self.data = mData.fromHexStr(entext)
return self.__decrypt()
def decryptFromString(self,entext):
'''
從字符串進行AES解密
entext: 數據類型str
'''
mData = MData(characterSet=self.characterSet)
self.data = mData.fromString(entext)
return self.__decrypt()
def decryptFromBytes(self,entext):
'''
從二進制進行AES解密
entext: 數據類型bytes
'''
self.data = entext
return self.__decrypt()
def encryptFromString(self,data):
'''
對字符串進行AES加密
data: 待加密字符串,數據類型為str
'''
self.data = data.encode(self.characterSet)
return self.__encrypt()
def __encrypt(self):
if self.mode == AES.MODE_CBC:
aes = AES.new(self.key,self.mode,self.iv)
elif self.mode == AES.MODE_ECB:
aes = AES.new(self.key,self.mode)
else:
print("不支持這種模式")
return
data = self.__paddingData(self.data)
enData = aes.encrypt(data)
return MData(enData)
def __decrypt(self):
if self.mode == AES.MODE_CBC:
aes = AES.new(self.key,self.mode,self.iv)
elif self.mode == AES.MODE_ECB:
aes = AES.new(self.key,self.mode)
else:
print("不支持這種模式")
return
data = aes.decrypt(self.data)
mData = MData(self.__stripPaddingData(data),characterSet=self.characterSet)
return mData
if __name__ == '__main__':
key = b"1234567812345678"
iv = b"0000000000000000"
aes = AEScryptor(key,AES.MODE_CBC,iv,paddingMode= "ZeroPadding",characterSet='utf-8')
data = "好好學習"
rData = aes.encryptFromString(data)
print("密文:",rData.toBase64())
rData = aes.decryptFromBase64(rData.toBase64())
print("明文:",rData)
我簡單的對其進行了封裝,加密和解密返回的數據類型可以使用toBase64()
,toHexStr()
進行編碼。另外我沒有對key和iv進行補全,可以使用MData類自己實現,更多詳細使用可以通過源碼中注釋了解。