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.
Files changed (96) hide show
  1. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/PKG-INFO +200 -31
  2. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/__init__.py +5 -1
  3. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/aes_command.py +9 -4
  4. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/cert_parse.py +59 -5
  5. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/common.py +15 -4
  6. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/convert_command.py +9 -4
  7. easy_encryption_tool-2.8.2/easy_encryption_tool/doctor_command.py +123 -0
  8. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/ecc_command.py +10 -5
  9. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/envelope_command.py +49 -13
  10. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/hints.py +10 -0
  11. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/hmac_command.py +2 -4
  12. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/int_bytes_command.py +2 -1
  13. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/tencentcloud.py +3 -0
  14. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/main.py +15 -48
  15. easy_encryption_tool-2.8.2/easy_encryption_tool/pqc_command.py +902 -0
  16. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/rich_ui.py +3 -1
  17. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/rsa_command.py +5 -1
  18. easy_encryption_tool-2.8.2/easy_encryption_tool/shell_command.py +413 -0
  19. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/sm2_command.py +2 -1
  20. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/sm4_command.py +22 -9
  21. easy_encryption_tool-2.8.2/easy_encryption_tool/smart_group.py +79 -0
  22. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/aad.py +8 -1
  23. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/errors.py +10 -0
  24. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/types.py +3 -1
  25. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured_command.py +2 -0
  26. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/tool_version.py +24 -1
  27. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/ts_command.py +2 -1
  28. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/PKG-INFO +200 -31
  29. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/SOURCES.txt +8 -0
  30. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/requires.txt +4 -0
  31. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/setup.py +2 -0
  32. easy_encryption_tool-2.8.2/tests/test_doctor_command.py +41 -0
  33. easy_encryption_tool-2.8.2/tests/test_fix_281.py +270 -0
  34. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_main.py +14 -1
  35. easy_encryption_tool-2.8.2/tests/test_pqc_command.py +454 -0
  36. easy_encryption_tool-2.8.2/tests/test_shell_command.py +307 -0
  37. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/cipherhub_defaults.py +0 -0
  38. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/command_perf.py +0 -0
  39. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/crypto/__init__.py +0 -0
  40. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/hash_command.py +0 -0
  41. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/__init__.py +0 -0
  42. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/availability.py +0 -0
  43. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/__init__.py +0 -0
  44. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/aliyun.py +0 -0
  45. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/aws.py +0 -0
  46. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/azure.py +0 -0
  47. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/googlecloud.py +0 -0
  48. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/hashicorpvault.py +0 -0
  49. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/backends/huaweicloud.py +0 -0
  50. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/config.py +0 -0
  51. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/kms/errors.py +0 -0
  52. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/output_builder.py +0 -0
  53. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/output_renderer.py +0 -0
  54. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/random_str.py +0 -0
  55. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/shell_completion.py +0 -0
  56. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/__init__.py +0 -0
  57. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/format_detect.py +0 -0
  58. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/ini_store.py +0 -0
  59. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/json_store.py +0 -0
  60. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/keys.py +0 -0
  61. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/mac.py +0 -0
  62. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/policy.py +0 -0
  63. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/token.py +0 -0
  64. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/traversal.py +0 -0
  65. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/structured/yaml_store.py +0 -0
  66. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/validators.py +0 -0
  67. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool/zuc_command.py +0 -0
  68. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/dependency_links.txt +0 -0
  69. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/entry_points.txt +0 -0
  70. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/not-zip-safe +0 -0
  71. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/easy_encryption_tool.egg-info/top_level.txt +0 -0
  72. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/setup.cfg +0 -0
  73. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/__init__.py +0 -0
  74. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/conftest.py +0 -0
  75. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/sm4_cbc_demo.py +0 -0
  76. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_aes_command.py +0 -0
  77. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_cert_parse.py +0 -0
  78. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_cipherhub_compatibility.py +0 -0
  79. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_command_perf.py +0 -0
  80. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_common.py +0 -0
  81. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_crypto.py +0 -0
  82. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_crypto_core.py +0 -0
  83. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_ecc_command.py +0 -0
  84. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_hash_command.py +0 -0
  85. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_hints.py +0 -0
  86. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_hmac_command.py +0 -0
  87. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_output_contract.py +0 -0
  88. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_random_str.py +0 -0
  89. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_rich_ui.py +0 -0
  90. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_rsa_command.py +0 -0
  91. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_sm2_command.py +0 -0
  92. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_sm4_command.py +0 -0
  93. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_structured_regressions.py +0 -0
  94. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_tool_version.py +0 -0
  95. {easy_encryption_tool-2.6.0 → easy_encryption_tool-2.8.2}/tests/test_validators.py +0 -0
  96. {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
1
+ Metadata-Version: 2.4
2
2
  Name: easy_encryption_tool
3
- Version: 2.6.0
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
- Usage: main.py aes [OPTIONS]
201
-
202
- Options:
203
- -m, --mode [cbc|gcm] aes mode,默认为 cbc 模式,可选 cbc 或 gcm 模式
204
- [default: cbc]
205
- -k, --key TEXT key 默认 32 字节,即 256 位,只允许输入可见字符,
206
- 长度不够则自动补齐,长度超出则自动截取 [default:
207
- kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk]
208
- -v, --iv-nonce TEXT cbc 模式下,iv 默认 16 字节即 128 位,gcm 模式下 nonce 默认
209
- 12 字节即 96 位,长度不够则自动补齐,长度超出则自动截取 [default:
210
- vvvvvvvvvvvvvvvv]
211
- -r, --random-key-iv 是否自动生成随机的密钥和 iv/nonce,如果随机生成,则密钥长度默认 32
212
- 字节,iv 默认为 16 字节, nonce 默认为 12 字节
213
- -a, --action [encrypt|decrypt] 加密(encrypt)或 解密(decrypt),加密后输出 base64 编码的字符串
214
- [default: encrypt]
215
- -i, --input-data TEXT 输入数据,即被加密或解密的数据,加密时允许输入:字符串、 base64
216
- 编码数据、文件路径,解密时允许输入:base64 编码数据、文件路径
217
- [required]
218
- -e, --is-base64-encoded 如果 -i/--input-data 的值被 base64 编码过,则需要带上 -e
219
- 参数,-e 与 -f 互斥 [default: False]
220
- -f, --is-a-file 如果 -i/--input-data 的值是一个文件,则需要带上 -f
221
- 参数表示当前需要被处理的是一个文件,-e 与 -f 互斥
222
- -l, --input-limit INTEGER 输入内容最大长度,单位为 MB,默认为 1MB,在 -i 为非文件时生效
223
- [default: 1]
224
- -o, --output-file TEXT 指定输出文件,当输入时指定了文件,则输出时必须指定
225
- --aad TEXT gcm 模式下的关联认证数据 AAD,默认: "密码学人 CipherHUB 默认 AAD 数据"
226
- --help Show this message and exit.
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
- - 密钥:默认 32 字节,即 256 位,不足会自动补齐,超过会自动截取
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.0"
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",
@@ -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
- error(
654
- "decrypt failed: PKCS#7 unpad error. If cipher was GCM-encrypted without padding, retry without --gcm-pad"
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. If cipher was GCM-encrypted without padding, retry without --gcm-pad"
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))
@@ -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 == 200:
296
- return response.content, None
297
- return None, "http_{}".format(response.status_code)
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={"encoding": encoding, "gm_cert": gm_cert, "verbose": verbose},
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
- fitted, padded, truncated = _fit_str(text_val or "", length)
215
- if padded:
216
- warning(
217
- "Key/IV was padded (short input). Ensure compatibility with peer; consider using full-length key/IV."
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."
@@ -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
- s = data.decode("utf-8")
34
- return all(c in string.printable for c in s)
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