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?模式下需要;數據類型為byte |
下面簡單的一個例子ECB模式加密解密 :
from Crypto.Cipher import AESpassword = 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 AESpassword = 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 base64password = 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 base64password = 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 | 0x08 |
10 | 6 | 0x07 |
11 | 5 | 0x06 |
12 | 4 | 0x05 |
13 | 3 | 0x04 |
14 | 2 | 0x03 |
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肯定為bytesself.data = dataself.characterSet = characterSetdef 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.datadef fromBase64(self,data):self.data = base64.b64decode(data.encode(self.characterSet))return self.datadef fromHexStr(self,data):self.data = binascii.a2b_hex(data)return self.datadef 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.datadef __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_ECBiv: iv偏移量,字節型數據paddingMode: 填充模式,默認為NoPadding, 可選NoPadding,ZeroPadding,PKCS5Padding,PKCS7PaddingcharacterSet: 字符集編碼'''self.key = keyself.mode = modeself.iv = ivself.characterSet = characterSetself.paddingMode = paddingModeself.data = ""def __ZeroPadding(self,data):data += b'\x00'while len(data) % 16 != 0:data += b'\x00'return datadef __StripZeroPadding(self,data):data = data[:-1]while len(data) % 16 != 0:data = data.rstrip(b'\x00')if data[-1] != b"\x00":breakreturn datadef __PKCS5_7Padding(self,data):needSize = 16-len(data) % 16if needSize == 0:needSize = 16return data + needSize.to_bytes(1,'little')*needSizedef __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 dataelse: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 = characterSetdef setPaddingMode(self,mode):'''設置填充模式mode: 可選NoPadding,ZeroPadding,PKCS5Padding,PKCS7Padding'''self.paddingMode = modedef 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 = entextreturn 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 mDataif __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類自己實現,更多詳細使用可以通過源碼中注釋了解。
文章出自:
原文鏈接:https://blog.csdn.net/chouzhou9701/article/details/122019967
csdn上發現的一位大佬寫,怕找不到了就完全復制他是文章