python實現AES加密解密


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 加密常用的有 ECBCBC 模式(我只用了這兩個模式,還有其他模式);數據類型為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模式的加密解密,從這個例子中可以看出參數中有幾個限制。

  1. 秘鑰必須為16字節或者16字節的倍數的字節型數據。
  2. 明文必須為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/ 這個網站舉例子:

image-20211218202237880

模式: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")) 

因為無論是 utf8gbk 編碼,針對英文字符編碼都是一個字符對應一個字節,所以這里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類自己實現,更多詳細使用可以通過源碼中注釋了解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM