Skip to content

Commit 0bea639

Browse files
feat: Add support for prefix ciphertext decryption (closed #20)
1 parent cb810f0 commit 0bea639

File tree

5 files changed

+200
-82
lines changed

5 files changed

+200
-82
lines changed

bkcrypto/contrib/django/ciphers.py

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
1010
specific language governing permissions and limitations under the License.
1111
"""
12+
import abc
1213
import typing
1314

1415
from bkcrypto.asymmetric.ciphers import BaseAsymmetricCipher
@@ -17,7 +18,7 @@
1718
from bkcrypto.symmetric.ciphers import BaseSymmetricCipher
1819
from bkcrypto.symmetric.options import SymmetricOptions
1920

20-
from .init_configs import AsymmetricCipherInitConfig, SymmetricCipherInitConfig
21+
from .init_configs import CipherInitConfig
2122
from .settings import crypto_settings
2223

2324

@@ -47,54 +48,86 @@ def get_symmetric_cipher(
4748
)
4849

4950

50-
class SymmetricCipherManager:
51-
_cache: typing.Optional[typing.Dict[str, BaseSymmetricCipher]] = None
51+
class BaseCipherManager(abc.ABC):
5252

53-
def __init__(self):
54-
self._cache: [str, BaseSymmetricCipher] = {}
53+
_cache: typing.Dict[str, typing.Any] = None
5554

56-
def cipher(
57-
self, using: typing.Optional[str] = None, cipher_type: typing.Optional[str] = None
58-
) -> BaseSymmetricCipher:
55+
def __init__(self):
56+
self._cache = {}
5957

58+
def _get_init_config(self, using: typing.Optional[str] = None) -> CipherInitConfig:
6059
using: str = using or "default"
61-
if using not in crypto_settings.SYMMETRIC_CIPHERS:
60+
init_configs: typing.Dict[str, CipherInitConfig] = self._get_init_configs_from_settings()
61+
if using not in init_configs:
6262
raise RuntimeError(f"Invalid using {using}")
63+
return init_configs[using]
64+
65+
@abc.abstractmethod
66+
def _get_init_configs_from_settings(self) -> typing.Dict[str, CipherInitConfig]:
67+
raise NotImplementedError
68+
69+
@abc.abstractmethod
70+
def _get_cipher_type_from_settings(self) -> str:
71+
raise NotImplementedError
6372

64-
cipher_type: str = cipher_type or crypto_settings.SYMMETRIC_CIPHER_TYPE
73+
@abc.abstractmethod
74+
def _get_cipher(self, cipher_type: str, init_config: CipherInitConfig):
75+
raise NotImplementedError
76+
77+
def _cipher(self, using: typing.Optional[str] = None, cipher_type: typing.Optional[str] = None):
78+
79+
# try to get cipher from cache
80+
cipher_type: str = cipher_type or self._get_cipher_type_from_settings()
6581
cache_key: str = f"{using}-{cipher_type}"
6682
if cache_key in self._cache:
6783
return self._cache[cache_key]
6884

69-
init_config: SymmetricCipherInitConfig = crypto_settings.SYMMETRIC_CIPHERS[using]
70-
cipher: BaseSymmetricCipher = get_symmetric_cipher(**init_config.as_get_cipher_params(cipher_type))
71-
self._cache[cache_key] = cipher
72-
return cipher
85+
# create & cache instance
86+
init_config: CipherInitConfig = self._get_init_config(using=using)
87+
self._cache[cache_key] = self._get_cipher(cipher_type, init_config)
88+
return self._cache[cache_key]
7389

90+
@abc.abstractmethod
91+
def cipher(self, using: typing.Optional[str] = None, cipher_type: typing.Optional[str] = None):
92+
raise NotImplementedError
7493

75-
class AsymmetricCipherManager:
76-
_cache: typing.Optional[typing.Dict[str, BaseAsymmetricCipher]] = None
7794

78-
def __init__(self):
79-
self._cache: [str, BaseAsymmetricCipher] = {}
95+
class SymmetricCipherManager(BaseCipherManager):
96+
97+
_cache: typing.Optional[typing.Dict[str, BaseSymmetricCipher]] = None
98+
99+
def _get_init_configs_from_settings(self) -> typing.Dict[str, CipherInitConfig]:
100+
return crypto_settings.SYMMETRIC_CIPHERS
101+
102+
def _get_cipher_type_from_settings(self) -> str:
103+
return crypto_settings.SYMMETRIC_CIPHER_TYPE
104+
105+
def _get_cipher(self, cipher_type: str, init_config: CipherInitConfig) -> BaseSymmetricCipher:
106+
return get_symmetric_cipher(**init_config.as_get_cipher_params(cipher_type))
80107

81108
def cipher(
82109
self, using: typing.Optional[str] = None, cipher_type: typing.Optional[str] = None
83-
) -> BaseAsymmetricCipher:
110+
) -> BaseSymmetricCipher:
111+
return self._cipher(using, cipher_type)
84112

85-
using: str = using or "default"
86-
if using not in crypto_settings.ASYMMETRIC_CIPHERS:
87-
raise RuntimeError(f"Invalid using {using}")
88113

89-
cipher_type: str = cipher_type or crypto_settings.ASYMMETRIC_CIPHER_TYPE
90-
cache_key: str = f"{using}-{cipher_type}"
91-
if cache_key in self._cache:
92-
return self._cache[cache_key]
114+
class AsymmetricCipherManager(BaseCipherManager):
93115

94-
init_config: AsymmetricCipherInitConfig = crypto_settings.ASYMMETRIC_CIPHERS[using]
95-
cipher: BaseAsymmetricCipher = get_asymmetric_cipher(**init_config.as_get_cipher_params(cipher_type))
96-
self._cache[cache_key] = cipher
97-
return cipher
116+
_cache: typing.Optional[typing.Dict[str, BaseAsymmetricCipher]] = None
117+
118+
def _get_init_configs_from_settings(self) -> typing.Dict[str, CipherInitConfig]:
119+
return crypto_settings.ASYMMETRIC_CIPHERS
120+
121+
def _get_cipher_type_from_settings(self) -> str:
122+
return crypto_settings.ASYMMETRIC_CIPHER_TYPE
123+
124+
def _get_cipher(self, cipher_type: str, init_config: CipherInitConfig) -> BaseAsymmetricCipher:
125+
return get_asymmetric_cipher(**init_config.as_get_cipher_params(cipher_type))
126+
127+
def cipher(
128+
self, using: typing.Optional[str] = None, cipher_type: typing.Optional[str] = None
129+
) -> BaseAsymmetricCipher:
130+
return self._cipher(using, cipher_type)
98131

99132

100133
symmetric_cipher_manager = SymmetricCipherManager()

bkcrypto/contrib/django/fields.py

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,10 @@
1313

1414
from django.db import models
1515

16-
from bkcrypto.contrib.django.ciphers import symmetric_cipher_manager
17-
from bkcrypto.contrib.django.init_configs import SymmetricCipherInitConfig
18-
from bkcrypto.contrib.django.settings import crypto_settings
19-
from bkcrypto.symmetric.ciphers.base import BaseSymmetricCipher
16+
from bkcrypto.contrib.django.selectors import SymmetricCipherSelectorMixin
2017

2118

22-
class SymmetricFieldMixin:
23-
24-
cipher: BaseSymmetricCipher = None
25-
26-
# 是否指定固定前缀,如果不为 None,密文将统一使用 prefix 作为前缀
27-
prefix: str = None
28-
# 指定对称加密实例,默认使用 `default`
29-
using: str = None
30-
31-
def prefix_selector(self, value: str) -> typing.Tuple[bool, str, typing.Optional[str]]:
32-
"""
33-
密文前缀匹配,用于提取可能存在的加密类型
34-
:param value:
35-
:return:
36-
"""
37-
if self.prefix is not None:
38-
if value.startswith(self.prefix):
39-
return True, value[len(self.prefix) :], None
40-
else:
41-
return False, value, None
42-
else:
43-
init_config: SymmetricCipherInitConfig = crypto_settings.SYMMETRIC_CIPHERS[self.using]
44-
for prefix, cipher_type in init_config.prefix_cipher_type_map.items():
45-
if value.startswith(prefix):
46-
return True, value[len(prefix) :], cipher_type
47-
return False, value, None
48-
19+
class SymmetricFieldMixin(SymmetricCipherSelectorMixin):
4920
def deconstruct(self):
5021
name, path, args, kwargs = super().deconstruct()
5122
if self.prefix is not None:
@@ -63,15 +34,7 @@ def get_decrypted_value(self, value):
6334
if value is None:
6435
return value
6536

66-
is_match, trusted_value, cipher_type = self.prefix_selector(value)
67-
if is_match:
68-
try:
69-
cipher: BaseSymmetricCipher = symmetric_cipher_manager.cipher(using=self.using, cipher_type=cipher_type)
70-
value = cipher.decrypt(trusted_value)
71-
except Exception:
72-
pass
73-
74-
return value
37+
return self.decrypt(value)
7538

7639
def from_db_value(self, value, expression, connection, context=None):
7740
"""出库后解密数据"""
@@ -102,16 +65,7 @@ def get_prep_value(self, value):
10265
if hasattr(sp, "get_prep_value"):
10366
value = sp.get_prep_value(value)
10467

105-
if self.prefix is not None:
106-
prefix: str = self.prefix
107-
else:
108-
init_config: SymmetricCipherInitConfig = crypto_settings.SYMMETRIC_CIPHERS[self.using]
109-
prefix: str = init_config.db_prefix_map[crypto_settings.SYMMETRIC_CIPHER_TYPE]
110-
111-
cipher: BaseSymmetricCipher = symmetric_cipher_manager.cipher(using=self.using)
112-
value = prefix + cipher.encrypt(value)
113-
114-
return value
68+
return self.encrypt(value)
11569

11670

11771
class SymmetricTextField(SymmetricFieldMixin, models.TextField):
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云 - crypto-python-sdk
4+
(BlueKing - crypto-python-sdk) available.
5+
Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
6+
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at https://opensource.org/licenses/MIT
8+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
9+
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
10+
specific language governing permissions and limitations under the License.
11+
"""
12+
import abc
13+
import typing
14+
15+
from bkcrypto.asymmetric.ciphers import BaseAsymmetricCipher
16+
from bkcrypto.contrib.django.ciphers import asymmetric_cipher_manager, symmetric_cipher_manager
17+
from bkcrypto.contrib.django.init_configs import CipherInitConfig
18+
from bkcrypto.contrib.django.settings import crypto_settings
19+
from bkcrypto.symmetric.ciphers.base import BaseSymmetricCipher
20+
21+
22+
class CipherSelectorMixin:
23+
init_config: CipherInitConfig = None
24+
25+
# 是否指定固定前缀,如果不为 None,密文将统一使用 prefix 作为前缀
26+
prefix: str = None
27+
# 指定对称加密实例,默认使用 `default`
28+
using: str = None
29+
30+
@abc.abstractmethod
31+
def _get_cipher_type_from_settings(self) -> str:
32+
raise NotImplementedError
33+
34+
@abc.abstractmethod
35+
def get_cipher(self, cipher_type: typing.Optional[str] = None):
36+
raise NotImplementedError
37+
38+
@abc.abstractmethod
39+
def get_init_config(self) -> CipherInitConfig:
40+
raise NotImplementedError
41+
42+
def prefix_selector(self, ciphertext_with_prefix: str) -> typing.Tuple[bool, str, typing.Optional[str]]:
43+
"""
44+
密文前缀匹配,用于提取可能存在的加密类型
45+
:param ciphertext_with_prefix:
46+
:return:
47+
"""
48+
if self.prefix is not None:
49+
if ciphertext_with_prefix.startswith(self.prefix):
50+
return True, ciphertext_with_prefix[len(self.prefix) :], None
51+
else:
52+
return False, ciphertext_with_prefix, None
53+
else:
54+
for prefix, cipher_type in self.get_init_config().prefix_cipher_type_map.items():
55+
if ciphertext_with_prefix.startswith(prefix):
56+
return True, ciphertext_with_prefix[len(prefix) :], cipher_type
57+
return False, ciphertext_with_prefix, None
58+
59+
def encrypt(self, plaintext: str) -> str:
60+
if self.prefix is not None:
61+
prefix: str = self.prefix
62+
else:
63+
prefix: str = self.get_init_config().db_prefix_map[self._get_cipher_type_from_settings()]
64+
65+
cipher = self.get_cipher()
66+
ciphertext_with_prefix: str = prefix + cipher.encrypt(plaintext)
67+
return ciphertext_with_prefix
68+
69+
def decrypt(self, ciphertext_with_prefix: str) -> str:
70+
71+
is_match, trusted_value, cipher_type = self.prefix_selector(ciphertext_with_prefix)
72+
if is_match:
73+
try:
74+
# 解密时使用前缀匹配到的算法
75+
cipher = self.get_cipher(cipher_type=cipher_type)
76+
plaintext: str = cipher.decrypt(trusted_value)
77+
except Exception:
78+
return ciphertext_with_prefix
79+
else:
80+
return ciphertext_with_prefix
81+
82+
return plaintext
83+
84+
85+
class SymmetricCipherSelectorMixin(CipherSelectorMixin):
86+
def _get_cipher_type_from_settings(self) -> str:
87+
return crypto_settings.SYMMETRIC_CIPHER_TYPE
88+
89+
def get_cipher(self, cipher_type: typing.Optional[str] = None) -> BaseSymmetricCipher:
90+
return symmetric_cipher_manager.cipher(using=self.using, cipher_type=cipher_type)
91+
92+
def get_init_config(self) -> CipherInitConfig:
93+
return crypto_settings.SYMMETRIC_CIPHERS[self.using]
94+
95+
96+
class AsymmetricCipherSelectorMixin(CipherSelectorMixin):
97+
def _get_cipher_type_from_settings(self) -> str:
98+
return crypto_settings.ASYMMETRIC_CIPHER_TYPE
99+
100+
def get_cipher(self, cipher_type: typing.Optional[str] = None) -> BaseAsymmetricCipher:
101+
return asymmetric_cipher_manager.cipher(using=self.using, cipher_type=cipher_type)
102+
103+
def get_init_config(self) -> CipherInitConfig:
104+
return crypto_settings.ASYMMETRIC_CIPHERS[self.using]
105+
106+
107+
class CipherSelector:
108+
def __init__(self, using: typing.Optional[str] = None, prefix: typing.Optional[str] = None):
109+
"""
110+
对称加密
111+
:param using: 指定对称加密实例,默认使用 `default`
112+
:param prefix: 是否指定固定前缀,如果不为 None,密文将统一使用 prefix 作为前缀
113+
"""
114+
self.prefix = prefix
115+
self.using = using or "default"
116+
117+
118+
class SymmetricCipherSelector(SymmetricCipherSelectorMixin, CipherSelector):
119+
pass
120+
121+
122+
class AsymmetricCipherSelector(AsymmetricCipherSelectorMixin, CipherSelector):
123+
pass

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "bk-crypto-python-sdk"
3-
version = "1.0.4"
3+
version = "1.1.0"
44
description = "bk-crypto-python-sdk is a lightweight cryptography toolkit for Python applications based on Cryptodome / tongsuopy and other encryption libraries."
55
authors = ["TencentBlueKing <[email protected]>"]
66
readme = "readme.md"

release.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 版本日志
1+
**# 版本日志
22

33
## 1.0.0 - 2023-07-03
44

@@ -38,3 +38,11 @@
3838
### Fixed
3939

4040
* [ Fixed ] Fix the issue of "Too many arguments for this mode" in AES CTR mode ([#16](https://github.com/TencentBlueKing/crypto-python-sdk/issues/16))
41+
42+
43+
## 1.1.0 - 2023-08-07
44+
45+
### Feature
46+
47+
* [ Feature ] Add support for non-Django projects ([#19](https://github.com/TencentBlueKing/crypto-python-sdk/issues/19))
48+
* [ Feature ] Add support for prefix ciphertext decryption ([#20](https://github.com/TencentBlueKing/crypto-python-sdk/issues/20))

0 commit comments

Comments
 (0)