Cryptography
cryptography 库致力于成为“cryptography for humans”,提供各种函数帮助你创建安全,易于使用的加密方案。
本文的环境是在python3.8运行,不过亲测3.5以上运行没有报错。
安装cryptography库
pip install cryptography
在cryptography库中,对称加密算法的抽象是fernet模块,包括了对数据的加解密以及签名验证功能,以及密钥过期机制。
该模块采用如下定义:
- 加解密算法为AES,密钥位长128,CBC模式,填充标准PKCS7
- 签名算法为SHA256的HMAC,密钥位长128位
- 密钥可以设置过期时间
使用fernet模块的示例代码
import base64
import os
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
password = b"password"
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
backend=default_backend() )
key = base64.urlsafe_b64encode(kdf.derive(password))
f = Fernet(key)
token = f.encrypt(b"Secret message!")
f.decrypt(token)
print(f.decrypt(token)) # 返回b'Secret message!'
可见使用Fernet可以比较轻易的实现对明文的加密,下面对一些函数进行讲解
密钥生成部分
(1) generate_key(cls)
fernet模块封装了对称加密的操作,其中提供了生成随机密钥的函数
生成一个32位随机数,使用base64编码
def generate_key(cls):
return base64.urlsafe_b64encode(os.urandom(32))
(2) PBKDF2HMAC
PBKDF2HMAC是个密钥推导函数,通过多次对salt进行hash运算从而产生密钥
password = b"password"
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
backend=default_backend() )
加密部分
(1) encrypt和_encrypt_from_parts
主要使用 encrypt和_encrypt_from_parts两个函数对数据进行加密和认证
首先,encrypt函数主要负责获取current_time,并随机生成CBC初始向量iv
def encrypt(self, data):
return self.encrypt_at_time(data, int(time.time()))
def encrypt_at_time(self, data, current_time):
iv = os.urandom(16)
return self._encrypt_from_parts(data, current_time, iv)
# iv是随机生成的16位初始向量
_encrypt_from_parts(self,data,current_time,iv)
特点如下:
· 指定padding方式为PKCS7
· 把要加密的原始data用padding方式补齐
· 加密算法使用AES,分组密码模式使用CBC
· 产生认证吗,把current_time、iv、ciphertext三者合并得到一个basic_parts
padder = padding.PKCS7(algorithms.AES.block_size).padder() # 设定填充模式为 PKCS7
padded_data = padder.update(data) + padder.finalize() #使用PKCS对数据进行填充
encryptor = Cipher(
algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend
).encryptor()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
basic_parts = (
b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext
)
· 计算basic_parts的hmac值
· 把basic_parts + hmac 做base64计算后返回,这就是我们最终得到的加密数据,里面包含了时间戳、iv、密文、hmac
· 接着使用SHA256()哈希函数产生HMAC认证码。HMAC是使用hash算法构造的含有密钥散列函数算法,其中哈希算法采用了SHA256,密钥是self._signing_key(32位key中的前16位),产生固定长度的认证码,以防止密文在传输过程中被篡改。
h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend)
h.update(basic_parts)
hmac = h.finalize()
总的代码:
def encrypt(self, data):
return self.encrypt_at_time(data, int(time.time()))
def encrypt_at_time(self, data, current_time):
iv = os.urandom(16)
return self._encrypt_from_parts(data, current_time, iv)
def _encrypt_from_parts(self, data, current_time, iv):
utils._check_bytes("data", data)
padder = padding.PKCS7(algorithms.AES.block_size).padder() # 设定填充模式为 PKCS7
padded_data = padder.update(data) + padder.finalize() #使用PKCS对数据进行填充
encryptor = Cipher(
algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend
).encryptor()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
basic_parts = (b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext)
#把current_time、iv、ciphertext三者合并得到一个basic_parts**
h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend)
h.update(basic_parts)
hmac = h.finalize()
return base64.urlsafe_b64encode(basic_parts + hmac)
解密部分
与加密encrypt完全相反
**· 得到current_time **
· base64解码token,得到包含时间戳、iv、密文、hmac的data
· 根据时间戳和ttl判断密钥是否失效。
· 计算hmac,并于之前的hmac进行验证,判断密钥有效性
· 获取iv(9:25)、密文(25:-32),通过密钥进行解密,得到经过pad的明文
· 通过PKCS7进行unpaid操作,得到去掉补齐的明文
def decrypt(self, token, ttl=None):
timestamp, data = Fernet._get_unverified_token_data(token)
return self._decrypt_data(data, timestamp, ttl, int(time.time()))
def decrypt_at_time(self, token, ttl, current_time):
if ttl is None:
raise ValueError(
"decrypt_at_time() can only be used with a non-None ttl"
)
timestamp, data = Fernet._get_unverified_token_data(token)
return self._decrypt_data(data, timestamp, ttl, current_time)
def extract_timestamp(self, token):
timestamp, data = Fernet._get_unverified_token_data(token)
# Verify the token was not tampered with.
self._verify_signature(data)
return timestamp
@staticmethod
def _get_unverified_token_data(token):
utils._check_bytes("token", token)
try:
data = base64.urlsafe_b64decode(token)
except (TypeError, binascii.Error):
raise InvalidToken
if not data or six.indexbytes(data, 0) != 0x80:
raise InvalidToken
try:
(timestamp,) = struct.unpack(">Q", data[1:9])
except struct.error:
raise InvalidToken
return timestamp, data
def _verify_signature(self, data):
h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend)
h.update(data[:-32])
try:
h.verify(data[-32:])
except InvalidSignature:
raise InvalidToken
def _decrypt_data(self, data, timestamp, ttl, current_time):
if ttl is not None:
if timestamp + ttl < current_time:
raise InvalidToken
if current_time + _MAX_CLOCK_SKEW < timestamp:
raise InvalidToken
self._verify_signature(data)
iv = data[9:25]
ciphertext = data[25:-32]
decryptor = Cipher(
algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend
).decryptor()
plaintext_padded = decryptor.update(ciphertext)
try:
plaintext_padded += decryptor.finalize()
except ValueError:
raise InvalidToken
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
unpadded = unpadder.update(plaintext_padded)
try:
unpadded += unpadder.finalize()
except ValueError:
raise InvalidToken
return unpadded
补充:
(1) struct.pack和struct.unpack
python有时需要处理二进制数据,例如 存取文件,socket操作时.这时候,可以使用python的struct模块来完成.可以用struct来处理c语言中的结构体,首先pack将其转化为Python 的字符串类型表示的二进制,然后unpack再转换为Python的结构化类型(元组…)
struct.pack(fmt,v1,v2,…)
将v1,v2等参数的值进行一层包装,转换成二进制形式,包装的方法由fmt指定。被包装的参数对应于fmt,如:pack(‘i’ ,1),此时就是对1按照C中int类型转换成二进制。最后返回一个包装后的字符串
格式化字符串fmt(format string)由一个或多个格式字符(format characters)组成,对于这些格式字符的描述参照Python manual如下:
FORMAT | C TYPE | PYTHON TYPE | STANDARD SIZE | NOTES |
---|---|---|---|---|
x | pad byte | no value | ||
c | char | string of length 1 | 1 | |
b | signed char | integer | 1 | (3) |
B | unsigned char | integer | 1 | (3) |
? | _Bool | bool | 1 | (1) |
h | short | integer | 2 | (3) |
H | unsigned short | integer | 2 | (3) |
i | int | integer | 4 | (3) |
I | unsigned int | integer | 4 | (3) |
l | long | integer | 4 | (3) |
L | unsigned long | integer | 4 | (3) |
q | long long | integer | 8 | (2), (3) |
Q | unsigned long long | integer | 8 | (2), (3) |
f | float | float | 4 | (4) |
d | double | float | 8 | (4) |
s | char[] | string | ||
p | char[] | string | ||
P | void * | integer | (5), (3) |
代码示例
buffer = struct.pack( "2ihb" , 1,1,2,3) #2i表示输入两个数字
print( buffer )
print(struct.unpack( "2ihb" , buffer ))
Output:
b'\x01\x00\x00\x00\x01\x00\x00\x00\x02\x00\x03'
(1, 1, 2, 3)
大端模式:
字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。假设储存 0x1234
内存地址 | 0x4000(低地址) | 0x4001(高地址) |
---|---|---|
存放内容 | 0x12 | 0x34 |
小端模式:
与大端存储模式相反,在小端存储模式中,低地址中存放的是字数据的低字节,高地址存放的是字数据的高字节。
内存地址 | 0x4000 | 0x4001 |
---|---|---|
存放内容 | 0x34 | 0x12 |
本机默认为小端模式,低地址存放低位数据。首先将参数1,2,3打包,打包前1,2,3明显属于python int数据类型,pack后就变成了C结构的二进制串,转成 python的string类型来显示就是 ‘\x01\x00\x00\x00\x02\x00\x03’。 (用2进制储存,16进制显示,左侧为低地址,右侧为高地址)
int在C语言中是4位,1则表示为01 00 00 00
short 在C语言中是2位,2表示为 02 00
同理b 代表C struct中的signed char类型,占1位,故而表示为 03
在Format string 的首位,有一个可选字符来决定大端和小端,列表如下:
CHARACTER | BYTE ORDER | SIZE | ALIGNMENT |
---|---|---|---|
@ | native | native | native |
= | native | standard | none |
< | little-endian | standard | none |
> | big-endian | standard | none |
! | network (= big-endian) | standard | none |
如果没有在fmt首位设置,则默认为@,使用本机的对齐模式。如程序所示,使用的format string中首位为!,即为大端模式标准对齐方式,故而输出的为’\x00\x00\x00\x01\x00\x02\x03’,其中低位01被存放在内存的高位,高位自己就被放在内存的低地址位了。
# data from a sequence, network byteorder
data = [1,2,3,4,5]
buffer = struct.pack( "!3ihb" , * data)
print(repr(buffer))
print(struct.unpack("!3ihb", buffer))
output:
b'\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x04\x05'
(1, 2, 3, 4, 5)