crypto-gu 0.1.0__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.
- crypto_gu-0.1.0/LICENSE +21 -0
- crypto_gu-0.1.0/PKG-INFO +130 -0
- crypto_gu-0.1.0/README.md +107 -0
- crypto_gu-0.1.0/crypto_gu/__init__.py +12 -0
- crypto_gu-0.1.0/crypto_gu/asymmetric/__init__.py +0 -0
- crypto_gu-0.1.0/crypto_gu/asymmetric/pkcs1.py +212 -0
- crypto_gu-0.1.0/crypto_gu/asymmetric/rsa.py +108 -0
- crypto_gu-0.1.0/crypto_gu/attacks/__init__.py +0 -0
- crypto_gu-0.1.0/crypto_gu/attacks/aes.py +214 -0
- crypto_gu-0.1.0/crypto_gu/attacks/rsa.py +134 -0
- crypto_gu-0.1.0/crypto_gu/constant_time.py +52 -0
- crypto_gu-0.1.0/crypto_gu/encoding.py +332 -0
- crypto_gu-0.1.0/crypto_gu/errors.py +65 -0
- crypto_gu-0.1.0/crypto_gu/hashes/__init__.py +45 -0
- crypto_gu-0.1.0/crypto_gu/hashes/blake2b.py +113 -0
- crypto_gu-0.1.0/crypto_gu/hashes/blake2s.py +113 -0
- crypto_gu-0.1.0/crypto_gu/hashes/hmac.py +104 -0
- crypto_gu-0.1.0/crypto_gu/hashes/md5.py +101 -0
- crypto_gu-0.1.0/crypto_gu/hashes/sha1.py +76 -0
- crypto_gu-0.1.0/crypto_gu/hashes/sha256.py +84 -0
- crypto_gu-0.1.0/crypto_gu/hashes/sha512.py +103 -0
- crypto_gu-0.1.0/crypto_gu/kdf.py +229 -0
- crypto_gu-0.1.0/crypto_gu/number_theory.py +499 -0
- crypto_gu-0.1.0/crypto_gu/padding.py +173 -0
- crypto_gu-0.1.0/crypto_gu/rng/__init__.py +0 -0
- crypto_gu-0.1.0/crypto_gu/rng/mt19937.py +140 -0
- crypto_gu-0.1.0/crypto_gu/symmetric/__init__.py +0 -0
- crypto_gu-0.1.0/crypto_gu/symmetric/aes.py +369 -0
- crypto_gu-0.1.0/crypto_gu/symmetric/aes_gcm.py +126 -0
- crypto_gu-0.1.0/crypto_gu/symmetric/chacha20.py +105 -0
- crypto_gu-0.1.0/crypto_gu/symmetric/chacha20poly1305.py +74 -0
- crypto_gu-0.1.0/crypto_gu/symmetric/poly1305.py +38 -0
- crypto_gu-0.1.0/crypto_gu.egg-info/PKG-INFO +130 -0
- crypto_gu-0.1.0/crypto_gu.egg-info/SOURCES.txt +51 -0
- crypto_gu-0.1.0/crypto_gu.egg-info/dependency_links.txt +1 -0
- crypto_gu-0.1.0/crypto_gu.egg-info/requires.txt +4 -0
- crypto_gu-0.1.0/crypto_gu.egg-info/top_level.txt +1 -0
- crypto_gu-0.1.0/pyproject.toml +40 -0
- crypto_gu-0.1.0/setup.cfg +4 -0
- crypto_gu-0.1.0/tests/test_aead.py +162 -0
- crypto_gu-0.1.0/tests/test_aes.py +143 -0
- crypto_gu-0.1.0/tests/test_aes_attacks.py +126 -0
- crypto_gu-0.1.0/tests/test_blake2.py +134 -0
- crypto_gu-0.1.0/tests/test_chacha20.py +119 -0
- crypto_gu-0.1.0/tests/test_constant_time.py +52 -0
- crypto_gu-0.1.0/tests/test_encoding.py +210 -0
- crypto_gu-0.1.0/tests/test_hashes.py +223 -0
- crypto_gu-0.1.0/tests/test_kdf.py +171 -0
- crypto_gu-0.1.0/tests/test_mt19937.py +105 -0
- crypto_gu-0.1.0/tests/test_number_theory.py +246 -0
- crypto_gu-0.1.0/tests/test_pkcs1.py +120 -0
- crypto_gu-0.1.0/tests/test_rsa.py +152 -0
- crypto_gu-0.1.0/tests/test_sha_variants.py +108 -0
crypto_gu-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TSVMV
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
crypto_gu-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crypto-gu
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure standard-library cryptography toolkit: RFC/NIST-verified primitives plus deterministic attack implementations for research and teaching.
|
|
5
|
+
Author: TSVMV
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/TSVMV/crypto-gu
|
|
8
|
+
Keywords: cryptography,teaching,research,aes,chacha20,poly1305,gcm,blake2,scrypt,rsa,oaep,pss
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Education
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Security :: Cryptography
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: oracle
|
|
20
|
+
Requires-Dist: cryptography>=41; extra == "oracle"
|
|
21
|
+
Requires-Dist: pycryptodome>=3.19; extra == "oracle"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# crypto-gu
|
|
25
|
+
|
|
26
|
+
面向密码学研究者与教学的纯标准库工具箱。生产形态的原语(哈希、KDF、分组与流密码、AEAD、RSA 填充方案)与确定性攻击实现放在同一个内核里,每个原语均以 RFC / NIST 权威测试向量验证。
|
|
27
|
+
|
|
28
|
+
## 设计原则
|
|
29
|
+
|
|
30
|
+
- 仅依赖 Python 标准库,零第三方依赖,Python >= 3.11。
|
|
31
|
+
- 每个原语都有权威向量背书;向量取自 RFC / NIST 原文,或由 `hashlib`、OpenSSL、`cryptography`、`pycryptodome` 实测生成,逐条嵌入测试。
|
|
32
|
+
- 攻击层只做确定性恢复(利用结构弱点),不含密钥空间暴力枚举。
|
|
33
|
+
- `constant_time` 模块消除明显的数据依赖分支与提前退出;CPython 无法提供严格的常量时间保证,此处保持诚实定位。
|
|
34
|
+
|
|
35
|
+
## 模块总览
|
|
36
|
+
|
|
37
|
+
| 模块 | 内容 |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `crypto_gu.hashes` | SHA-1 / SHA-256 / SHA-512、MD5、BLAKE2b / BLAKE2s、HMAC、长度扩展攻击 |
|
|
40
|
+
| `crypto_gu.kdf` | PBKDF2、HKDF(extract / expand)、scrypt |
|
|
41
|
+
| `crypto_gu.symmetric.aes` | AES-128/192/256 分组原语与 ECB / CBC / CFB / OFB / CTR 模式 |
|
|
42
|
+
| `crypto_gu.symmetric.chacha20` | ChaCha20 流密码 |
|
|
43
|
+
| `crypto_gu.symmetric.poly1305` | Poly1305 一次性 MAC |
|
|
44
|
+
| `crypto_gu.symmetric.chacha20poly1305` | ChaCha20-Poly1305 AEAD |
|
|
45
|
+
| `crypto_gu.symmetric.aes_gcm` | AES-GCM AEAD,tag 可截断至 4..16 字节 |
|
|
46
|
+
| `crypto_gu.asymmetric.rsa` | RSA 密钥生成与教科书式原语(配合攻击层做因子恢复) |
|
|
47
|
+
| `crypto_gu.asymmetric.pkcs1` | MGF1、RSAES-OAEP、RSASSA-PSS(RFC 8017) |
|
|
48
|
+
| `crypto_gu.number_theory` | 素性检验、模逆、CRT、Tonelli-Shanks、Pollard rho / p-1、Fermat、BSGS |
|
|
49
|
+
| `crypto_gu.rng.mt19937` | MT19937 生成器与从输出反推内部状态 |
|
|
50
|
+
| `crypto_gu.encoding` / `crypto_gu.padding` | hex / base32/58/64/85、XOR、Morse、BCD;PKCS#7 等填充方案 |
|
|
51
|
+
| `crypto_gu.constant_time` | 无提前退出的比较、无分支 select |
|
|
52
|
+
| `crypto_gu.attacks.aes` | ECB byte-at-a-time、CBC padding oracle |
|
|
53
|
+
| `crypto_gu.attacks.rsa` | Wiener、Fermat、Pollard p-1、共模攻击、Hastad broadcast |
|
|
54
|
+
|
|
55
|
+
## 快速上手
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# 哈希与 HMAC
|
|
59
|
+
from crypto_gu.hashes import sha256, blake2b
|
|
60
|
+
from crypto_gu.hashes.hmac import hmac_sha256
|
|
61
|
+
|
|
62
|
+
sha256(b"message").hex()
|
|
63
|
+
blake2b(b"message").hex()
|
|
64
|
+
hmac_sha256(b"key", b"message")
|
|
65
|
+
|
|
66
|
+
# KDF
|
|
67
|
+
from crypto_gu.kdf import pbkdf2, hkdf, scrypt
|
|
68
|
+
|
|
69
|
+
pbkdf2(b"password", b"salt", 100000, 32)
|
|
70
|
+
hkdf(b"input key material", 32, salt=b"salt", info=b"ctx")
|
|
71
|
+
scrypt(b"password", b"NaCl", 16384, 8, 1, 64)
|
|
72
|
+
|
|
73
|
+
# AEAD:密文与认证 tag 拼接返回,认证失败抛 InvalidTagError
|
|
74
|
+
from crypto_gu.symmetric import aes_gcm, chacha20poly1305
|
|
75
|
+
|
|
76
|
+
key = bytes(range(32))
|
|
77
|
+
nonce = b"\x00" * 12
|
|
78
|
+
sealed = aes_gcm.encrypt(key, nonce, b"payload", b"header")
|
|
79
|
+
aes_gcm.decrypt(key, nonce, sealed, b"header")
|
|
80
|
+
|
|
81
|
+
one_shot = chacha20poly1305.encrypt(key, nonce, b"payload", b"header")
|
|
82
|
+
chacha20poly1305.decrypt(key, nonce, one_shot, b"header")
|
|
83
|
+
|
|
84
|
+
# RSA-OAEP / RSA-PSS
|
|
85
|
+
from crypto_gu.asymmetric.rsa import RSAKey
|
|
86
|
+
from crypto_gu.asymmetric import pkcs1
|
|
87
|
+
|
|
88
|
+
key = RSAKey.generate(2048)
|
|
89
|
+
ciphertext = pkcs1.oaep_encrypt(key, b"secret", "sha256")
|
|
90
|
+
pkcs1.oaep_decrypt(key, ciphertext, "sha256")
|
|
91
|
+
|
|
92
|
+
signature = pkcs1.pss_sign(key, b"document", "sha256", 32)
|
|
93
|
+
pkcs1.pss_verify_signature(key, b"document", signature, "sha256", 32)
|
|
94
|
+
|
|
95
|
+
# 攻击研究层(确定性恢复,仅供授权环境实验)
|
|
96
|
+
from crypto_gu.attacks.rsa import wiener, common_modulus
|
|
97
|
+
|
|
98
|
+
wiener(n, e) # d 过小时由 (n, e) 恢复私钥
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 验证背书
|
|
102
|
+
|
|
103
|
+
| 原语 | 权威向量 | 交叉 oracle |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| AES 分组与 ECB/CBC/CFB/OFB/CTR | FIPS-197、NIST SP 800-38A | OpenSSL CLI |
|
|
106
|
+
| ChaCha20 / Poly1305 / ChaCha20-Poly1305 | RFC 8439 §2.4.2、§2.5.2、§2.8.2 | `cryptography`、`pycryptodome` |
|
|
107
|
+
| AES-GCM(含 96 位外 IV、AAD、截断 tag) | NIST GCM 规范测试用例 1-5 | `cryptography`、`pycryptodome` |
|
|
108
|
+
| SHA-1 / SHA-256 / SHA-512 | FIPS 180-4、RFC 6234 | `hashlib` 全量比对 |
|
|
109
|
+
| MD5 | RFC 1321 | `hashlib` 全量比对 |
|
|
110
|
+
| BLAKE2b / BLAKE2s | RFC 7693 附录 A/B/E | `hashlib`(27 组参数矩阵) |
|
|
111
|
+
| HMAC | RFC 2202、RFC 4231 | `hashlib` / `hmac` 模块 |
|
|
112
|
+
| PBKDF2 | RFC 7914 §11 | `hashlib.pbkdf2_hmac` |
|
|
113
|
+
| HKDF | RFC 5869 A.1-A.3 | RFC 向量逐条嵌入 |
|
|
114
|
+
| scrypt | RFC 7914 §12 全 4 组 | `hashlib.scrypt`(OpenSSL `kdf` 已另行交叉验证) |
|
|
115
|
+
| RSAES-OAEP / RSASSA-PSS | RFC 8017(嵌入固定密钥与密文/签名向量) | `cryptography`、`pycryptodome` 双向 |
|
|
116
|
+
| MT19937 | 参考实现输出序列 | 状态恢复回环 |
|
|
117
|
+
|
|
118
|
+
## 测试
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python3 -m unittest discover -s tests -q
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
当前 214 个测试约 2 分钟跑完;耗时集中在 scrypt(RFC 7914 §12 大参数组)与 CBC padding oracle 攻击测试。测试自带全部向量,不需要任何第三方库;`[oracle]` extra 仅用于自行做交叉验证。
|
|
125
|
+
|
|
126
|
+
## 局限声明
|
|
127
|
+
|
|
128
|
+
- 纯 Python 实现面向研究与教学,吞吐量与侧信道防护弱于原生密码库。
|
|
129
|
+
- 生产业务若需绝对性能或形式化侧信道保证,建议叠加成熟原生实现。
|
|
130
|
+
- 攻击层仅限在明确授权的环境中复现教科书级结构弱点,请遵守所在司法辖区的法律法规。
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# crypto-gu
|
|
2
|
+
|
|
3
|
+
面向密码学研究者与教学的纯标准库工具箱。生产形态的原语(哈希、KDF、分组与流密码、AEAD、RSA 填充方案)与确定性攻击实现放在同一个内核里,每个原语均以 RFC / NIST 权威测试向量验证。
|
|
4
|
+
|
|
5
|
+
## 设计原则
|
|
6
|
+
|
|
7
|
+
- 仅依赖 Python 标准库,零第三方依赖,Python >= 3.11。
|
|
8
|
+
- 每个原语都有权威向量背书;向量取自 RFC / NIST 原文,或由 `hashlib`、OpenSSL、`cryptography`、`pycryptodome` 实测生成,逐条嵌入测试。
|
|
9
|
+
- 攻击层只做确定性恢复(利用结构弱点),不含密钥空间暴力枚举。
|
|
10
|
+
- `constant_time` 模块消除明显的数据依赖分支与提前退出;CPython 无法提供严格的常量时间保证,此处保持诚实定位。
|
|
11
|
+
|
|
12
|
+
## 模块总览
|
|
13
|
+
|
|
14
|
+
| 模块 | 内容 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `crypto_gu.hashes` | SHA-1 / SHA-256 / SHA-512、MD5、BLAKE2b / BLAKE2s、HMAC、长度扩展攻击 |
|
|
17
|
+
| `crypto_gu.kdf` | PBKDF2、HKDF(extract / expand)、scrypt |
|
|
18
|
+
| `crypto_gu.symmetric.aes` | AES-128/192/256 分组原语与 ECB / CBC / CFB / OFB / CTR 模式 |
|
|
19
|
+
| `crypto_gu.symmetric.chacha20` | ChaCha20 流密码 |
|
|
20
|
+
| `crypto_gu.symmetric.poly1305` | Poly1305 一次性 MAC |
|
|
21
|
+
| `crypto_gu.symmetric.chacha20poly1305` | ChaCha20-Poly1305 AEAD |
|
|
22
|
+
| `crypto_gu.symmetric.aes_gcm` | AES-GCM AEAD,tag 可截断至 4..16 字节 |
|
|
23
|
+
| `crypto_gu.asymmetric.rsa` | RSA 密钥生成与教科书式原语(配合攻击层做因子恢复) |
|
|
24
|
+
| `crypto_gu.asymmetric.pkcs1` | MGF1、RSAES-OAEP、RSASSA-PSS(RFC 8017) |
|
|
25
|
+
| `crypto_gu.number_theory` | 素性检验、模逆、CRT、Tonelli-Shanks、Pollard rho / p-1、Fermat、BSGS |
|
|
26
|
+
| `crypto_gu.rng.mt19937` | MT19937 生成器与从输出反推内部状态 |
|
|
27
|
+
| `crypto_gu.encoding` / `crypto_gu.padding` | hex / base32/58/64/85、XOR、Morse、BCD;PKCS#7 等填充方案 |
|
|
28
|
+
| `crypto_gu.constant_time` | 无提前退出的比较、无分支 select |
|
|
29
|
+
| `crypto_gu.attacks.aes` | ECB byte-at-a-time、CBC padding oracle |
|
|
30
|
+
| `crypto_gu.attacks.rsa` | Wiener、Fermat、Pollard p-1、共模攻击、Hastad broadcast |
|
|
31
|
+
|
|
32
|
+
## 快速上手
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# 哈希与 HMAC
|
|
36
|
+
from crypto_gu.hashes import sha256, blake2b
|
|
37
|
+
from crypto_gu.hashes.hmac import hmac_sha256
|
|
38
|
+
|
|
39
|
+
sha256(b"message").hex()
|
|
40
|
+
blake2b(b"message").hex()
|
|
41
|
+
hmac_sha256(b"key", b"message")
|
|
42
|
+
|
|
43
|
+
# KDF
|
|
44
|
+
from crypto_gu.kdf import pbkdf2, hkdf, scrypt
|
|
45
|
+
|
|
46
|
+
pbkdf2(b"password", b"salt", 100000, 32)
|
|
47
|
+
hkdf(b"input key material", 32, salt=b"salt", info=b"ctx")
|
|
48
|
+
scrypt(b"password", b"NaCl", 16384, 8, 1, 64)
|
|
49
|
+
|
|
50
|
+
# AEAD:密文与认证 tag 拼接返回,认证失败抛 InvalidTagError
|
|
51
|
+
from crypto_gu.symmetric import aes_gcm, chacha20poly1305
|
|
52
|
+
|
|
53
|
+
key = bytes(range(32))
|
|
54
|
+
nonce = b"\x00" * 12
|
|
55
|
+
sealed = aes_gcm.encrypt(key, nonce, b"payload", b"header")
|
|
56
|
+
aes_gcm.decrypt(key, nonce, sealed, b"header")
|
|
57
|
+
|
|
58
|
+
one_shot = chacha20poly1305.encrypt(key, nonce, b"payload", b"header")
|
|
59
|
+
chacha20poly1305.decrypt(key, nonce, one_shot, b"header")
|
|
60
|
+
|
|
61
|
+
# RSA-OAEP / RSA-PSS
|
|
62
|
+
from crypto_gu.asymmetric.rsa import RSAKey
|
|
63
|
+
from crypto_gu.asymmetric import pkcs1
|
|
64
|
+
|
|
65
|
+
key = RSAKey.generate(2048)
|
|
66
|
+
ciphertext = pkcs1.oaep_encrypt(key, b"secret", "sha256")
|
|
67
|
+
pkcs1.oaep_decrypt(key, ciphertext, "sha256")
|
|
68
|
+
|
|
69
|
+
signature = pkcs1.pss_sign(key, b"document", "sha256", 32)
|
|
70
|
+
pkcs1.pss_verify_signature(key, b"document", signature, "sha256", 32)
|
|
71
|
+
|
|
72
|
+
# 攻击研究层(确定性恢复,仅供授权环境实验)
|
|
73
|
+
from crypto_gu.attacks.rsa import wiener, common_modulus
|
|
74
|
+
|
|
75
|
+
wiener(n, e) # d 过小时由 (n, e) 恢复私钥
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 验证背书
|
|
79
|
+
|
|
80
|
+
| 原语 | 权威向量 | 交叉 oracle |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| AES 分组与 ECB/CBC/CFB/OFB/CTR | FIPS-197、NIST SP 800-38A | OpenSSL CLI |
|
|
83
|
+
| ChaCha20 / Poly1305 / ChaCha20-Poly1305 | RFC 8439 §2.4.2、§2.5.2、§2.8.2 | `cryptography`、`pycryptodome` |
|
|
84
|
+
| AES-GCM(含 96 位外 IV、AAD、截断 tag) | NIST GCM 规范测试用例 1-5 | `cryptography`、`pycryptodome` |
|
|
85
|
+
| SHA-1 / SHA-256 / SHA-512 | FIPS 180-4、RFC 6234 | `hashlib` 全量比对 |
|
|
86
|
+
| MD5 | RFC 1321 | `hashlib` 全量比对 |
|
|
87
|
+
| BLAKE2b / BLAKE2s | RFC 7693 附录 A/B/E | `hashlib`(27 组参数矩阵) |
|
|
88
|
+
| HMAC | RFC 2202、RFC 4231 | `hashlib` / `hmac` 模块 |
|
|
89
|
+
| PBKDF2 | RFC 7914 §11 | `hashlib.pbkdf2_hmac` |
|
|
90
|
+
| HKDF | RFC 5869 A.1-A.3 | RFC 向量逐条嵌入 |
|
|
91
|
+
| scrypt | RFC 7914 §12 全 4 组 | `hashlib.scrypt`(OpenSSL `kdf` 已另行交叉验证) |
|
|
92
|
+
| RSAES-OAEP / RSASSA-PSS | RFC 8017(嵌入固定密钥与密文/签名向量) | `cryptography`、`pycryptodome` 双向 |
|
|
93
|
+
| MT19937 | 参考实现输出序列 | 状态恢复回环 |
|
|
94
|
+
|
|
95
|
+
## 测试
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python3 -m unittest discover -s tests -q
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
当前 214 个测试约 2 分钟跑完;耗时集中在 scrypt(RFC 7914 §12 大参数组)与 CBC padding oracle 攻击测试。测试自带全部向量,不需要任何第三方库;`[oracle]` extra 仅用于自行做交叉验证。
|
|
102
|
+
|
|
103
|
+
## 局限声明
|
|
104
|
+
|
|
105
|
+
- 纯 Python 实现面向研究与教学,吞吐量与侧信道防护弱于原生密码库。
|
|
106
|
+
- 生产业务若需绝对性能或形式化侧信道保证,建议叠加成熟原生实现。
|
|
107
|
+
- 攻击层仅限在明确授权的环境中复现教科书级结构弱点,请遵守所在司法辖区的法律法规。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""crypto_gu: a pure standard-library cryptography toolkit.
|
|
2
|
+
|
|
3
|
+
Research- and teaching-oriented primitives (hashes, KDFs, ciphers, AEAD,
|
|
4
|
+
RSA padding schemes) alongside deterministic attack implementations
|
|
5
|
+
(padding oracles, small-exponent and factorisation helpers). No third
|
|
6
|
+
party dependencies; every primitive is validated against RFC/NIST test
|
|
7
|
+
vectors.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
__all__ = ["__version__"]
|
|
File without changes
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""RSA encryption and signature schemes from PKCS #1 v2.2 (RFC 8017).
|
|
2
|
+
|
|
3
|
+
Provides MGF1, RSAES-OAEP (section 7.1) and RSASSA-PSS (section 8.1). The
|
|
4
|
+
underlying trapdoor is the textbook :class:`~crypto_gu.asymmetric.rsa.RSAKey`
|
|
5
|
+
already in this package; these helpers add the randomised, provably related
|
|
6
|
+
padding that turns it into a real-world scheme.
|
|
7
|
+
|
|
8
|
+
Pure Python, standard library only (``os.urandom`` supplies the randomness).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from crypto_gu.errors import DecryptionError, InvalidSignatureError
|
|
16
|
+
from crypto_gu.hashes import HASH_TABLE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _hash_length(hash_name: str) -> int:
|
|
20
|
+
key = hash_name.lower()
|
|
21
|
+
if key not in HASH_TABLE:
|
|
22
|
+
raise ValueError("unknown hash algorithm: %r" % hash_name)
|
|
23
|
+
return len(HASH_TABLE[key](b""))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _digest(hash_name: str, data: bytes) -> bytes:
|
|
27
|
+
return HASH_TABLE[hash_name.lower()](data)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _i2osp(value: int, length: int) -> bytes:
|
|
31
|
+
if value < 0 or value >> (8 * length):
|
|
32
|
+
raise ValueError("integer too large")
|
|
33
|
+
return value.to_bytes(length, "big")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _os2ip(data: bytes) -> int:
|
|
37
|
+
return int.from_bytes(data, "big")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def mgf1(seed: bytes, length: int, hash_name: str = "sha256") -> bytes:
|
|
41
|
+
"""Mask generation function MGF1 (RFC 8017 appendix B.2.1)."""
|
|
42
|
+
if length < 0:
|
|
43
|
+
raise ValueError("mask length must be non-negative")
|
|
44
|
+
hlen = _hash_length(hash_name)
|
|
45
|
+
if length > (1 << 32) * hlen:
|
|
46
|
+
raise ValueError("mask too long")
|
|
47
|
+
output = b""
|
|
48
|
+
counter = 0
|
|
49
|
+
while len(output) < length:
|
|
50
|
+
output += _digest(hash_name, seed + counter.to_bytes(4, "big"))
|
|
51
|
+
counter += 1
|
|
52
|
+
return output[:length]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _xor(left: bytes, right: bytes) -> bytes:
|
|
56
|
+
return bytes(a ^ b for a, b in zip(left, right))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _modulus_size(n: int) -> int:
|
|
60
|
+
return (n.bit_length() + 7) // 8
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --------------------------------------------------------------------------- #
|
|
64
|
+
# RSAES-OAEP
|
|
65
|
+
# --------------------------------------------------------------------------- #
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def oaep_encode(message: bytes, k: int, hash_name: str = "sha256", label: bytes = b"") -> bytes:
|
|
69
|
+
"""EME-OAEP encoding (RFC 8017 section 7.1.1)."""
|
|
70
|
+
hlen = _hash_length(hash_name)
|
|
71
|
+
if len(message) > k - 2 * hlen - 2:
|
|
72
|
+
raise ValueError("message too long for OAEP")
|
|
73
|
+
lhash = _digest(hash_name, label)
|
|
74
|
+
ps = b"\x00" * (k - len(message) - 2 * hlen - 2)
|
|
75
|
+
db = lhash + ps + b"\x01" + message
|
|
76
|
+
seed = os.urandom(hlen)
|
|
77
|
+
db_mask = mgf1(seed, k - hlen - 1, hash_name)
|
|
78
|
+
masked_db = _xor(db, db_mask)
|
|
79
|
+
seed_mask = mgf1(masked_db, hlen, hash_name)
|
|
80
|
+
masked_seed = _xor(seed, seed_mask)
|
|
81
|
+
return b"\x00" + masked_seed + masked_db
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def oaep_decode(encoded: bytes, k: int, hash_name: str = "sha256", label: bytes = b"") -> bytes:
|
|
85
|
+
"""EME-OAEP decoding; raises :class:`DecryptionError` on malformed input."""
|
|
86
|
+
hlen = _hash_length(hash_name)
|
|
87
|
+
if len(encoded) != k or k < 2 * hlen + 2:
|
|
88
|
+
raise DecryptionError("decryption error")
|
|
89
|
+
if encoded[0] != 0:
|
|
90
|
+
raise DecryptionError("decryption error")
|
|
91
|
+
masked_seed = encoded[1 : 1 + hlen]
|
|
92
|
+
masked_db = encoded[1 + hlen :]
|
|
93
|
+
seed = _xor(masked_seed, mgf1(masked_db, hlen, hash_name))
|
|
94
|
+
db = _xor(masked_db, mgf1(seed, k - hlen - 1, hash_name))
|
|
95
|
+
if db[:hlen] != _digest(hash_name, label):
|
|
96
|
+
raise DecryptionError("decryption error")
|
|
97
|
+
index = hlen
|
|
98
|
+
while index < len(db) and db[index] == 0:
|
|
99
|
+
index += 1
|
|
100
|
+
if index >= len(db) or db[index] != 1:
|
|
101
|
+
raise DecryptionError("decryption error")
|
|
102
|
+
return db[index + 1 :]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def oaep_encrypt(key, message: bytes, hash_name: str = "sha256", label: bytes = b"") -> bytes:
|
|
106
|
+
"""RSAES-OAEP encrypt with a public (or full) :class:`RSAKey`."""
|
|
107
|
+
k = _modulus_size(key.n)
|
|
108
|
+
encoded = oaep_encode(message, k, hash_name, label)
|
|
109
|
+
m = _os2ip(encoded)
|
|
110
|
+
if m >= key.n:
|
|
111
|
+
raise ValueError("encoded message representative out of range")
|
|
112
|
+
return _i2osp(pow(m, key.e, key.n), k)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def oaep_decrypt(key, ciphertext: bytes, hash_name: str = "sha256", label: bytes = b"") -> bytes:
|
|
116
|
+
"""RSAES-OAEP decrypt; requires the private exponent."""
|
|
117
|
+
if key.d is None:
|
|
118
|
+
raise DecryptionError("decryption error")
|
|
119
|
+
k = _modulus_size(key.n)
|
|
120
|
+
if len(ciphertext) != k:
|
|
121
|
+
raise DecryptionError("decryption error")
|
|
122
|
+
c = _os2ip(ciphertext)
|
|
123
|
+
if c >= key.n:
|
|
124
|
+
raise DecryptionError("decryption error")
|
|
125
|
+
m = pow(c, key.d, key.n)
|
|
126
|
+
return oaep_decode(_i2osp(m, k), k, hash_name, label)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# --------------------------------------------------------------------------- #
|
|
130
|
+
# RSASSA-PSS
|
|
131
|
+
# --------------------------------------------------------------------------- #
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def pss_encode(message: bytes, em_bits: int, hash_name: str = "sha256", salt_length: int = 32) -> bytes:
|
|
135
|
+
"""EMSA-PSS encoding (RFC 8017 section 9.1.1)."""
|
|
136
|
+
hlen = _hash_length(hash_name)
|
|
137
|
+
em_len = (em_bits + 7) // 8
|
|
138
|
+
if em_len < hlen + salt_length + 2:
|
|
139
|
+
raise ValueError("encoding error")
|
|
140
|
+
m_hash = _digest(hash_name, message)
|
|
141
|
+
salt = os.urandom(salt_length)
|
|
142
|
+
h = _digest(hash_name, b"\x00" * 8 + m_hash + salt)
|
|
143
|
+
ps = b"\x00" * (em_len - salt_length - hlen - 2)
|
|
144
|
+
db = ps + b"\x01" + salt
|
|
145
|
+
db_mask = mgf1(h, em_len - hlen - 1, hash_name)
|
|
146
|
+
masked_db = bytearray(_xor(db, db_mask))
|
|
147
|
+
masked_db[0] &= 0xFF >> (8 * em_len - em_bits)
|
|
148
|
+
return bytes(masked_db) + h + b"\xbc"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def pss_verify(message: bytes, encoded: bytes, em_bits: int, hash_name: str = "sha256",
|
|
152
|
+
salt_length: int = 32) -> bool:
|
|
153
|
+
"""EMSA-PSS verification (RFC 8017 section 9.1.2). Returns a bool."""
|
|
154
|
+
hlen = _hash_length(hash_name)
|
|
155
|
+
em_len = (em_bits + 7) // 8
|
|
156
|
+
if len(encoded) != em_len or em_len < hlen + salt_length + 2:
|
|
157
|
+
return False
|
|
158
|
+
if encoded[-1] != 0xBC:
|
|
159
|
+
return False
|
|
160
|
+
masked_db = bytearray(encoded[: em_len - hlen - 1])
|
|
161
|
+
h = encoded[em_len - hlen - 1 : em_len - 1]
|
|
162
|
+
if masked_db[0] & ~(0xFF >> (8 * em_len - em_bits)):
|
|
163
|
+
return False
|
|
164
|
+
masked_db[0] &= 0xFF >> (8 * em_len - em_bits)
|
|
165
|
+
db = bytearray(_xor(bytes(masked_db), mgf1(h, em_len - hlen - 1, hash_name)))
|
|
166
|
+
db[0] &= 0xFF >> (8 * em_len - em_bits)
|
|
167
|
+
ps_len = em_len - hlen - salt_length - 2
|
|
168
|
+
if db[:ps_len] != b"\x00" * ps_len or db[ps_len] != 0x01:
|
|
169
|
+
return False
|
|
170
|
+
salt = bytes(db[-salt_length:]) if salt_length else b""
|
|
171
|
+
m_hash = _digest(hash_name, message)
|
|
172
|
+
return h == _digest(hash_name, b"\x00" * 8 + m_hash + salt)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def pss_sign(key, message: bytes, hash_name: str = "sha256", salt_length: int = 32) -> bytes:
|
|
176
|
+
"""RSASSA-PSS sign; requires the private exponent."""
|
|
177
|
+
if key.d is None:
|
|
178
|
+
raise InvalidSignatureError("signing requires a private key")
|
|
179
|
+
k = _modulus_size(key.n)
|
|
180
|
+
em_bits = key.n.bit_length() - 1
|
|
181
|
+
encoded = pss_encode(message, em_bits, hash_name, salt_length)
|
|
182
|
+
m = _os2ip(encoded)
|
|
183
|
+
if m >= key.n:
|
|
184
|
+
raise ValueError("encoded message representative out of range")
|
|
185
|
+
return _i2osp(pow(m, key.d, key.n), k)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def pss_verify_signature(key, message: bytes, signature: bytes, hash_name: str = "sha256",
|
|
189
|
+
salt_length: int = 32) -> bool:
|
|
190
|
+
"""RSASSA-PSS verify with a public (or full) :class:`RSAKey`."""
|
|
191
|
+
k = _modulus_size(key.n)
|
|
192
|
+
if len(signature) != k:
|
|
193
|
+
return False
|
|
194
|
+
s = _os2ip(signature)
|
|
195
|
+
if s >= key.n:
|
|
196
|
+
return False
|
|
197
|
+
m = pow(s, key.e, key.n)
|
|
198
|
+
em_bits = key.n.bit_length() - 1
|
|
199
|
+
return pss_verify(message, _i2osp(m, k), em_bits, hash_name, salt_length)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = [
|
|
203
|
+
"mgf1",
|
|
204
|
+
"oaep_encode",
|
|
205
|
+
"oaep_decode",
|
|
206
|
+
"oaep_encrypt",
|
|
207
|
+
"oaep_decrypt",
|
|
208
|
+
"pss_encode",
|
|
209
|
+
"pss_verify",
|
|
210
|
+
"pss_sign",
|
|
211
|
+
"pss_verify_signature",
|
|
212
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""RSA public-key primitives.
|
|
2
|
+
|
|
3
|
+
Pure Python, standard library only. Key generation uses the Miller-Rabin
|
|
4
|
+
based :mod:`crypto_gu.number_theory` primes. Textbook RSA helpers are
|
|
5
|
+
provided, plus :func:`construct_private` to build a private key from an
|
|
6
|
+
explicit factorisation (the output of the attacks in
|
|
7
|
+
:mod:`crypto_gu.attacks.rsa`).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from crypto_gu import number_theory as nt
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _i2b(value: int, size: int) -> bytes:
|
|
18
|
+
return value.to_bytes(size, "big")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _b2i(data: bytes) -> int:
|
|
22
|
+
return int.from_bytes(data, "big")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RSAKey:
|
|
27
|
+
"""A minimal RSA public/private key pair. ``d`` may be ``None``."""
|
|
28
|
+
|
|
29
|
+
n: int
|
|
30
|
+
e: int = 65537
|
|
31
|
+
d: int | None = None
|
|
32
|
+
p: int | None = None
|
|
33
|
+
q: int | None = None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def generate(cls, bits: int = 1024, e: int = 65537) -> "RSAKey":
|
|
37
|
+
half = bits // 2
|
|
38
|
+
p = nt.rand_prime(half)
|
|
39
|
+
q = nt.rand_prime(bits - half)
|
|
40
|
+
while q == p:
|
|
41
|
+
q = nt.rand_prime(bits - half)
|
|
42
|
+
return cls._from_pq(p, q, e)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def _from_pq(cls, p: int, q: int, e: int) -> "RSAKey":
|
|
46
|
+
n = p * q
|
|
47
|
+
phi = (p - 1) * (q - 1)
|
|
48
|
+
d = nt.modinv(e, phi)
|
|
49
|
+
return cls(n=n, e=e, d=d, p=p, q=q)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_pq(cls, p: int, q: int, e: int = 65537) -> "RSAKey":
|
|
53
|
+
return cls._from_pq(p, q, e)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_ned(cls, n: int, e: int, d: int) -> "RSAKey":
|
|
57
|
+
return cls(n=n, e=e, d=d)
|
|
58
|
+
|
|
59
|
+
def public(self) -> "RSAKey":
|
|
60
|
+
return RSAKey(n=self.n, e=self.e)
|
|
61
|
+
|
|
62
|
+
def _size(self) -> int:
|
|
63
|
+
return (self.n.bit_length() + 7) // 8
|
|
64
|
+
|
|
65
|
+
def encrypt(self, message: bytes) -> bytes:
|
|
66
|
+
m = _b2i(message)
|
|
67
|
+
if m >= self.n:
|
|
68
|
+
raise ValueError("message too long for this RSA modulus")
|
|
69
|
+
return _i2b(pow(m, self.e, self.n), self._size())
|
|
70
|
+
|
|
71
|
+
def decrypt(self, ciphertext: bytes) -> bytes:
|
|
72
|
+
if self.d is None:
|
|
73
|
+
raise ValueError("cannot decrypt with a public-only key")
|
|
74
|
+
c = _b2i(ciphertext)
|
|
75
|
+
m = _i2b(pow(c, self.d, self.n), self._size()).lstrip(b"\x00")
|
|
76
|
+
return m or b"\x00"
|
|
77
|
+
|
|
78
|
+
def sign(self, message: bytes) -> bytes:
|
|
79
|
+
return self.decrypt(message)
|
|
80
|
+
|
|
81
|
+
def verify(self, message: bytes, signature: bytes) -> bool:
|
|
82
|
+
return _b2i(self.encrypt(signature)) == _b2i(message)
|
|
83
|
+
|
|
84
|
+
def max_bytes(self) -> int:
|
|
85
|
+
return (self.n.bit_length() - 1) // 8
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def generate_keypair(bits: int = 1024, e: int = 65537) -> RSAKey:
|
|
89
|
+
return RSAKey.generate(bits, e)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def public_encrypt(n: int, e: int, message: bytes) -> bytes:
|
|
93
|
+
return RSAKey(n=n, e=e).encrypt(message)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def private_decrypt(n: int, e: int, d: int, ciphertext: bytes) -> bytes:
|
|
97
|
+
return RSAKey(n=n, e=e, d=d).decrypt(ciphertext)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def construct_private(n: int, p: int, q: int, e: int = 65537) -> RSAKey:
|
|
101
|
+
"""Build the private key from a recovered factorisation."""
|
|
102
|
+
if p * q != n:
|
|
103
|
+
raise ValueError("p * q does not equal n")
|
|
104
|
+
return RSAKey._from_pq(p, q, e)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ["RSAKey", "generate_keypair", "public_encrypt", "private_decrypt",
|
|
108
|
+
"construct_private"]
|
|
File without changes
|