• python实现AES加密解密


    1. 前言
    AES是一种对称加密,所谓对称加密就是加密与解密使用的秘钥是一个。

    之前写过一片关于python AES加密解密的文章,但是这里面细节实在很多,这次我从 参数类型、加密模式、编码模式、补全模式、等等方面 系统的说明如何使用AES加密解密。

    看文章不能急功近利,为了解决一个问题临时查到一个代码套用进去,或许可以迅速解决问题,但是遇到新的问题还需要再次查询,这种我认为还是比较浪费时间的。我相信看完认真看完这篇文章的你会大有收获。

    2. 环境安装
    1. pip uninstall crypto
    2. pip uninstall pycryptodome
    3. 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模式加密解密 :

    1. from Crypto.Cipher import AES
    2. password = b'1234567812345678' #秘钥,b就是表示为bytes类型
    3. text = b'abcdefghijklmnhi' #需要加密的内容,bytes类型
    4. aes = AES.new(password,AES.MODE_ECB) #创建一个aes对象
    5. # AES.MODE_ECB 表示模式是ECB模式
    6. en_text = aes.encrypt(text) #加密明文
    7. print("密文:",en_text) #加密明文,bytes类型
    8. den_text = aes.decrypt(en_text) # 解密密文
    9. print("明文:",den_text)

    输出:

    1. 密文: b'WU\xe0\x0e\xa3\x87\x12\x95\\]O\xd7\xe3\xd4 )'
    2. 明文: b'abcdefghijklmnhi'

    以上是针对ECB模式的加密解密,从这个例子中可以看出参数中有几个限制。

    1. 秘钥必须为16字节或者16字节的倍数的字节型数据。
    2. 明文必须为16字节或者16字节的倍数的字节型数据,如果不够16字节需要进行补全,关于补全规则,后面会在补全模式中具体介绍。

    通过CBC模式例子:

    1. from Crypto.Cipher import AES
    2. password = b'1234567812345678' #秘钥,b就是表示为bytes类型
    3. iv = b'1234567812345678' # iv偏移量,bytes类型
    4. text = b'abcdefghijklmnhi' #需要加密的内容,bytes类型
    5. aes = AES.new(password,AES.MODE_CBC,iv) #创建一个aes对象
    6. # AES.MODE_CBC 表示模式是CBC模式
    7. en_text = aes.encrypt(text)
    8. print("密文:",en_text) #加密明文,bytes类型
    9. aes = AES.new(password,AES.MODE_CBC,iv) #CBC模式下解密需要重新创建一个aes对象
    10. den_text = aes.decrypt(en_text)
    11. print("明文:",den_text)

    输出:

    1. 密文: b'\x93\x8bN!\xe7~>\xb0M\xba\x91\xab74;0'
    2. 明文: 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模式针对中文明文进行加密解密举例:

    1. from Crypto.Cipher import AES
    2. password = b'1234567812345678' #秘钥,b就是表示为bytes类型
    3. text = "好好学习天天向上".encode('gbk') #gbk编码,是1个中文字符对应2个字节,8个中文正好16字节
    4. aes = AES.new(password,AES.MODE_ECB) #创建一个aes对象
    5. # AES.MODE_ECB 表示模式是ECB模式
    6. print(len(text))
    7. en_text = aes.encrypt(text) #加密明文
    8. print("密文:",en_text) #加密明文,bytes类型
    9. den_text = aes.decrypt(en_text) # 解密密文
    10. print("明文:",den_text.decode("gbk")) # 解密后同样需要进行解码

    输出:

    1. 16
    2. 密文: b'=\xdd8k\x86\xed\xec\x17\x1f\xf7\xb2\x84~\x02\xc6C'
    3. 明文: 好好学习天天向上

    对于中文明文,我们可以使用encode()函数进行编码,将字符串转换成bytes类型数据,而这里我选择gbk编码,是为了正好能满足16字节,utf8编码是一个中文字符对应3个字节。这里为了举例所以才选择使用gbk编码。

    在解密后,同样是需要decode()函数进行解码的,将字节型数据转换回中文字符(字符串类型)。

    现在我们来看另外一种情况,密文是经过base64编码的(这种也是非常常见的,很多网站也是这样使用的),我们用 http://tool.chacuo.net/cryptaes/ 这个网站举例子:

    模式:ECB

    密码: 1234567812345678

    字符集:gbk编码

    输出: base64

    我们来写一个python 进行aes解密:

    1. from Crypto.Cipher import AES
    2. import base64
    3. password = b'1234567812345678'
    4. aes = AES.new(password,AES.MODE_ECB)
    5. en_text = b"Pd04a4bt7Bcf97KEfgLGQw=="
    6. en_text = base64.decodebytes(en_text) #将进行base64解码,返回值依然是bytes
    7. den_text = aes.decrypt(en_text)
    8. print("明文:",den_text.decode("gbk"))

    输出:

    明文: 好好学习天天向上

    这里的 b"Pd04a4bt7Bcf97KEfgLGQw==" 是一个bytes数据, 如果你传递的是一个字符串,你可以直接使用 encode()函数 将其转换为 bytes类型数据。

    1. from Crypto.Cipher import AES
    2. import base64
    3. password = b'1234567812345678'
    4. aes = AES.new(password,AES.MODE_ECB)
    5. en_text = "Pd04a4bt7Bcf97KEfgLGQw==".encode() #将字符串转换成bytes数据
    6. en_text = base64.decodebytes(en_text) #将进行base64解码,参数为bytes数据,返回值依然是bytes
    7. den_text = aes.decrypt(en_text)
    8. print("明文:",den_text.decode("gbk"))

    因为无论是 utf8 和 gbk 编码,针对英文字符编码都是一个字符对应一个字节,所以这里**encode()**函数主要作用就是转换成bytes数据,然后使用base64进行解码。

    hexstr,base64编码解码例子:

    1. import base64
    2. import binascii
    3. data = "hello".encode()
    4. data = base64.b64encode(data)
    5. print("base64编码:",data)
    6. data = base64.b64decode(data)
    7. print("base64解码:",data)
    8. data = binascii.b2a_hex(data)
    9. print("hexstr编码:",data)
    10. data = binascii.a2b_hex(data)
    11. print("hexstr解码:",data)

    输出:

    1. base64编码: b'aGVsbG8='
    2. base64解码: b'hello'
    3. hexstr编码: b'68656c6c6f'
    4. 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)添加的填充字节数每个填充字节的值
    0160x10
    1150x0F
    2140x0E
    3130X0D
    4120x0C
    5110x0B
    6100x0A
    790x09
    880x08
    970x08
    1060x07
    1150x06
    1240x05
    1330x04
    1420x03
    1510x01

    这里可以看到,当明文长度值已经对齐时(mod 16 = 0),还是需要进行填充,并且填充16个字节值为0x10。ZeroPadding填充逻辑也是类似的,只不过填充的字节值都为0x00,在python表示成 b'\x00'。

    填充完毕后,就可以使用 AES进行加密解密了,当然解密后,也需要剔除填充的数据,无奈Python这些步骤需要自己实现(如果有这样的库还请评论指出)。

    7.Python的完整实现:
    1. from Crypto.Cipher import AES
    2. import base64
    3. import binascii
    4. # 数据类
    5. class MData():
    6. def __init__(self, data = b"",characterSet='utf-8'):
    7. # data肯定为bytes
    8. self.data = data
    9. self.characterSet = characterSet
    10. def saveData(self,FileName):
    11. with open(FileName,'wb') as f:
    12. f.write(self.data)
    13. def fromString(self,data):
    14. self.data = data.encode(self.characterSet)
    15. return self.data
    16. def fromBase64(self,data):
    17. self.data = base64.b64decode(data.encode(self.characterSet))
    18. return self.data
    19. def fromHexStr(self,data):
    20. self.data = binascii.a2b_hex(data)
    21. return self.data
    22. def toString(self):
    23. return self.data.decode(self.characterSet)
    24. def toBase64(self):
    25. return base64.b64encode(self.data).decode()
    26. def toHexStr(self):
    27. return binascii.b2a_hex(self.data).decode()
    28. def toBytes(self):
    29. return self.data
    30. def __str__(self):
    31. try:
    32. return self.toString()
    33. except Exception:
    34. return self.toBase64()
    35. ### 封装类
    36. class AEScryptor():
    37. def __init__(self,key,mode,iv = '',paddingMode= "NoPadding",characterSet ="utf-8"):
    38. '''
    39. 构建一个AES对象
    40. key: 秘钥,字节型数据
    41. mode: 使用模式,只提供两种,AES.MODE_CBC, AES.MODE_ECB
    42. iv: iv偏移量,字节型数据
    43. paddingMode: 填充模式,默认为NoPadding, 可选NoPadding,ZeroPadding,PKCS5Padding,PKCS7Padding
    44. characterSet: 字符集编码
    45. '''
    46. self.key = key
    47. self.mode = mode
    48. self.iv = iv
    49. self.characterSet = characterSet
    50. self.paddingMode = paddingMode
    51. self.data = ""
    52. def __ZeroPadding(self,data):
    53. data += b'\x00'
    54. while len(data) % 16 != 0:
    55. data += b'\x00'
    56. return data
    57. def __StripZeroPadding(self,data):
    58. data = data[:-1]
    59. while len(data) % 16 != 0:
    60. data = data.rstrip(b'\x00')
    61. if data[-1] != b"\x00":
    62. break
    63. return data
    64. def __PKCS5_7Padding(self,data):
    65. needSize = 16-len(data) % 16
    66. if needSize == 0:
    67. needSize = 16
    68. return data + needSize.to_bytes(1,'little')*needSize
    69. def __StripPKCS5_7Padding(self,data):
    70. paddingSize = data[-1]
    71. return data.rstrip(paddingSize.to_bytes(1,'little'))
    72. def __paddingData(self,data):
    73. if self.paddingMode == "NoPadding":
    74. if len(data) % 16 == 0:
    75. return data
    76. else:
    77. return self.__ZeroPadding(data)
    78. elif self.paddingMode == "ZeroPadding":
    79. return self.__ZeroPadding(data)
    80. elif self.paddingMode == "PKCS5Padding" or self.paddingMode == "PKCS7Padding":
    81. return self.__PKCS5_7Padding(data)
    82. else:
    83. print("不支持Padding")
    84. def __stripPaddingData(self,data):
    85. if self.paddingMode == "NoPadding":
    86. return self.__StripZeroPadding(data)
    87. elif self.paddingMode == "ZeroPadding":
    88. return self.__StripZeroPadding(data)
    89. elif self.paddingMode == "PKCS5Padding" or self.paddingMode == "PKCS7Padding":
    90. return self.__StripPKCS5_7Padding(data)
    91. else:
    92. print("不支持Padding")
    93. def setCharacterSet(self,characterSet):
    94. '''
    95. 设置字符集编码
    96. characterSet: 字符集编码
    97. '''
    98. self.characterSet = characterSet
    99. def setPaddingMode(self,mode):
    100. '''
    101. 设置填充模式
    102. mode: 可选NoPadding,ZeroPadding,PKCS5Padding,PKCS7Padding
    103. '''
    104. self.paddingMode = mode
    105. def decryptFromBase64(self,entext):
    106. '''
    107. 从base64编码字符串编码进行AES解密
    108. entext: 数据类型str
    109. '''
    110. mData = MData(characterSet=self.characterSet)
    111. self.data = mData.fromBase64(entext)
    112. return self.__decrypt()
    113. def decryptFromHexStr(self,entext):
    114. '''
    115. 从hexstr编码字符串编码进行AES解密
    116. entext: 数据类型str
    117. '''
    118. mData = MData(characterSet=self.characterSet)
    119. self.data = mData.fromHexStr(entext)
    120. return self.__decrypt()
    121. def decryptFromString(self,entext):
    122. '''
    123. 从字符串进行AES解密
    124. entext: 数据类型str
    125. '''
    126. mData = MData(characterSet=self.characterSet)
    127. self.data = mData.fromString(entext)
    128. return self.__decrypt()
    129. def decryptFromBytes(self,entext):
    130. '''
    131. 从二进制进行AES解密
    132. entext: 数据类型bytes
    133. '''
    134. self.data = entext
    135. return self.__decrypt()
    136. def encryptFromString(self,data):
    137. '''
    138. 对字符串进行AES加密
    139. data: 待加密字符串,数据类型为str
    140. '''
    141. self.data = data.encode(self.characterSet)
    142. return self.__encrypt()
    143. def __encrypt(self):
    144. if self.mode == AES.MODE_CBC:
    145. aes = AES.new(self.key,self.mode,self.iv)
    146. elif self.mode == AES.MODE_ECB:
    147. aes = AES.new(self.key,self.mode)
    148. else:
    149. print("不支持这种模式")
    150. return
    151. data = self.__paddingData(self.data)
    152. enData = aes.encrypt(data)
    153. return MData(enData)
    154. def __decrypt(self):
    155. if self.mode == AES.MODE_CBC:
    156. aes = AES.new(self.key,self.mode,self.iv)
    157. elif self.mode == AES.MODE_ECB:
    158. aes = AES.new(self.key,self.mode)
    159. else:
    160. print("不支持这种模式")
    161. return
    162. data = aes.decrypt(self.data)
    163. mData = MData(self.__stripPaddingData(data),characterSet=self.characterSet)
    164. return mData
    165. if __name__ == '__main__':
    166. key = b"1234567812345678"
    167. iv = b"0000000000000000"
    168. aes = AEScryptor(key,AES.MODE_CBC,iv,paddingMode= "ZeroPadding",characterSet='utf-8')
    169. data = "好好学习"
    170. rData = aes.encryptFromString(data)
    171. print("密文:",rData.toBase64())
    172. rData = aes.decryptFromBase64(rData.toBase64())
    173. print("明文:",rData)

    我简单的对其进行了封装,加密和解密返回的数据类型可以使用toBase64(),toHexStr() 进行编码。另外我没有对key和iv进行补全,可以使用MData类自己实现,更多详细使用可以通过源码中注释了解。

    原文来自一位大佬,原链接:https://blog.csdn.net/chouzhou9701/article/details/122019967

    8.python实现 RSA加密

    -------RES加密来自于一个全球领先的视频为基础的安防大厂 DAHUA------

    1. from Crypto.Cipher import PKCS1_v1_5
    2. from Crypto.PublicKey import RSA
    3. def get_encrypt_password(pubkey, password):
    4. if isinstance(password, str): # isinstance函数来判断一个对象是否是一个已知的类型
    5. password = password.encode() # 把字符串password 编码成字节类型(byte)
    6. # 秘钥生成 --XXX接口返回一个publickey,然后再拼接后作为秘钥
    7. pub_key = """-----BEGIN PUBLIC KEY-----
    8. """ + pubkey.strip('"') + """
    9. -----END PUBLIC KEY-----"""
    10. # 实例化一个rsa 对象
    11. key_obj = RSA.importKey(pub_key)
    12. # 新建一个cipher对象
    13. cipher = PKCS1_v1_5.new(key_obj)
    14. # 调用cipher加密方法后再进行一次base64编码
    15. web_safe_pwd = base64.b64encode(cipher.encrypt(password))
    16. # 返回RSA 加密后的密文
    17. return web_safe_pwd.decode()

    ==================================================

    在使用requests库中的POST方法时传参数据体数据类型错误的坑

    post 传json时,这里的是传python中的dict 而不是dict做dumps后的数据,这里是坑,测试了几次记录一下

    1. import requests
    2. # 请求HTTPS 跳过ssl校验 并不打印警告信息 ,这里飘红不影响
    3. from requests.packages import urllib3
    4. urllib3.disable_warnings()
    5. # 获取token
    6. def getToken():
    7. url = f"{IP}:{PORT}/xxxxx
    8. # 获取 access_token
    9. access_Token = getToken()["data"]["access_token"]
    10. Authorization = f"bearer {access_Token}"
    11. # 增加头部
    12. headers = {
    13. 'Content-type': 'application/json',
    14. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64;'
    15. ' x64) AppleWebKit/537.36 (KHTML, like'
    16. ' Gecko) Chrome/89.0.4389.90 Safari/537.36',
    17. 'Cookie': 'JSESSIONID=123456789;name=value' ## 这里也可以设置cookie,
    18. 'Authorization': Authorization
    19. }
    20. # 请求体--数据部分
    21. data = {
    22. "grant_type": "password",
    23. "username": USERNAME,
    24. "password": rsaPassword,
    25. "client_id": CLIENT_ID,
    26. "client_secret": CLIENT_SECRET,
    27. "public_key": pubkey
    28. }
    29. # jdata = json.dumps(data)
    30. # post 传json时,这里的是传python中的dict 而不是dict做dumps后的数据,这里是坑,测试了几次
    31. # 一个respones对象
    32. res = requests.post(url=url, headers=headers,json=data, verify=False,)
    33. return res.json() #返回的直接是python中的字符串格式

  • 相关阅读:
    拒绝服务 DDoS 攻击
    ZooKeeper之Java API的基本使用以及应用场景的实现
    STL-stack
    HXAPIGate系列——HXAPIGate快速入门
    SpringBoot+Vue+token实现(表单+图片)上传、图片地址保存到数据库。上传图片保存位置自己定义、图片可以在前端回显(一))
    数据库设计之E-R图和关系表
    TinyShell(CSAPP实验)
    【深度学习】详解 ViLT
    从0到1 express 安装swagger
    NestJs和Vite使用monorepo管理项目中,需要使用共享的文件夹步骤
  • 原文地址:https://blog.csdn.net/weixin_42419429/article/details/136405371