easy-encryption-tool 2.6.0__tar.gz → 2.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/PKG-INFO +200 -31
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/__init__.py +5 -1
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/aes_command.py +9 -4
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/cert_parse.py +59 -5
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/common.py +15 -4
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/convert_command.py +9 -4
- easy_encryption_tool-2.8.2/easy_encryption_tool/doctor_command.py +123 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/ecc_command.py +10 -5
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/envelope_command.py +49 -13
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/hints.py +10 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/hmac_command.py +2 -4
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/int_bytes_command.py +2 -1
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/tencentcloud.py +3 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/main.py +15 -48
- easy_encryption_tool-2.8.2/easy_encryption_tool/pqc_command.py +902 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/rich_ui.py +3 -1
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/rsa_command.py +5 -1
- easy_encryption_tool-2.8.2/easy_encryption_tool/shell_command.py +413 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/sm2_command.py +2 -1
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/sm4_command.py +22 -9
- easy_encryption_tool-2.8.2/easy_encryption_tool/smart_group.py +79 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/aad.py +8 -1
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/errors.py +10 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/types.py +3 -1
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured_command.py +2 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/tool_version.py +24 -1
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/ts_command.py +2 -1
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/PKG-INFO +200 -31
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/SOURCES.txt +8 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/requires.txt +4 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/setup.py +2 -0
- easy_encryption_tool-2.8.2/tests/test_doctor_command.py +41 -0
- easy_encryption_tool-2.8.2/tests/test_fix_281.py +270 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_main.py +14 -1
- easy_encryption_tool-2.8.2/tests/test_pqc_command.py +454 -0
- easy_encryption_tool-2.8.2/tests/test_shell_command.py +307 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/cipherhub_defaults.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/command_perf.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/crypto/__init__.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/hash_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/__init__.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/availability.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/__init__.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/aliyun.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/aws.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/azure.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/googlecloud.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/hashicorpvault.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/huaweicloud.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/config.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/errors.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/output_builder.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/output_renderer.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/random_str.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/shell_completion.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/__init__.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/format_detect.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/ini_store.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/json_store.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/keys.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/mac.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/policy.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/token.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/traversal.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/yaml_store.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/validators.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/zuc_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/dependency_links.txt +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/entry_points.txt +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/not-zip-safe +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/top_level.txt +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/setup.cfg +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/__init__.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/conftest.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/sm4_cbc_demo.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_aes_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_cert_parse.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_cipherhub_compatibility.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_command_perf.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_common.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_crypto.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_crypto_core.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_ecc_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_hash_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_hints.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_hmac_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_output_contract.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_random_str.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_rich_ui.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_rsa_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_sm2_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_sm4_command.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_structured_regressions.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_tool_version.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_validators.py +0 -0
- {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_zuc_command.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: easy_encryption_tool
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.2
|
|
4
4
|
Summary: 易加密 CLI 工具,支持 AES/SM4/ZUC/RSA/ECC/SM2 等加解密与签名验签
|
|
5
5
|
Home-page: https://cipherhub.cloud
|
|
6
6
|
Author: bowenerchen
|
|
@@ -30,8 +30,11 @@ Requires-Dist: requests>=2.28
|
|
|
30
30
|
Requires-Dist: pyopenssl>=24.0
|
|
31
31
|
Requires-Dist: rich>=13.0
|
|
32
32
|
Requires-Dist: rich-click>=1.9.1
|
|
33
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
33
34
|
Provides-Extra: gmssl
|
|
34
35
|
Requires-Dist: easy_gmssl>=2.0.1; extra == "gmssl"
|
|
36
|
+
Provides-Extra: pqc
|
|
37
|
+
Requires-Dist: cryptography>=45.0; extra == "pqc"
|
|
35
38
|
Provides-Extra: kms
|
|
36
39
|
Requires-Dist: boto3>=1.26; extra == "kms"
|
|
37
40
|
Requires-Dist: tencentcloud-sdk-python-kms>=3.0.0; extra == "kms"
|
|
@@ -44,6 +47,19 @@ Requires-Dist: azure-identity>=1.15.0; extra == "kms"
|
|
|
44
47
|
Provides-Extra: test
|
|
45
48
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
46
49
|
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
50
|
+
Dynamic: author
|
|
51
|
+
Dynamic: author-email
|
|
52
|
+
Dynamic: classifier
|
|
53
|
+
Dynamic: description
|
|
54
|
+
Dynamic: description-content-type
|
|
55
|
+
Dynamic: home-page
|
|
56
|
+
Dynamic: keywords
|
|
57
|
+
Dynamic: license
|
|
58
|
+
Dynamic: project-url
|
|
59
|
+
Dynamic: provides-extra
|
|
60
|
+
Dynamic: requires-dist
|
|
61
|
+
Dynamic: requires-python
|
|
62
|
+
Dynamic: summary
|
|
47
63
|
|
|
48
64
|
# 易加密(easy_encryption_tool)
|
|
49
65
|
|
|
@@ -71,6 +87,20 @@ pip install easy_gmssl
|
|
|
71
87
|
|
|
72
88
|
**依赖国密算法的命令**:`hash -a sm3`、`hmac -a sm3`、`sm2`、`sm4`、`zuc`、`cert-parse -g`。未安装 `easy_gmssl` 时,执行这些命令会提示:`xxx 需要 easy_gmssl 库,请执行: pip install easy_gmssl`。
|
|
73
89
|
|
|
90
|
+
### 后量子算法支持(可选)
|
|
91
|
+
|
|
92
|
+
如需使用 **后量子算法**(ML-KEM 密钥封装、ML-DSA 签名),需要 `cryptography >= 45`:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 推荐:通过 [pqc] extra 一次性安装并保证 cryptography 版本
|
|
96
|
+
pip install "easy-encryption-tool[pqc]"
|
|
97
|
+
|
|
98
|
+
# 或:单独升级 cryptography(若已安装 easy-encryption-tool)
|
|
99
|
+
pip install "cryptography>=45"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**依赖后量子的命令**:`pqc mlkem`、`pqc mldsa`。cryptography 版本过旧时,执行这些命令会提示升级;参数集(如 ML-KEM-512)是否可用取决于 cryptography 版本,`easy_encryption_tool version` 的 Dependencies 行会列出。
|
|
103
|
+
|
|
74
104
|
项目地址:[easy-encryption-tool · PyPI](https://pypi.org/project/easy-encryption-tool/)
|
|
75
105
|
|
|
76
106
|
---
|
|
@@ -102,9 +132,11 @@ Options:
|
|
|
102
132
|
Commands:
|
|
103
133
|
aes aes加解密工具,默认支持 aes-cbc-256 和 aes-gcm-256
|
|
104
134
|
cert-parse 解析 pem 或 der 格式的证书,支持国际算法及国密 SM2(-g)
|
|
135
|
+
doctor 环境与可选能力体检(cryptography/PQC、easy_gmssl、七家云 KMS)
|
|
105
136
|
ecc ecc签名验签和密钥交换验证工具
|
|
106
137
|
hash 哈希摘要工具,支持 SM3、SHA256、SHA384、SHA512
|
|
107
138
|
hmac hmac消息验证码工具,支持 SM3 及国际哈希算法
|
|
139
|
+
pqc 后量子命令组:ML-KEM 密钥封装与 ML-DSA 签名验签(需 cryptography>=45)
|
|
108
140
|
random-str 随机字符串生成器
|
|
109
141
|
rsa rsa加解密和签名验签工具
|
|
110
142
|
sm2 国密 SM2 加解密和签名验签工具(多种密文/签名格式)
|
|
@@ -126,6 +158,31 @@ source ~/.easy_encryption_tool_complete.sh
|
|
|
126
158
|
|
|
127
159
|
补全能力包括:子命令补全、选项补全、固定可选值补全(如 `-A` encrypt/decrypt、`-m` cbc/gcm)、文件路径补全(如 `-i` 在指定 `-f` 时、`-o`、密钥文件路径等)。详见 [CLI 使用说明](../docs/CLI_USAGE_GUIDE.md#4-install-completion---shell-自动补全)。
|
|
128
160
|
|
|
161
|
+
## 环境与能力体检(eet doctor)
|
|
162
|
+
|
|
163
|
+
一行命令检查本机环境与全部可选能力,缺什么给什么修复命令:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
❯ easy_encryption_tool doctor
|
|
167
|
+
╭─ eet doctor ─╮
|
|
168
|
+
│ python ✓ ok 3.12.7 │
|
|
169
|
+
│ eet ✓ ok v2.8.1 │
|
|
170
|
+
│ cryptography ✓ ok 48.0.0 │
|
|
171
|
+
│ pqc ✓ ok available (ML-KEM-768/1024, ML-DSA-44/65/87) │
|
|
172
|
+
│ easy_gmssl ✓ ok 2.0.1 (SM2/SM3/SM4/ZUC) │
|
|
173
|
+
│ ── cloud KMS backends ── │
|
|
174
|
+
│ kms aws ✓ ok boto3 │
|
|
175
|
+
│ kms tencentcloud ✓ ok tencentcloud.kms.v20190118 │
|
|
176
|
+
│ kms aliyun ✓ ok alibabacloud_kms20160120 │
|
|
177
|
+
│ kms huawei ✓ ok huaweicloudsdkkms │
|
|
178
|
+
│ kms google ✓ ok google.cloud.kms_v1 │
|
|
179
|
+
│ kms vault ✓ ok hvac │
|
|
180
|
+
│ kms azure ✗ missing azure.keyvault.keys.crypto │
|
|
181
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
缺失项的 Fix 列给出对应的 `pip install` 命令(如 `pip install 'easy-encryption-tool[kms]'`)。`doctor` 输出纯表格,不产生 JSON,适合直接阅读与截图排障。
|
|
185
|
+
|
|
129
186
|
## 显示版本
|
|
130
187
|
|
|
131
188
|
```python
|
|
@@ -197,41 +254,92 @@ try write to test_random failed
|
|
|
197
254
|
|
|
198
255
|
```python
|
|
199
256
|
❯ easy_encryption_tool aes --help
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
257
|
+
|
|
258
|
+
Usage: eet aes [OPTIONS]
|
|
259
|
+
|
|
260
|
+
AES encrypt/decrypt. Encrypt: each missing key/IV piece (-k/-v or b64
|
|
261
|
+
variants) is generated from full-entropy CSPRNG bytes and output as base64.
|
|
262
|
+
Decrypt: requires explicit key/IV (--key-b64/--iv-b64 or -k/-v). Input:
|
|
263
|
+
plaintext (-i), base64 (-e), or file (-f).
|
|
264
|
+
|
|
265
|
+
╭─ Options ────────────────────────────────────────────────────────────────────╮
|
|
266
|
+
│ --mode -m [cbc|gcm] AES mode: cbc or gcm │
|
|
267
|
+
│ [default: cbc] │
|
|
268
|
+
│ --key -k TEXT Key string (normalized to 32 │
|
|
269
|
+
│ bytes / 256-bit, │
|
|
270
|
+
│ CipherHUB-compatible) │
|
|
271
|
+
│ [default: │
|
|
272
|
+
│ 01234567890123456789012345678 │
|
|
273
|
+
│ 901] │
|
|
274
|
+
│ --iv-nonce -v TEXT CBC uses IV (16 bytes); GCM │
|
|
275
|
+
│ uses nonce (12 bytes). String │
|
|
276
|
+
│ input is normalized for │
|
|
277
|
+
│ compatibility [default: │
|
|
278
|
+
│ 0123456789012345] │
|
|
279
|
+
│ --aad TEXT GCM additional authenticated │
|
|
280
|
+
│ data (AAD), default: │
|
|
281
|
+
│ "密码学人 CipherHUB 默认 AAD │
|
|
282
|
+
│ 数据" [default: 密码学人 │
|
|
283
|
+
│ CipherHUB 默认 AAD 数据] │
|
|
284
|
+
│ --random-key-iv -r Force both key and IV/nonce │
|
|
285
|
+
│ random (encrypt only). On │
|
|
286
|
+
│ encrypt any missing │
|
|
287
|
+
│ -k/-v/--key-b64/--iv-b64 │
|
|
288
|
+
│ piece is randomly generated │
|
|
289
|
+
│ anyway and shown as base64; │
|
|
290
|
+
│ use --key-b64/--iv-b64 for │
|
|
291
|
+
│ decrypt. Mutually exclusive │
|
|
292
|
+
│ with │
|
|
293
|
+
│ -k/-v/--key-b64/--iv-b64. │
|
|
294
|
+
│ --key-b64 TEXT Base64 key bytes (must decode │
|
|
295
|
+
│ to 32 bytes). Use for decrypt │
|
|
296
|
+
│ with key from encrypt -r │
|
|
297
|
+
│ output. Mutually exclusive │
|
|
298
|
+
│ with -k │
|
|
299
|
+
│ --iv-b64 TEXT Base64 IV/nonce bytes (CBC │
|
|
300
|
+
│ 16, GCM 12). Use for decrypt │
|
|
301
|
+
│ with IV from encrypt -r │
|
|
302
|
+
│ output. Mutually exclusive │
|
|
303
|
+
│ with -v │
|
|
304
|
+
│ --action -A [encrypt|decrypt] Action: encrypt or decrypt │
|
|
305
|
+
│ [default: encrypt] │
|
|
306
|
+
│ * --input-data -i TEXT Input data. Encrypt: │
|
|
307
|
+
│ text/base64/file. Decrypt: │
|
|
308
|
+
│ base64/file [required] │
|
|
309
|
+
│ --is-base64-encoded -e Interpret -i as base64 data │
|
|
310
|
+
│ (mutually exclusive with -f) │
|
|
311
|
+
│ --is-a-file -f Interpret -i as file path │
|
|
312
|
+
│ (mutually exclusive with -e) │
|
|
313
|
+
│ --input-limit -l INTEGER Max non-file input size in MB │
|
|
314
|
+
│ [default: 1] │
|
|
315
|
+
│ --output-file -o TEXT Output file path (required │
|
|
316
|
+
│ with -f) │
|
|
317
|
+
│ --no-force Do not overwrite an existing │
|
|
318
|
+
│ output file │
|
|
319
|
+
│ --gcm-pad [GCM only] Enable PKCS#7 │
|
|
320
|
+
│ padding before encrypt and │
|
|
321
|
+
│ unpadding after decrypt. │
|
|
322
|
+
│ Default is no padding; │
|
|
323
|
+
│ encrypt and decrypt must both │
|
|
324
|
+
│ use or both omit --gcm-pad. │
|
|
325
|
+
│ File decrypt with --gcm-pad │
|
|
326
|
+
│ loads entire file into │
|
|
327
|
+
│ memory; large files may cause │
|
|
328
|
+
│ OOM. │
|
|
329
|
+
│ --pretty-json Output colored pretty JSON │
|
|
330
|
+
│ (default) │
|
|
331
|
+
│ --json Output single-line JSON (use │
|
|
332
|
+
│ with jq for field extraction) │
|
|
333
|
+
│ --help Show this message and exit. │
|
|
334
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
227
335
|
```
|
|
228
336
|
|
|
229
337
|
### 关于密钥、IV和模式的预设
|
|
230
338
|
|
|
231
339
|
- 加密模式:仅支持 CBC 模式和 GCM 模式
|
|
232
340
|
- **填充规则**:CBC 模式始终按 PKCS#7 填充;GCM 模式**默认不填充**(与 CipherHUB stream_cipher 兼容),若需填充可加 `--gcm-pad`,加密与解密需一致使用
|
|
233
|
-
-
|
|
234
|
-
- IV:CBC 模式下时 IV 长度默认 16 字节,GCM 模式下 Nonce 长度默认 12 字节(其中 4
|
|
341
|
+
- 密钥:长度必须恰好 32 字节(256 位)。加密时完全不给则随机生成并以 base64 输出;**给了但不足 32 字节会被拒绝**(2.8.1 起不再随机补齐——补齐过的密钥无法复现,密文将永远解不开);超过 32 字节会截断并警告
|
|
342
|
+
- IV:CBC 模式下时 IV 长度默认 16 字节,GCM 模式下 Nonce 长度默认 12 字节(其中 4 字节预留作为计数器,由算法自行处理);与密钥同理——不给则加密时随机生成,给短了拒绝,给长了截断并警告
|
|
235
343
|
|
|
236
344
|
### 关于输入数据的预设
|
|
237
345
|
|
|
@@ -1416,6 +1524,67 @@ mode:ECDSA
|
|
|
1416
1524
|
------ d1c74d3acc46413b took 15.220 milli-seconds to execute ------
|
|
1417
1525
|
```
|
|
1418
1526
|
|
|
1527
|
+
## PQC 后量子(ML-KEM / ML-DSA)
|
|
1528
|
+
|
|
1529
|
+
后量子命令组 `pqc`,提供 **ML-KEM**(FIPS 203 密钥封装)与 **ML-DSA**(FIPS 204 签名)能力,需要 `cryptography >= 45`。参数集可用性取决于所装 cryptography 版本(`easy_encryption_tool version` 的 Dependencies 行会列出当前可用参数集)。
|
|
1530
|
+
|
|
1531
|
+
与 [CipherHUB 工具站](https://cipherhub.cloud) 的互通关系:**公钥可互换**(双方均为 NIST 标准 OID 的 SPKI/PKCS#8,工具站后端 liboqs 生成的公钥可直接喂给本工具 encap/verify);**私钥不可互换**(本工具私钥为 PKCS#8 种子形态加密存储,工具站为展开形态,二者格式不同)。
|
|
1532
|
+
|
|
1533
|
+
### 生成 ML-KEM 密钥对
|
|
1534
|
+
|
|
1535
|
+
```shell
|
|
1536
|
+
# 私钥强制密码保护:-p 指定密码,或 -r 随机生成(两者都不传默认随机)
|
|
1537
|
+
❯ easy_encryption_tool pqc mlkem generate -f demo -p 1234567890 --json
|
|
1538
|
+
|
|
1539
|
+
{"metadata": {"operation": "generate", "algorithm": "ML-KEM-768", "parameters": {"encoding": "pem", "seed_derived": false}}, "result": {"public_file": "demo_mlkem768_public.pem", "private_file": "demo_mlkem768_private_cipher.pem", "password": "1234567890"}, "runtime": {"request_id": "a2bac659-2d54-47f2-be3f-aab0fa28174c", "timestamp": "2026-08-28T15:07:32.353623+08:00", "duration_ms": 9.809, "host": "MacM2Max-2.local", "version": "v2.7.0"}}
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
`-a/--algorithm` 可选 `ML-KEM-512`、`ML-KEM-768`(默认)、`ML-KEM-1024`;`--seed-in-hex` 接受 64 字节 hex(128 个十六进制字符)做确定性派生,仅供教学与向量复现,生产环境留空即随机生成。
|
|
1543
|
+
|
|
1544
|
+
### ML-KEM 封装与解封装
|
|
1545
|
+
|
|
1546
|
+
发送方持有公钥做封装,得到密文与共享秘密;接收方持私钥解封装还原同一个共享秘密(典型用法:共享秘密作为对称加密的密钥材料)。
|
|
1547
|
+
|
|
1548
|
+
```shell
|
|
1549
|
+
# 封装:输出 ciphertext(交给孩子信道传输)与 shared_secret(发送方本地留存)
|
|
1550
|
+
❯ easy_encryption_tool pqc mlkem encap -f demo_mlkem768_public.pem --json
|
|
1551
|
+
|
|
1552
|
+
{"metadata": {"operation": "encap", "algorithm": "ML-KEM-768", "input": {"type": "key"}, "parameters": {"ciphertext_bytes": 1088}}, "result": {"ciphertext_in_hex": "cbf390d5c3a98de7ece00606...(共 2176 个 hex 字符)", "ciphertext_in_base64": "y/OQ1cOpjefs4AYGeHQe/Cf2knCg...(共 1452 个字符)", "ciphertext_bytes": 1088, "shared_secret_in_hex": "ca4954152db0dd42d6e50ff35ecc51a7590bc487cf7339ee352610cc4b7a56c3", "shared_secret_in_base64": "yklUFS2w3ULW5Q/zXsxRp1kLxIfPcznuNSYQzEt6VsM=", "shared_secret_sha256": "b71c5ae5a9132597df7a96250af8b029b24ec1e91ce92cc18de699dd8f35b8cc", "shared_secret_bytes": 32}, "runtime": {...}}
|
|
1553
|
+
|
|
1554
|
+
# 解封装:-i 传入封装输出的 ciphertext_in_base64
|
|
1555
|
+
❯ easy_encryption_tool pqc mlkem decap -f demo_mlkem768_private_cipher.pem -p 1234567890 -i "<ciphertext_in_base64>" --json
|
|
1556
|
+
|
|
1557
|
+
{"metadata": {"operation": "decap", "algorithm": "ML-KEM-768", "input": {"type": "binary", "size": 1088}}, "result": {"shared_secret_in_hex": "9b907dd7844ce9dd906e645ecaaa95936b18b43d7f66b21ae2ef265496ef1f7a", "shared_secret_in_base64": "m5B914RM6d2QbmReyqqVk2sYtD1/ZrIa4u8mVJbvH3o=", "shared_secret_sha256": "c8f5e74ba6ca40c5cef8d72de727c67183b256359c2639fb489e54abcc167697", "shared_secret_bytes": 32}, "runtime": {...}}
|
|
1558
|
+
```
|
|
1559
|
+
|
|
1560
|
+
注意两次 `shared_secret` 不同是正常的:ML-KEM 封装本身随机化,同一条 ciphertext 的 encap/decap 结果必然一致(上例两段输出来自两次独立的封装)。按 FIPS 203 的隐式拒绝语义,篡改后的 ciphertext 解封装不报错,但得到的共享秘密必然不同——比对 `shared_secret_sha256` 即可发现。
|
|
1561
|
+
|
|
1562
|
+
### 生成 ML-DSA 密钥对
|
|
1563
|
+
|
|
1564
|
+
```shell
|
|
1565
|
+
❯ easy_encryption_tool pqc mldsa generate -f demo -p 1234567890 --json
|
|
1566
|
+
|
|
1567
|
+
{"metadata": {"operation": "generate", "algorithm": "ML-DSA-65", "parameters": {"encoding": "pem", "seed_derived": false}}, "result": {"public_file": "demo_mldsa65_public.pem", "private_file": "demo_mldsa65_private_cipher.pem", "password": "1234567890"}, "runtime": {...}}
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
`-a/--algorithm` 可选 `ML-DSA-44`、`ML-DSA-65`(默认)、`ML-DSA-87`;`--seed-in-hex` 接受 32 字节 hex(64 个十六进制字符)做确定性派生,仅供教学与向量复现。
|
|
1571
|
+
|
|
1572
|
+
### ML-DSA 签名与验签
|
|
1573
|
+
|
|
1574
|
+
```shell
|
|
1575
|
+
# 签名:--context 为可选域分隔字符串(最长 255 字节),验签时必须传同一个值
|
|
1576
|
+
❯ easy_encryption_tool pqc mldsa sign -f demo_mldsa65_private_cipher.pem -p 1234567890 -i "hello,post-quantum" --context eet-demo --json
|
|
1577
|
+
|
|
1578
|
+
{"metadata": {"operation": "sign", "algorithm": "ML-DSA-65", "input": {"type": "text", "size": 18}, "parameters": {"context": "eet-demo"}}, "result": {"signature_in_hex": "b2de9bf26b6685e2b64d8c348ccf726f...(共 6618 个 hex 字符)", "signature_in_base64": "st6b8mtmheK2TYw0jM9ybwQwSnPvYWp2M+Yk...(共 4412 个字符)", "signature_bytes": 3309}, "runtime": {...}}
|
|
1579
|
+
|
|
1580
|
+
# 验签:-s 传入签名输出的 signature_in_base64,--context 必须与签名时一致
|
|
1581
|
+
❯ easy_encryption_tool pqc mldsa verify -f demo_mldsa65_public.pem -i "hello,post-quantum" --context eet-demo -s "<signature_in_base64>" --json
|
|
1582
|
+
|
|
1583
|
+
{"metadata": {"operation": "verify", "algorithm": "ML-DSA-65", "input": {"type": "text", "size": 18}, "parameters": {"context": "eet-demo"}}, "result": {"valid": true}, "runtime": {...}}
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
ML-DSA 签名是随机化的:同一私钥对同一消息多次签名结果不同,属正常现象。验签失败(密钥、消息、context 不匹配或签名被篡改)时输出错误文档并以退出码 1 结束。
|
|
1587
|
+
|
|
1419
1588
|
## 证书解析与验证
|
|
1420
1589
|
|
|
1421
1590
|
### 解析证书并验证签名
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# @Time: 2024-04-04 22:49:35
|
|
4
4
|
|
|
5
5
|
# Single source version, keep in sync with setup.py
|
|
6
|
-
__version__ = "2.
|
|
6
|
+
__version__ = "2.8.2"
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
9
9
|
"__version__",
|
|
@@ -11,6 +11,7 @@ __all__ = [
|
|
|
11
11
|
"common",
|
|
12
12
|
"command_perf",
|
|
13
13
|
"convert_command",
|
|
14
|
+
"doctor_command",
|
|
14
15
|
"ecc_command",
|
|
15
16
|
"envelope_command",
|
|
16
17
|
"hmac_command",
|
|
@@ -19,10 +20,13 @@ __all__ = [
|
|
|
19
20
|
"main",
|
|
20
21
|
"output_builder",
|
|
21
22
|
"output_renderer",
|
|
23
|
+
"pqc_command",
|
|
22
24
|
"random_str",
|
|
23
25
|
"rsa_command",
|
|
24
26
|
"rich_ui",
|
|
27
|
+
"shell_command",
|
|
25
28
|
"shell_completion",
|
|
29
|
+
"smart_group",
|
|
26
30
|
"structured_command",
|
|
27
31
|
"tool_version",
|
|
28
32
|
"ts_command",
|
{easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/aes_command.py
RENAMED
|
@@ -650,9 +650,14 @@ def aes_command(
|
|
|
650
650
|
except ValueError as e:
|
|
651
651
|
err_msg = str(e).lower()
|
|
652
652
|
if "padding" in err_msg or "pkcs7" in err_msg:
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
653
|
+
if mode == aes_gcm_mode:
|
|
654
|
+
error(
|
|
655
|
+
"decrypt failed: PKCS#7 unpad error. If cipher was GCM-encrypted without padding, retry without --gcm-pad"
|
|
656
|
+
)
|
|
657
|
+
else:
|
|
658
|
+
error(
|
|
659
|
+
"decrypt failed: PKCS#7 unpad error (wrong key/IV or corrupted ciphertext)"
|
|
660
|
+
)
|
|
656
661
|
else:
|
|
657
662
|
error("decrypt failed: {}".format(e))
|
|
658
663
|
hint_msg = validators.hint_decrypt_maybe_b64(input_data, "AES decrypt")
|
|
@@ -1005,7 +1010,7 @@ def aes_command(
|
|
|
1005
1010
|
err_msg = str(e).lower()
|
|
1006
1011
|
if "padding" in err_msg or "pkcs7" in err_msg:
|
|
1007
1012
|
error(
|
|
1008
|
-
"decrypt failed: PKCS#7 unpad error
|
|
1013
|
+
"decrypt failed: PKCS#7 unpad error (wrong key/IV or corrupted ciphertext)"
|
|
1009
1014
|
)
|
|
1010
1015
|
else:
|
|
1011
1016
|
error("decrypt {} failed: {}".format(input_data, e))
|
{easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/cert_parse.py
RENAMED
|
@@ -67,6 +67,10 @@ OID_AD_CA_ISSUERS = "1.3.6.1.5.5.7.48.2"
|
|
|
67
67
|
OID_AD_OCSP = "1.3.6.1.5.5.7.48.1"
|
|
68
68
|
|
|
69
69
|
AuthorityInformationAccessCAIssuers = "caIssuers"
|
|
70
|
+
|
|
71
|
+
# A CA issuer certificate is a few KB; anything past this is not a certificate
|
|
72
|
+
# but someone using the AIA fetch as a download pipe.
|
|
73
|
+
MAX_CA_ISSUER_BYTES = 1024 * 1024
|
|
70
74
|
AuthorityInformationAccessOCSP = "OCSP"
|
|
71
75
|
AuthorityInformationAccess = (
|
|
72
76
|
"x509.AuthorityInformationAccess",
|
|
@@ -286,15 +290,30 @@ def read_file_from_url(url: str) -> tuple[bytes | None, str | None]:
|
|
|
286
290
|
- (bytes, None): success
|
|
287
291
|
- (None, "empty_url"): url empty or None
|
|
288
292
|
- (None, "http_NNN"): non-200 status
|
|
293
|
+
- (None, "too_large"): response exceeded MAX_CA_ISSUER_BYTES
|
|
289
294
|
- (None, "network"): timeout or connection error
|
|
290
295
|
"""
|
|
291
296
|
if url is None or not str(url).strip():
|
|
292
297
|
return None, "empty_url"
|
|
293
298
|
try:
|
|
294
|
-
response = requests.get(url, timeout=10)
|
|
295
|
-
if response.status_code
|
|
296
|
-
|
|
297
|
-
|
|
299
|
+
response = requests.get(url, timeout=10, stream=True)
|
|
300
|
+
if response.status_code != 200:
|
|
301
|
+
response.close()
|
|
302
|
+
return None, "http_{}".format(response.status_code)
|
|
303
|
+
# The URL comes from the certificate being parsed, which may be
|
|
304
|
+
# attacker-controlled; never buffer an unbounded response body.
|
|
305
|
+
chunks: list[bytes] = []
|
|
306
|
+
total = 0
|
|
307
|
+
for chunk in response.iter_content(chunk_size=64 * 1024):
|
|
308
|
+
if not chunk:
|
|
309
|
+
continue
|
|
310
|
+
total += len(chunk)
|
|
311
|
+
if total > MAX_CA_ISSUER_BYTES:
|
|
312
|
+
response.close()
|
|
313
|
+
return None, "too_large"
|
|
314
|
+
chunks.append(chunk)
|
|
315
|
+
response.close()
|
|
316
|
+
return b"".join(chunks), None
|
|
298
317
|
except requests.Timeout:
|
|
299
318
|
return None, "timeout"
|
|
300
319
|
except requests.RequestException:
|
|
@@ -368,6 +387,12 @@ def verify_cert_file_signature_from_ca_issuer(cert: X509, ca_issuer_url: str):
|
|
|
368
387
|
fetch_err.replace("http_", ""), ca_issuer_url
|
|
369
388
|
)
|
|
370
389
|
)
|
|
390
|
+
if fetch_err == "too_large":
|
|
391
|
+
raise ValueError(
|
|
392
|
+
"CA issuer response exceeds {} bytes, aborting download: {}".format(
|
|
393
|
+
MAX_CA_ISSUER_BYTES, ca_issuer_url
|
|
394
|
+
)
|
|
395
|
+
)
|
|
371
396
|
raise ValueError(
|
|
372
397
|
"fetch CA issuer cert failed (network error): {}".format(ca_issuer_url)
|
|
373
398
|
)
|
|
@@ -384,6 +409,10 @@ def verify_cert_file_signature_from_ca_issuer(cert: X509, ca_issuer_url: str):
|
|
|
384
409
|
continue
|
|
385
410
|
try:
|
|
386
411
|
verify_cert_signature(cert, ca_cert)
|
|
412
|
+
except ValueError:
|
|
413
|
+
# carries an actionable reason (unsupported CA key type, load
|
|
414
|
+
# failure) — don't flatten it into a generic verify failure
|
|
415
|
+
raise
|
|
387
416
|
except BaseException:
|
|
388
417
|
raise InvalidSignature(
|
|
389
418
|
"verify cert signature failed, ca issuer: {}".format(
|
|
@@ -505,6 +534,17 @@ def _parse_sm2_cert(cert_file: str, verbose: bool) -> tuple[dict[str, Any] | Non
|
|
|
505
534
|
show_default=True,
|
|
506
535
|
help="Show verbose certificate details",
|
|
507
536
|
)
|
|
537
|
+
@click.option(
|
|
538
|
+
"--fetch-aia",
|
|
539
|
+
required=False,
|
|
540
|
+
type=click.BOOL,
|
|
541
|
+
is_flag=True,
|
|
542
|
+
default=False,
|
|
543
|
+
help=(
|
|
544
|
+
"Fetch the certificate's AIA caIssuers URL to verify its signature "
|
|
545
|
+
"(makes a network request to a URL found inside the cert; opt-in)"
|
|
546
|
+
),
|
|
547
|
+
)
|
|
508
548
|
@output_format_options
|
|
509
549
|
@command_perf.timing_decorator
|
|
510
550
|
def parse_x509_cert_file(
|
|
@@ -512,6 +552,7 @@ def parse_x509_cert_file(
|
|
|
512
552
|
encoding: click.STRING,
|
|
513
553
|
gm_cert: click.BOOL,
|
|
514
554
|
verbose: click.BOOL,
|
|
555
|
+
fetch_aia: click.BOOL,
|
|
515
556
|
output_format: str | None,
|
|
516
557
|
):
|
|
517
558
|
request_id = create_request_id()
|
|
@@ -588,6 +629,14 @@ def parse_x509_cert_file(
|
|
|
588
629
|
"status": "skipped",
|
|
589
630
|
"message": "AIA caIssuers URL is empty, skip signature verification",
|
|
590
631
|
}
|
|
632
|
+
elif not fetch_aia:
|
|
633
|
+
verification = {
|
|
634
|
+
"status": "skipped",
|
|
635
|
+
"message": (
|
|
636
|
+
"AIA signature verification skipped (offline mode); "
|
|
637
|
+
"pass --fetch-aia to fetch the caIssuers URL"
|
|
638
|
+
),
|
|
639
|
+
}
|
|
591
640
|
else:
|
|
592
641
|
try:
|
|
593
642
|
verify_cert_file_signature_from_ca_issuer(cert, ca_issuer_url)
|
|
@@ -617,7 +666,12 @@ def parse_x509_cert_file(
|
|
|
617
666
|
algorithm="x509-certificate",
|
|
618
667
|
input_type="file",
|
|
619
668
|
input_size=len(cert_raw_bytes),
|
|
620
|
-
parameters={
|
|
669
|
+
parameters={
|
|
670
|
+
"encoding": encoding,
|
|
671
|
+
"gm_cert": gm_cert,
|
|
672
|
+
"verbose": verbose,
|
|
673
|
+
"fetch_aia": fetch_aia,
|
|
674
|
+
},
|
|
621
675
|
)
|
|
622
676
|
result = build_result(
|
|
623
677
|
certificate_type="x509",
|
|
@@ -211,11 +211,22 @@ def resolve_symmetric_key_iv(
|
|
|
211
211
|
return None
|
|
212
212
|
return ib, "", False
|
|
213
213
|
if from_cli:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
piece_label = "key" if piece == "key" else "IV/nonce"
|
|
215
|
+
raw_len = len((text_val or "").encode("utf-8"))
|
|
216
|
+
if raw_len < length:
|
|
217
|
+
# Random padding made short inputs "work" for encryption but
|
|
218
|
+
# the padded value is never shown, so the ciphertext could not
|
|
219
|
+
# be decrypted again by anyone. Refuse instead (N4).
|
|
220
|
+
error(
|
|
221
|
+
"{}: {} is {} bytes but this operation requires exactly {} "
|
|
222
|
+
"bytes; short values are rejected instead of being padded "
|
|
223
|
+
"with random bytes (a padded value could never be "
|
|
224
|
+
"reproduced for decryption)".format(
|
|
225
|
+
algo, piece_label, raw_len, length
|
|
226
|
+
)
|
|
218
227
|
)
|
|
228
|
+
return None
|
|
229
|
+
fitted, padded, truncated = _fit_str(text_val or "", length)
|
|
219
230
|
if truncated:
|
|
220
231
|
warning(
|
|
221
232
|
"Key/IV was truncated (long input). Ensure compatibility with peer; consider using exact-length key/IV."
|
{easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/convert_command.py
RENAMED
|
@@ -7,7 +7,6 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import base64
|
|
9
9
|
import binascii
|
|
10
|
-
import string
|
|
11
10
|
import time
|
|
12
11
|
|
|
13
12
|
import click
|
|
@@ -25,18 +24,24 @@ from easy_encryption_tool.output_renderer import (
|
|
|
25
24
|
resolve_output_format,
|
|
26
25
|
)
|
|
27
26
|
from easy_encryption_tool.rich_ui import error
|
|
27
|
+
from easy_encryption_tool.smart_group import SmartGroup
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _is_printable_utf8(data: bytes) -> bool:
|
|
31
31
|
"""Return True when bytes decode to printable UTF-8 text."""
|
|
32
32
|
try:
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
# str.isprintable() covers all printable Unicode (CJK included),
|
|
34
|
+
# unlike string.printable which is ASCII-only. It does however reject
|
|
35
|
+
# control whitespace (\n, \t, \r, ...) that string.printable allowed,
|
|
36
|
+
# so accept whitespace explicitly: multiline text must stay UTF-8,
|
|
37
|
+
# not fall through to hex/base64.
|
|
38
|
+
text = data.decode("utf-8")
|
|
39
|
+
return all(c.isprintable() or c.isspace() for c in text)
|
|
35
40
|
except UnicodeDecodeError:
|
|
36
41
|
return False
|
|
37
42
|
|
|
38
43
|
|
|
39
|
-
@click.group(name="convert", short_help="String format conversion: UTF-8, Base64, Hex")
|
|
44
|
+
@click.group(name="convert", short_help="String format conversion: UTF-8, Base64, Hex", cls=SmartGroup)
|
|
40
45
|
def convert_group():
|
|
41
46
|
"""Convert between UTF-8 text, Base64, and Hex representations."""
|
|
42
47
|
pass
|