使用Cryptography进行对称密钥加密

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如下:

FORMATC TYPEPYTHON TYPESTANDARD SIZENOTES
xpad byteno value
ccharstring of length 11
bsigned charinteger1(3)
Bunsigned charinteger1(3)
?_Boolbool1(1)
hshortinteger2(3)
Hunsigned shortinteger2(3)
iintinteger4(3)
Iunsigned intinteger4(3)
llonginteger4(3)
Lunsigned longinteger4(3)
qlong longinteger8(2), (3)
Qunsigned long longinteger8(2), (3)
ffloatfloat4(4)
ddoublefloat8(4)
schar[]string
pchar[]string
Pvoid *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(高地址)
存放内容0x120x34

小端模式:

与大端存储模式相反,在小端存储模式中,低地址中存放的是字数据的低字节,高地址存放的是字数据的高字节。

内存地址0x40000x4001
存放内容0x340x12

本机默认为小端模式,低地址存放低位数据。首先将参数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 的首位,有一个可选字符来决定大端和小端,列表如下:

CHARACTERBYTE ORDERSIZEALIGNMENT
@nativenativenative
=nativestandardnone
<little-endianstandardnone
>big-endianstandardnone
!network (= big-endian)standardnone

如果没有在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)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值