Files
hsa/.venv/lib/python3.10/site-packages/qcloud_cos/crypto.py
2025-09-11 13:29:12 +00:00

430 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
import os
import random
import logging
import copy
import base64
import struct
from .cos_comm import *
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto import Random
from Crypto.Util import Counter
from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5
from .cos_exception import CosClientError
from .streambody import StreamBody
from abc import ABCMeta, abstractmethod
logger = logging.getLogger(__name__)
_AES_CTR_COUNTER_BITS_LENGTH = 8 * 16
_AES_256_KEY_SIZE = 32
def random_key(key_len):
return Random.new().read(key_len)
def random_iv():
iv = Random.new().read(16)
return iv
def iv_to_big_int(iv):
iv_pair = struct.unpack(">QQ", iv)
iv_int = iv_pair[0] << 64 | iv_pair[1]
return iv_int
class AESCTRCipher(object):
"""数据加密类,用于加密用户数据"""
def __init__(self):
"""初始化"""
self.__block_size_len = AES.block_size
self.__cipher = None
self.__key_len = _AES_256_KEY_SIZE
def new_cipher(self, key, start, offset=0):
"""初始化AES加解密对象
:param key(string): 对称密钥
:param start(int): 对称加密初始随机值
:param offset(int): 主要用于解密想要解密文本的偏移必须为16的整数倍
"""
block_index_offset = self.__calc_offset(offset)
new_start = start + block_index_offset
my_counter = Counter.new(_AES_CTR_COUNTER_BITS_LENGTH, initial_value=new_start)
self.__cipher = AES.new(key, AES.MODE_CTR, counter=my_counter)
def encrypt(self, plaintext):
"""加密数据
:param plaintext(string): 需要加密的数据
"""
if self.__cipher is None:
raise CosClientError('cipher is not initialized')
return self.__cipher.encrypt(plaintext)
def decrypt(self, plaintext):
"""解密数据
:param plaintext(string): 需要解密的数据数据的起始位置必须为16的整数倍
"""
if self.__cipher is None:
raise CosClientError('cipher is not initialized')
return self.__cipher.decrypt(plaintext)
# offset 必须为block_size的整数倍
def __calc_offset(self, offset):
"""通过文本偏移计算counter的偏移
:param offset(int): 文本偏移
"""
if not self.__is_block_aligned(offset):
raise CosClientError('offset is not align to encrypt block')
return offset // self.__block_size_len
def __is_block_aligned(self, offset):
"""判断文本偏移是否是block_size对齐
:param offset(int): 文本偏移
"""
if offset is None:
offset = 0
return 0 == offset % self.__block_size_len
def adjust_read_offset(self, offset):
"""用于调整读取的offset为block_size对齐"""
if offset:
offset = (offset // self.__block_size_len) * self.__block_size_len
return offset
def get_key(self):
"""获取随机密钥"""
return random_key(self.__key_len)
def get_counter_iv(self):
"""获取对称加密初始随机值"""
return random_iv()
class RSAKeyPair:
"""封装有公钥和私钥"""
def __init__(self, public_key, private_key):
self.publick_key = public_key
self.private_key = private_key
class RSAKeyPairPath:
"""封装有公钥和私钥的路径"""
def __init__(self, public_key_path, private_key_path):
self.public_key_path = public_key_path
self.private_key_path = private_key_path
class BaseProvider(object):
"""客户端主密钥加密基类"""
def __init__(self, cipher):
"""初始化
:param cipher(an AES object): 数据加解密类
"""
self.data_cipher = cipher
def get_data_key(self):
"""随机获取数据加解密密钥"""
return self.data_cipher.get_key()
@abstractmethod
def init_data_cipher(self):
"""初始化cipher"""
pass
@abstractmethod
def init_data_cipter_by_user(self, encrypt_key, encrypt_iv, offset=0):
"""根据密钥初始化cipher"""
pass
def adjust_read_offset(self, start):
"""用于调整读取的offset为block_size对齐"""
return self.data_cipher.adjust_read_offset(start)
def make_data_encrypt_adapter(self, stream):
"""创建数据流加密适配器"""
size = 0
if hasattr(stream, '__len__'):
size = len(stream)
elif hasattr(stream, 'tell') and hasattr(stream, 'seek'):
current = stream.tell()
stream.seek(0, os.SEEK_END)
size = stream.tell()
stream.seek(current, os.SEEK_SET)
else:
return None
return DataEncryptAdapter(stream, size, copy.copy(self.data_cipher))
def make_data_decrypt_adapter(self, rt, offset):
"""创建数据流解密适配器"""
return DataDecryptAdapter(rt, copy.copy(self.data_cipher), offset)
class RSAProvider(BaseProvider):
"""客户端非对称主密钥加密类"""
def __init__(self, key_pair_info=None, cipher=AESCTRCipher(), passphrase=None):
"""初始化"""
super(RSAProvider, self).__init__(cipher=cipher)
default_rsa_dir = os.path.expanduser('~/.cos_local_rsa')
default_public_key_path = os.path.join(default_rsa_dir, '.public_key.pem')
default_private_key_path = os.path.join(default_rsa_dir, '.private_key.pem')
self.__encrypt_obj = None
self.__decrypt_obj = None
self.__data_key = None
self.__data_iv = None
if isinstance(key_pair_info, RSAKeyPair):
self.__encrypt_obj = PKCS1_v1_5.new(RSA.importKey(key_pair_info.publick_key, passphrase=passphrase))
self.__decrypt_obj = PKCS1_v1_5.new(RSA.importKey(key_pair_info.private_key, passphrase=passphrase))
elif isinstance(key_pair_info, RSAKeyPairPath):
self.__encrypt_obj, self.__decrypt_obj = self.__get_key_by_path(key_pair_info.public_key_path,
key_pair_info.private_key_path, passphrase)
else:
logger.info('key_pair_info is None, try to get key from default path')
if os.path.exists(default_private_key_path) and os.path.exists(default_public_key_path):
self.__encrypt_obj, self.__decrypt_obj = self.__get_key_by_path(default_public_key_path,
default_private_key_path, passphrase)
# 为用户自动创建rsa
if self.__encrypt_obj is None and self.__decrypt_obj is None:
logger.warning('fail to get rsa key, will generate key')
private_key = RSA.generate(2048)
public_key = private_key.publickey()
self.__encrypt_obj = PKCS1_OAEP.new(public_key)
self.__decrypt_obj = PKCS1_OAEP.new(private_key)
if not os.path.exists(default_rsa_dir):
os.makedirs(default_rsa_dir)
with open(default_private_key_path, 'wb') as f:
f.write(private_key.exportKey(passphrase=passphrase))
with open(default_public_key_path, 'wb') as f:
f.write(public_key.exportKey(passphrase=passphrase))
@staticmethod
def get_rsa_key_pair(public_key, private_key):
"""开放给用户用于生成RSAKeyPair"""
if public_key is None or private_key is None:
raise CosClientError('public_key or private_key is not allowed to be None !!!')
return RSAKeyPair(public_key, private_key)
@staticmethod
def get_rsa_key_pair_path(public_key_path, private_key_path):
"""开放给用户用于生成RSAKeyPairPath"""
if public_key_path is None or private_key_path is None:
raise CosClientError('public_key or private_key is not allowed to be None !!!')
return RSAKeyPairPath(public_key_path, private_key_path)
def __get_key_by_path(self, public_path=None, private_path=None, passphrase=None):
"""用于从提供的路径中获取公钥和私钥"""
if public_path is None or private_path is None:
return None, None
encrypt_obj, decrypt_obj = None, None
if os.path.exists(public_path) and os.path.exists(private_path):
with open(public_path, 'rb') as f:
encrypt_obj = PKCS1_OAEP.new(RSA.importKey(f.read(), passphrase=passphrase))
with open(private_path, 'rb') as f:
decrypt_obj = PKCS1_OAEP.new(RSA.importKey(f.read(), passphrase=passphrase))
return encrypt_obj, decrypt_obj
def init_data_cipher(self):
"""初始化cipher"""
encrypt_key = None
encrypt_iv = None
self.__data_key = self.get_data_key()
self.__data_iv = self.data_cipher.get_counter_iv()
start = iv_to_big_int(self.__data_iv)
self.data_cipher.new_cipher(self.__data_key, start)
encrypt_key = self.__encrypt_obj.encrypt(self.__data_key)
encrypt_iv = self.__encrypt_obj.encrypt(self.__data_iv)
return encrypt_key, encrypt_iv
def init_data_cipter_by_user(self, encrypt_key, encrypt_iv, offset=0):
"""根据密钥初始化cipher"""
self.__data_key = self.__decrypt_obj.decrypt(encrypt_key)
self.__data_iv = self.__decrypt_obj.decrypt(encrypt_iv)
start = iv_to_big_int(self.__data_iv)
self.data_cipher.new_cipher(self.__data_key, start, offset)
class AESProvider(BaseProvider):
"""客户端对称主密钥加密类"""
def __init__(self, aes_key=None, aes_key_path=None, cipher=AESCTRCipher()):
"""初始化"""
super(AESProvider, self).__init__(cipher=cipher)
self.__ed_obj = None
self.__data_key = None
self.__data_iv = None
self.__my_counter = Counter.new(_AES_CTR_COUNTER_BITS_LENGTH, initial_value=0)
self.__aes_key = aes_key
self.__aes_key_path = aes_key_path
def init_ed_obj(self):
default_aes_dir = os.path.expanduser('~/.cos_local_aes')
default_key_path = os.path.join(default_aes_dir, '.aes_key.pem')
if self.__aes_key:
aes_key = to_bytes(base64.b64decode(to_bytes(self.__aes_key)))
self.__ed_obj = AES.new(aes_key, AES.MODE_CTR, counter=self.__my_counter)
elif self.__aes_key_path:
if os.path.exists(self.__aes_key_path):
with open(self.__aes_key_path, 'rb') as f:
aes_key = f.read()
aes_key = to_bytes(base64.b64decode(to_bytes(aes_key)))
self.__ed_obj = AES.new(aes_key, AES.MODE_CTR, counter=self.__my_counter)
else:
logger.info('aes_key and aes_key_path is None, try to get key from default path')
if os.path.exists(default_key_path):
with open(default_key_path, 'rb') as f:
aes_key = f.read()
aes_key = to_bytes(base64.b64decode(to_bytes(aes_key)))
self.__ed_obj = AES.new(aes_key, AES.MODE_CTR, counter=self.__my_counter)
if self.__ed_obj is None:
logger.warning('fail to get aes key, will generate key')
aes_key = random_key(_AES_256_KEY_SIZE)
self.__ed_obj = AES.new(aes_key, AES.MODE_CTR, counter=self.__my_counter)
if not os.path.exists(default_aes_dir):
os.makedirs(default_aes_dir)
with open(default_key_path, 'wb') as f:
aes_key = to_bytes(base64.b64encode(to_bytes(aes_key)))
f.write(aes_key)
def init_data_cipher(self):
"""初始化cipher"""
encrypt_key = None
encrypt_iv = None
self.__data_key = self.get_data_key()
self.__data_iv = self.data_cipher.get_counter_iv()
start = iv_to_big_int(self.__data_iv)
self.data_cipher.new_cipher(self.__data_key, start)
self.init_ed_obj()
encrypt_key = self.__ed_obj.encrypt(self.__data_key)
encrypt_iv = self.__ed_obj.encrypt(self.__data_iv)
return encrypt_key, encrypt_iv
def init_data_cipter_by_user(self, encrypt_key, encrypt_iv, offset=0):
"""根据密钥初始化cipher"""
self.init_ed_obj()
self.__data_key = self.__ed_obj.decrypt(encrypt_key)
self.__data_iv = self.__ed_obj.decrypt(encrypt_iv)
start = iv_to_big_int(self.__data_iv)
self.data_cipher.new_cipher(self.__data_key, start, offset)
class MetaHandle(object):
"""用于获取/生成加密的元信息"""
def __init__(self, encrypt_key=None, encrypt_iv=None):
"""初始化
:param encrypt_key(string): 加密的数据密钥
:param encrypt_iv(bytes): 加密counter的初始值
"""
self.__encrypt_key = encrypt_key
self.__encrypt_iv = encrypt_iv
def set_object_meta(self, headers):
"""设置加密元信息到object的头部"""
meta_data = dict()
meta_data['x-cos-meta-client-side-encryption-key'] = to_bytes(base64.b64encode(to_bytes(self.__encrypt_key)))
meta_data['x-cos-meta-client-side-encryption-iv'] = to_bytes(base64.b64encode(to_bytes(self.__encrypt_iv)))
headers['Metadata'] = meta_data
return headers
def get_object_meta(self, headers):
"""从object的头部获取加密元信息"""
if 'x-cos-meta-client-side-encryption-key' in headers and 'x-cos-meta-client-side-encryption-iv' in headers:
self.__encrypt_key = base64.b64decode(to_bytes(headers['x-cos-meta-client-side-encryption-key']))
self.__encrypt_iv = base64.b64decode(to_bytes(headers['x-cos-meta-client-side-encryption-iv']))
return self.__encrypt_key, self.__encrypt_iv
class DataEncryptAdapter(object):
"""用于读取经过加密后的的数据"""
def __init__(self, data, content_len, data_cipher):
self._data = to_bytes(data)
self._data_cipher = data_cipher
self._content_len = content_len
self._read_len = 0
@property
def len(self):
return self._content_len
def read(self, length):
"""读取加密后的数据"""
if self._read_len >= self._content_len:
return ''
if length is None or length < 0:
bytes_to_read = self._content_len - self._read_len
else:
bytes_to_read = min(length, self._content_len - self._read_len)
if isinstance(self._data, bytes):
content = self._data[self._read_len:self._read_len + bytes_to_read]
else:
content = self._data.read(bytes_to_read)
self._read_len += bytes_to_read
content = self._data_cipher.encrypt(content)
return content
class DataDecryptAdapter(StreamBody):
"""用于读取经过解密后的数据"""
def __init__(self, rt, data_cipher, offset=0):
"""初始化
:param rt(request object): request请求返回的对象
:param data_cipher(an AES object): 数据加解密类
:param offset(int): 数据读取的偏移量
"""
super(DataDecryptAdapter, self).__init__(rt)
self._data_cipher = data_cipher
self._offset = offset
def read(self, length, auto_decompress=False):
"""读取解密后的数据"""
if self._read_len >= self._content_len:
return ''
if self._use_encoding and not auto_decompress:
content = self._rt.raw.read(length)
else:
try:
content = next(self._rt.iter_content(length))
except StopIteration:
return ''
content = self._data_cipher.decrypt(content)
if self._read_len < self._offset and self._read_len + len(content) > self._offset:
content = content[self._offset:]
self._read_len = self._offset
return content