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.
Files changed (53) hide show
  1. crypto_gu-0.1.0/LICENSE +21 -0
  2. crypto_gu-0.1.0/PKG-INFO +130 -0
  3. crypto_gu-0.1.0/README.md +107 -0
  4. crypto_gu-0.1.0/crypto_gu/__init__.py +12 -0
  5. crypto_gu-0.1.0/crypto_gu/asymmetric/__init__.py +0 -0
  6. crypto_gu-0.1.0/crypto_gu/asymmetric/pkcs1.py +212 -0
  7. crypto_gu-0.1.0/crypto_gu/asymmetric/rsa.py +108 -0
  8. crypto_gu-0.1.0/crypto_gu/attacks/__init__.py +0 -0
  9. crypto_gu-0.1.0/crypto_gu/attacks/aes.py +214 -0
  10. crypto_gu-0.1.0/crypto_gu/attacks/rsa.py +134 -0
  11. crypto_gu-0.1.0/crypto_gu/constant_time.py +52 -0
  12. crypto_gu-0.1.0/crypto_gu/encoding.py +332 -0
  13. crypto_gu-0.1.0/crypto_gu/errors.py +65 -0
  14. crypto_gu-0.1.0/crypto_gu/hashes/__init__.py +45 -0
  15. crypto_gu-0.1.0/crypto_gu/hashes/blake2b.py +113 -0
  16. crypto_gu-0.1.0/crypto_gu/hashes/blake2s.py +113 -0
  17. crypto_gu-0.1.0/crypto_gu/hashes/hmac.py +104 -0
  18. crypto_gu-0.1.0/crypto_gu/hashes/md5.py +101 -0
  19. crypto_gu-0.1.0/crypto_gu/hashes/sha1.py +76 -0
  20. crypto_gu-0.1.0/crypto_gu/hashes/sha256.py +84 -0
  21. crypto_gu-0.1.0/crypto_gu/hashes/sha512.py +103 -0
  22. crypto_gu-0.1.0/crypto_gu/kdf.py +229 -0
  23. crypto_gu-0.1.0/crypto_gu/number_theory.py +499 -0
  24. crypto_gu-0.1.0/crypto_gu/padding.py +173 -0
  25. crypto_gu-0.1.0/crypto_gu/rng/__init__.py +0 -0
  26. crypto_gu-0.1.0/crypto_gu/rng/mt19937.py +140 -0
  27. crypto_gu-0.1.0/crypto_gu/symmetric/__init__.py +0 -0
  28. crypto_gu-0.1.0/crypto_gu/symmetric/aes.py +369 -0
  29. crypto_gu-0.1.0/crypto_gu/symmetric/aes_gcm.py +126 -0
  30. crypto_gu-0.1.0/crypto_gu/symmetric/chacha20.py +105 -0
  31. crypto_gu-0.1.0/crypto_gu/symmetric/chacha20poly1305.py +74 -0
  32. crypto_gu-0.1.0/crypto_gu/symmetric/poly1305.py +38 -0
  33. crypto_gu-0.1.0/crypto_gu.egg-info/PKG-INFO +130 -0
  34. crypto_gu-0.1.0/crypto_gu.egg-info/SOURCES.txt +51 -0
  35. crypto_gu-0.1.0/crypto_gu.egg-info/dependency_links.txt +1 -0
  36. crypto_gu-0.1.0/crypto_gu.egg-info/requires.txt +4 -0
  37. crypto_gu-0.1.0/crypto_gu.egg-info/top_level.txt +1 -0
  38. crypto_gu-0.1.0/pyproject.toml +40 -0
  39. crypto_gu-0.1.0/setup.cfg +4 -0
  40. crypto_gu-0.1.0/tests/test_aead.py +162 -0
  41. crypto_gu-0.1.0/tests/test_aes.py +143 -0
  42. crypto_gu-0.1.0/tests/test_aes_attacks.py +126 -0
  43. crypto_gu-0.1.0/tests/test_blake2.py +134 -0
  44. crypto_gu-0.1.0/tests/test_chacha20.py +119 -0
  45. crypto_gu-0.1.0/tests/test_constant_time.py +52 -0
  46. crypto_gu-0.1.0/tests/test_encoding.py +210 -0
  47. crypto_gu-0.1.0/tests/test_hashes.py +223 -0
  48. crypto_gu-0.1.0/tests/test_kdf.py +171 -0
  49. crypto_gu-0.1.0/tests/test_mt19937.py +105 -0
  50. crypto_gu-0.1.0/tests/test_number_theory.py +246 -0
  51. crypto_gu-0.1.0/tests/test_pkcs1.py +120 -0
  52. crypto_gu-0.1.0/tests/test_rsa.py +152 -0
  53. crypto_gu-0.1.0/tests/test_sha_variants.py +108 -0
@@ -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.
@@ -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