crypto-toolkit-py 1.0.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_toolkit_py-1.0.0/CHANGELOG.md +9 -0
- crypto_toolkit_py-1.0.0/LICENSE +21 -0
- crypto_toolkit_py-1.0.0/MANIFEST.in +7 -0
- crypto_toolkit_py-1.0.0/PKG-INFO +116 -0
- crypto_toolkit_py-1.0.0/README.md +69 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit/__init__.py +34 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit/encrypt.py +109 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit/exceptions.py +36 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit/hash.py +72 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit/password.py +203 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit/py.typed +0 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit/sign.py +79 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit_py.egg-info/PKG-INFO +116 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit_py.egg-info/SOURCES.txt +18 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit_py.egg-info/dependency_links.txt +1 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit_py.egg-info/requires.txt +20 -0
- crypto_toolkit_py-1.0.0/crypto_toolkit_py.egg-info/top_level.txt +1 -0
- crypto_toolkit_py-1.0.0/pyproject.toml +85 -0
- crypto_toolkit_py-1.0.0/setup.cfg +4 -0
- crypto_toolkit_py-1.0.0/setup.py +10 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 - 2026-08-29
|
|
4
|
+
|
|
5
|
+
- Initial Python release
|
|
6
|
+
- Safe hashing (SHA-2, SHA-3, BLAKE2) with HMAC
|
|
7
|
+
- Password hashing and key derivation (PBKDF2, scrypt, Argon2id, bcrypt)
|
|
8
|
+
- AEAD symmetric encryption (AES-256-GCM, ChaCha20-Poly1305)
|
|
9
|
+
- Ed25519 digital signatures
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parthiv Rawat
|
|
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,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crypto-toolkit-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Modern Cryptography & Hashing Toolkit - a misuse-resistant, high-level cryptography library for Python
|
|
5
|
+
Author-email: Parthiv Rawat <parthiv05022000@gmail.com>
|
|
6
|
+
Maintainer-email: Parthiv Rawat <parthiv05022000@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/parthivrawat/crypto-toolkit-py
|
|
9
|
+
Project-URL: Documentation, https://github.com/parthivrawat/crypto-toolkit-py/tree/main/python#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/parthivrawat/crypto-toolkit-py
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/parthivrawat/crypto-toolkit-py/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/parthivrawat/crypto-toolkit-py/blob/main/python/CHANGELOG.md
|
|
13
|
+
Keywords: crypto,cryptography,hash,hashing,hmac,password,encryption,argon2,ed25519,security
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Security
|
|
25
|
+
Classifier: Topic :: Security :: Cryptography
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: crypto
|
|
32
|
+
Requires-Dist: cryptography>=43.0.0; extra == "crypto"
|
|
33
|
+
Provides-Extra: argon2
|
|
34
|
+
Requires-Dist: argon2-cffi>=23.1.0; extra == "argon2"
|
|
35
|
+
Provides-Extra: bcrypt
|
|
36
|
+
Requires-Dist: bcrypt>=4.0.0; extra == "bcrypt"
|
|
37
|
+
Provides-Extra: full
|
|
38
|
+
Requires-Dist: cryptography>=43.0.0; extra == "full"
|
|
39
|
+
Requires-Dist: argon2-cffi>=23.1.0; extra == "full"
|
|
40
|
+
Requires-Dist: bcrypt>=4.0.0; extra == "full"
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# Modern Cryptography & Hashing Toolkit (Python)
|
|
49
|
+
|
|
50
|
+
A misuse-resistant, high-level cryptography library with safe defaults, constant-time verification, and a clear API for hashing, password hashing, key derivation, symmetric encryption, HMAC, and digital signatures.
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Safe hashing**: SHA-256, SHA-384, SHA-512, SHA-3, BLAKE2 (MD5 and SHA-1 rejected)
|
|
55
|
+
- **HMAC**: Constant-time verification
|
|
56
|
+
- **Password hashing**: PBKDF2-SHA256, scrypt, Argon2id, bcrypt with adaptive costs
|
|
57
|
+
- **Key derivation**: PBKDF2 and scrypt
|
|
58
|
+
- **Symmetric encryption**: AES-256-GCM or ChaCha20-Poly1305 with automatic nonce management
|
|
59
|
+
- **Digital signatures**: Ed25519
|
|
60
|
+
- **Zero runtime dependencies** for core hashing, HMAC, and key derivation
|
|
61
|
+
- **Optional extras** for AEAD encryption and signatures (`[crypto]`), Argon2id (`[argon2]`), and bcrypt (`[bcrypt]`)
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install crypto-toolkit-py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
With all optional dependencies:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install "crypto-toolkit-py[full]"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from crypto_toolkit import hash, password, encrypt, sign
|
|
79
|
+
|
|
80
|
+
# Safe, deterministic hashing
|
|
81
|
+
sha256 = hash.string('hello world', algorithm='sha-256')
|
|
82
|
+
print(sha256)
|
|
83
|
+
|
|
84
|
+
mac = hash.hmac('secret-key', 'message', algorithm='sha-256')
|
|
85
|
+
assert hash.verify_hmac(mac, 'secret-key', 'message', algorithm='sha-256')
|
|
86
|
+
|
|
87
|
+
# Password hashing
|
|
88
|
+
ph = password.hash('user-password')
|
|
89
|
+
assert password.verify('user-password', ph)
|
|
90
|
+
|
|
91
|
+
# Key derivation
|
|
92
|
+
salt = b'16-or-more-random-bytes'
|
|
93
|
+
key = password.derive('passphrase', salt=salt, length=32)
|
|
94
|
+
|
|
95
|
+
# Symmetric encryption (requires cryptography)
|
|
96
|
+
ciphertext = encrypt.symmetric('sensitive data', key=key)
|
|
97
|
+
plaintext = encrypt.decrypt(ciphertext, key=key)
|
|
98
|
+
assert plaintext == 'sensitive data'
|
|
99
|
+
|
|
100
|
+
# Digital signatures (requires cryptography)
|
|
101
|
+
private_key, public_key = sign.generate_keypair()
|
|
102
|
+
signature = sign.ed25519('message', private_key=private_key)
|
|
103
|
+
assert sign.verify(signature, 'message', public_key=public_key)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cd implementations/security/crypto-toolkit/python
|
|
110
|
+
pip install -e ".[dev,full]"
|
|
111
|
+
pytest test_crypto_toolkit.py -v
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT License
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Modern Cryptography & Hashing Toolkit (Python)
|
|
2
|
+
|
|
3
|
+
A misuse-resistant, high-level cryptography library with safe defaults, constant-time verification, and a clear API for hashing, password hashing, key derivation, symmetric encryption, HMAC, and digital signatures.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Safe hashing**: SHA-256, SHA-384, SHA-512, SHA-3, BLAKE2 (MD5 and SHA-1 rejected)
|
|
8
|
+
- **HMAC**: Constant-time verification
|
|
9
|
+
- **Password hashing**: PBKDF2-SHA256, scrypt, Argon2id, bcrypt with adaptive costs
|
|
10
|
+
- **Key derivation**: PBKDF2 and scrypt
|
|
11
|
+
- **Symmetric encryption**: AES-256-GCM or ChaCha20-Poly1305 with automatic nonce management
|
|
12
|
+
- **Digital signatures**: Ed25519
|
|
13
|
+
- **Zero runtime dependencies** for core hashing, HMAC, and key derivation
|
|
14
|
+
- **Optional extras** for AEAD encryption and signatures (`[crypto]`), Argon2id (`[argon2]`), and bcrypt (`[bcrypt]`)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install crypto-toolkit-py
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
With all optional dependencies:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install "crypto-toolkit-py[full]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from crypto_toolkit import hash, password, encrypt, sign
|
|
32
|
+
|
|
33
|
+
# Safe, deterministic hashing
|
|
34
|
+
sha256 = hash.string('hello world', algorithm='sha-256')
|
|
35
|
+
print(sha256)
|
|
36
|
+
|
|
37
|
+
mac = hash.hmac('secret-key', 'message', algorithm='sha-256')
|
|
38
|
+
assert hash.verify_hmac(mac, 'secret-key', 'message', algorithm='sha-256')
|
|
39
|
+
|
|
40
|
+
# Password hashing
|
|
41
|
+
ph = password.hash('user-password')
|
|
42
|
+
assert password.verify('user-password', ph)
|
|
43
|
+
|
|
44
|
+
# Key derivation
|
|
45
|
+
salt = b'16-or-more-random-bytes'
|
|
46
|
+
key = password.derive('passphrase', salt=salt, length=32)
|
|
47
|
+
|
|
48
|
+
# Symmetric encryption (requires cryptography)
|
|
49
|
+
ciphertext = encrypt.symmetric('sensitive data', key=key)
|
|
50
|
+
plaintext = encrypt.decrypt(ciphertext, key=key)
|
|
51
|
+
assert plaintext == 'sensitive data'
|
|
52
|
+
|
|
53
|
+
# Digital signatures (requires cryptography)
|
|
54
|
+
private_key, public_key = sign.generate_keypair()
|
|
55
|
+
signature = sign.ed25519('message', private_key=private_key)
|
|
56
|
+
assert sign.verify(signature, 'message', public_key=public_key)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd implementations/security/crypto-toolkit/python
|
|
63
|
+
pip install -e ".[dev,full]"
|
|
64
|
+
pytest test_crypto_toolkit.py -v
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT License
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Modern Cryptography & Hashing Toolkit for Python.
|
|
2
|
+
|
|
3
|
+
A misuse-resistant, high-level cryptography library with safe defaults,
|
|
4
|
+
constant-time verification, and clear APIs for hashing, password hashing,
|
|
5
|
+
key derivation, symmetric encryption, HMAC, and digital signatures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from . import encrypt, hash, password, sign
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
AlgorithmError,
|
|
11
|
+
CryptoKitError,
|
|
12
|
+
DecryptionError,
|
|
13
|
+
InvalidKeyError,
|
|
14
|
+
MissingDependencyError,
|
|
15
|
+
SignatureError,
|
|
16
|
+
VerificationError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AlgorithmError",
|
|
23
|
+
"CryptoKitError",
|
|
24
|
+
"DecryptionError",
|
|
25
|
+
"InvalidKeyError",
|
|
26
|
+
"MissingDependencyError",
|
|
27
|
+
"SignatureError",
|
|
28
|
+
"VerificationError",
|
|
29
|
+
"__version__",
|
|
30
|
+
"encrypt",
|
|
31
|
+
"hash",
|
|
32
|
+
"password",
|
|
33
|
+
"sign",
|
|
34
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Symmetric AEAD encryption with safe, modern defaults."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
from .exceptions import AlgorithmError, DecryptionError, InvalidKeyError, MissingDependencyError
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
10
|
+
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
11
|
+
|
|
12
|
+
_HAS_CRYPTOGRAPHY = True
|
|
13
|
+
except Exception: # pragma: no cover
|
|
14
|
+
_HAS_CRYPTOGRAPHY = False
|
|
15
|
+
|
|
16
|
+
_VERSION = 1
|
|
17
|
+
|
|
18
|
+
_ALGORITHMS: dict = {}
|
|
19
|
+
if _HAS_CRYPTOGRAPHY:
|
|
20
|
+
_ALGORITHMS = {
|
|
21
|
+
"aes-256-gcm": {
|
|
22
|
+
"id": 1,
|
|
23
|
+
"key_len": 32,
|
|
24
|
+
"nonce_len": 12,
|
|
25
|
+
"cipher_cls": AESGCM,
|
|
26
|
+
},
|
|
27
|
+
"chacha20-poly1305": {
|
|
28
|
+
"id": 2,
|
|
29
|
+
"key_len": 32,
|
|
30
|
+
"nonce_len": 12,
|
|
31
|
+
"cipher_cls": ChaCha20Poly1305,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_ALGO_BY_ID = {v["id"]: k for k, v in _ALGORITHMS.items()}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _require_crypto():
|
|
39
|
+
if not _HAS_CRYPTOGRAPHY:
|
|
40
|
+
raise MissingDependencyError(
|
|
41
|
+
"AEAD encryption requires cryptography. Install: pip install 'crypto-toolkit-py[crypto]'"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_algorithm(algorithm: str) -> dict:
|
|
46
|
+
try:
|
|
47
|
+
return _ALGORITHMS[algorithm]
|
|
48
|
+
except KeyError:
|
|
49
|
+
raise AlgorithmError(f"Unsupported cipher: {algorithm}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _make_cipher(algorithm: str, key: bytes):
|
|
53
|
+
spec = _load_algorithm(algorithm)
|
|
54
|
+
if len(key) != spec["key_len"]:
|
|
55
|
+
raise InvalidKeyError(f"{algorithm} requires a {spec['key_len']}-byte key")
|
|
56
|
+
return spec["cipher_cls"](key)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def symmetric(data: Union[str, bytes], key: bytes, algorithm: str = "aes-256-gcm") -> bytes:
|
|
60
|
+
"""Encrypt ``data`` with an AEAD cipher using a versioned header."""
|
|
61
|
+
_require_crypto()
|
|
62
|
+
if isinstance(data, str):
|
|
63
|
+
data = data.encode("utf-8")
|
|
64
|
+
|
|
65
|
+
spec = _load_algorithm(algorithm)
|
|
66
|
+
cipher = _make_cipher(algorithm, key)
|
|
67
|
+
nonce = secrets.token_bytes(spec["nonce_len"])
|
|
68
|
+
ciphertext = cipher.encrypt(nonce, data, None)
|
|
69
|
+
return bytes([_VERSION, spec["id"]]) + nonce + ciphertext
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def decrypt(token: bytes, key: bytes, encoding: Optional[str] = "utf-8") -> Union[str, bytes]:
|
|
73
|
+
"""Decrypt and authenticate a token produced by ``symmetric``."""
|
|
74
|
+
_require_crypto()
|
|
75
|
+
if len(token) < 2:
|
|
76
|
+
raise DecryptionError("Ciphertext too short")
|
|
77
|
+
|
|
78
|
+
version = token[0]
|
|
79
|
+
if version != _VERSION:
|
|
80
|
+
raise DecryptionError(f"Unsupported ciphertext version: {version}")
|
|
81
|
+
|
|
82
|
+
algo_id = token[1]
|
|
83
|
+
algorithm = _ALGO_BY_ID.get(algo_id)
|
|
84
|
+
if algorithm is None:
|
|
85
|
+
raise DecryptionError(f"Unknown algorithm id: {algo_id}")
|
|
86
|
+
|
|
87
|
+
spec = _load_algorithm(algorithm)
|
|
88
|
+
if len(key) != spec["key_len"]:
|
|
89
|
+
raise InvalidKeyError(f"{algorithm} requires a {spec['key_len']}-byte key")
|
|
90
|
+
|
|
91
|
+
nonce_len = spec["nonce_len"]
|
|
92
|
+
if len(token) < 2 + nonce_len + 16:
|
|
93
|
+
raise DecryptionError("Ciphertext too short")
|
|
94
|
+
|
|
95
|
+
nonce = token[2 : 2 + nonce_len]
|
|
96
|
+
ciphertext = token[2 + nonce_len :]
|
|
97
|
+
cipher = _make_cipher(algorithm, key)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
plaintext = cipher.decrypt(nonce, ciphertext, None)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
raise DecryptionError("Decryption or authentication failed") from exc
|
|
103
|
+
|
|
104
|
+
if encoding:
|
|
105
|
+
try:
|
|
106
|
+
return plaintext.decode(encoding)
|
|
107
|
+
except UnicodeDecodeError:
|
|
108
|
+
return plaintext
|
|
109
|
+
return plaintext
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Exceptions raised by the crypto toolkit."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CryptoKitError(Exception):
|
|
5
|
+
"""Base exception for the crypto toolkit."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AlgorithmError(CryptoKitError):
|
|
10
|
+
"""Raised when an invalid or unsafe algorithm is requested."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidKeyError(CryptoKitError):
|
|
15
|
+
"""Raised when a key is malformed or has an incorrect length."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DecryptionError(CryptoKitError):
|
|
20
|
+
"""Raised when decryption or authentication fails."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SignatureError(CryptoKitError):
|
|
25
|
+
"""Raised when a signature operation fails."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MissingDependencyError(CryptoKitError):
|
|
30
|
+
"""Raised when an optional dependency is required but not installed."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VerificationError(CryptoKitError):
|
|
35
|
+
"""Raised when a password hash format is malformed."""
|
|
36
|
+
pass
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Deterministic hashing and HMAC with safe, modern defaults."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac as _hmac
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from .exceptions import AlgorithmError
|
|
9
|
+
|
|
10
|
+
_ALLOWED_HASH = frozenset(
|
|
11
|
+
{
|
|
12
|
+
"sha-256",
|
|
13
|
+
"sha-384",
|
|
14
|
+
"sha-512",
|
|
15
|
+
"sha3-256",
|
|
16
|
+
"sha3-384",
|
|
17
|
+
"sha3-512",
|
|
18
|
+
"blake2b",
|
|
19
|
+
"blake2s",
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_HASHLIB_NAME = {
|
|
24
|
+
"sha-256": "sha256",
|
|
25
|
+
"sha-384": "sha384",
|
|
26
|
+
"sha-512": "sha512",
|
|
27
|
+
"sha3-256": "sha3_256",
|
|
28
|
+
"sha3-384": "sha3_384",
|
|
29
|
+
"sha3-512": "sha3_512",
|
|
30
|
+
"blake2b": "blake2b",
|
|
31
|
+
"blake2s": "blake2s",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _normalize(algorithm: str) -> str:
|
|
36
|
+
name = algorithm.lower().replace("_", "-")
|
|
37
|
+
if name not in _ALLOWED_HASH:
|
|
38
|
+
raise AlgorithmError(f"Unsupported or unsafe hashing algorithm: {algorithm}")
|
|
39
|
+
return _HASHLIB_NAME[name]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _to_bytes(data: Union[str, bytes]) -> bytes:
|
|
43
|
+
return data.encode("utf-8") if isinstance(data, str) else data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def string(data: Union[str, bytes], algorithm: str = "sha-256") -> str:
|
|
47
|
+
"""Return a safe, deterministic hash of ``data`` as a hex string."""
|
|
48
|
+
digestmod = _normalize(algorithm)
|
|
49
|
+
return hashlib.new(digestmod, _to_bytes(data)).hexdigest()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def file(path: Union[str, Path], algorithm: str = "sha-256", block_size: int = 8192) -> str:
|
|
53
|
+
"""Return the hash of a file as a hex string."""
|
|
54
|
+
digestmod = _normalize(algorithm)
|
|
55
|
+
h = hashlib.new(digestmod)
|
|
56
|
+
with open(path, "rb") as f:
|
|
57
|
+
for chunk in iter(lambda: f.read(block_size), b""):
|
|
58
|
+
h.update(chunk)
|
|
59
|
+
return h.hexdigest()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def hmac(key: Union[str, bytes], data: Union[str, bytes], algorithm: str = "sha-256") -> str:
|
|
63
|
+
"""Return an HMAC of ``data`` as a hex string."""
|
|
64
|
+
digestmod = _normalize(algorithm)
|
|
65
|
+
mac = _hmac.new(_to_bytes(key), _to_bytes(data), digestmod)
|
|
66
|
+
return mac.hexdigest()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def verify_hmac(mac: str, key: Union[str, bytes], data: Union[str, bytes], algorithm: str = "sha-256") -> bool:
|
|
70
|
+
"""Verify an HMAC in constant time."""
|
|
71
|
+
expected = hmac(key, data, algorithm)
|
|
72
|
+
return _hmac.compare_digest(mac, expected)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Password hashing, verification, and key derivation with safe defaults."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac as _hmac
|
|
6
|
+
import secrets
|
|
7
|
+
from typing import Any, Optional, Union
|
|
8
|
+
|
|
9
|
+
from .exceptions import AlgorithmError, MissingDependencyError, VerificationError
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from argon2 import PasswordHasher as _Argon2Hasher
|
|
13
|
+
from argon2 import Type as _Argon2Type
|
|
14
|
+
|
|
15
|
+
_HAS_ARGON2 = True
|
|
16
|
+
except Exception: # pragma: no cover
|
|
17
|
+
_HAS_ARGON2 = False
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import bcrypt as _bcrypt
|
|
21
|
+
|
|
22
|
+
_HAS_BCRYPT = True
|
|
23
|
+
except Exception: # pragma: no cover
|
|
24
|
+
_HAS_BCRYPT = False
|
|
25
|
+
|
|
26
|
+
_ALLOWED = frozenset({"argon2id", "scrypt", "bcrypt", "pbkdf2_sha256"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _scrypt_available() -> bool:
|
|
30
|
+
try:
|
|
31
|
+
hashlib.scrypt(b"", salt=b"", n=2, r=1, p=1, dklen=1, maxmem=1024)
|
|
32
|
+
return True
|
|
33
|
+
except Exception:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_SCRYPT_AVAILABLE = _scrypt_available()
|
|
38
|
+
|
|
39
|
+
_DEFAULT = (
|
|
40
|
+
"argon2id"
|
|
41
|
+
if _HAS_ARGON2
|
|
42
|
+
else ("scrypt" if _SCRYPT_AVAILABLE else "pbkdf2_sha256")
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _to_bytes(value: Union[str, bytes]) -> bytes:
|
|
47
|
+
return value.encode("utf-8") if isinstance(value, str) else value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_pbkdf2(hashed: str):
|
|
51
|
+
try:
|
|
52
|
+
_, iterations, salt_b64, key_b64 = hashed.split("$")
|
|
53
|
+
return int(iterations), base64.b64decode(salt_b64), base64.b64decode(key_b64)
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
raise VerificationError("Invalid PBKDF2 hash format") from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _parse_scrypt(hashed: str):
|
|
59
|
+
try:
|
|
60
|
+
_, n, r, p, salt_b64, key_b64 = hashed.split("$")
|
|
61
|
+
return (
|
|
62
|
+
int(n),
|
|
63
|
+
int(r),
|
|
64
|
+
int(p),
|
|
65
|
+
base64.b64decode(salt_b64),
|
|
66
|
+
base64.b64decode(key_b64),
|
|
67
|
+
)
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
raise VerificationError("Invalid scrypt hash format") from exc
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def hash(password: str, algorithm: Optional[str] = None, **params: Any) -> str:
|
|
73
|
+
"""Hash a password with the strongest available algorithm by default."""
|
|
74
|
+
algo = algorithm or _DEFAULT
|
|
75
|
+
if algo not in _ALLOWED:
|
|
76
|
+
raise AlgorithmError(f"Unsupported password algorithm: {algo}")
|
|
77
|
+
|
|
78
|
+
if algo == "argon2id":
|
|
79
|
+
if not _HAS_ARGON2:
|
|
80
|
+
raise MissingDependencyError(
|
|
81
|
+
"Argon2id requires argon2-cffi. Install: pip install 'crypto-toolkit-py[argon2]'"
|
|
82
|
+
)
|
|
83
|
+
ph = _Argon2Hasher(
|
|
84
|
+
time_cost=params.get("time_cost", 3),
|
|
85
|
+
memory_cost=params.get("memory_cost", 65536),
|
|
86
|
+
parallelism=params.get("parallelism", 4),
|
|
87
|
+
type=_Argon2Type.ID,
|
|
88
|
+
)
|
|
89
|
+
return ph.hash(password)
|
|
90
|
+
|
|
91
|
+
if algo == "bcrypt":
|
|
92
|
+
if not _HAS_BCRYPT:
|
|
93
|
+
raise MissingDependencyError(
|
|
94
|
+
"bcrypt requires the bcrypt package. Install: pip install 'crypto-toolkit-py[bcrypt]'"
|
|
95
|
+
)
|
|
96
|
+
salt = _bcrypt.gensalt(rounds=params.get("rounds", 12))
|
|
97
|
+
return _bcrypt.hashpw(password.encode(), salt).decode("ascii")
|
|
98
|
+
|
|
99
|
+
if algo == "scrypt":
|
|
100
|
+
if not _SCRYPT_AVAILABLE:
|
|
101
|
+
raise MissingDependencyError("scrypt is not available on this platform")
|
|
102
|
+
salt = secrets.token_bytes(32)
|
|
103
|
+
n = params.get("n", 16384)
|
|
104
|
+
r = params.get("r", 8)
|
|
105
|
+
p = params.get("p", 1)
|
|
106
|
+
dklen = params.get("dklen", 64)
|
|
107
|
+
key = hashlib.scrypt(
|
|
108
|
+
password.encode(),
|
|
109
|
+
salt=salt,
|
|
110
|
+
n=n,
|
|
111
|
+
r=r,
|
|
112
|
+
p=p,
|
|
113
|
+
dklen=dklen,
|
|
114
|
+
maxmem=params.get("maxmem", 0),
|
|
115
|
+
)
|
|
116
|
+
return f"scrypt${n}${r}${p}${base64.b64encode(salt).decode()}${base64.b64encode(key).decode()}"
|
|
117
|
+
|
|
118
|
+
# pbkdf2_sha256
|
|
119
|
+
iterations = params.get("iterations", 100_000)
|
|
120
|
+
salt = secrets.token_bytes(32)
|
|
121
|
+
key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)
|
|
122
|
+
return f"pbkdf2_sha256${iterations}${base64.b64encode(salt).decode()}${base64.b64encode(key).decode()}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def verify(password: str, hashed: str) -> bool:
|
|
126
|
+
"""Verify a password against a hash in constant time."""
|
|
127
|
+
try:
|
|
128
|
+
if hashed.startswith("$argon2id$"):
|
|
129
|
+
if not _HAS_ARGON2:
|
|
130
|
+
return False
|
|
131
|
+
ph = _Argon2Hasher()
|
|
132
|
+
ph.verify(hashed, password)
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
if hashed.startswith("$2") and len(hashed) >= 59:
|
|
136
|
+
if not _HAS_BCRYPT:
|
|
137
|
+
return False
|
|
138
|
+
return _bcrypt.checkpw(password.encode(), hashed.encode())
|
|
139
|
+
|
|
140
|
+
if hashed.startswith("scrypt$"):
|
|
141
|
+
if not _SCRYPT_AVAILABLE:
|
|
142
|
+
return False
|
|
143
|
+
n, r, p, salt, stored_key = _parse_scrypt(hashed)
|
|
144
|
+
candidate = hashlib.scrypt(
|
|
145
|
+
password.encode(),
|
|
146
|
+
salt=salt,
|
|
147
|
+
n=n,
|
|
148
|
+
r=r,
|
|
149
|
+
p=p,
|
|
150
|
+
dklen=len(stored_key),
|
|
151
|
+
maxmem=0,
|
|
152
|
+
)
|
|
153
|
+
return _hmac.compare_digest(candidate, stored_key)
|
|
154
|
+
|
|
155
|
+
if hashed.startswith("pbkdf2_sha256$"):
|
|
156
|
+
iterations, salt, stored_key = _parse_pbkdf2(hashed)
|
|
157
|
+
candidate = hashlib.pbkdf2_hmac(
|
|
158
|
+
"sha256",
|
|
159
|
+
password.encode(),
|
|
160
|
+
salt,
|
|
161
|
+
iterations,
|
|
162
|
+
dklen=len(stored_key),
|
|
163
|
+
)
|
|
164
|
+
return _hmac.compare_digest(candidate, stored_key)
|
|
165
|
+
except Exception:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def derive(
|
|
172
|
+
passphrase: Union[str, bytes],
|
|
173
|
+
salt: Optional[bytes] = None,
|
|
174
|
+
length: int = 32,
|
|
175
|
+
algorithm: str = "pbkdf2_sha256",
|
|
176
|
+
**params: Any,
|
|
177
|
+
) -> bytes:
|
|
178
|
+
"""Derive a key from a passphrase and salt."""
|
|
179
|
+
if salt is None:
|
|
180
|
+
raise AlgorithmError("A non-None salt is required for deterministic key derivation")
|
|
181
|
+
if length < 1:
|
|
182
|
+
raise AlgorithmError("length must be a positive integer")
|
|
183
|
+
|
|
184
|
+
p = _to_bytes(passphrase)
|
|
185
|
+
|
|
186
|
+
if algorithm == "pbkdf2_sha256":
|
|
187
|
+
iterations = params.get("iterations", 100_000)
|
|
188
|
+
return hashlib.pbkdf2_hmac("sha256", p, salt, iterations, dklen=length)
|
|
189
|
+
|
|
190
|
+
if algorithm == "scrypt":
|
|
191
|
+
if not _SCRYPT_AVAILABLE:
|
|
192
|
+
raise MissingDependencyError("scrypt is not available on this platform")
|
|
193
|
+
return hashlib.scrypt(
|
|
194
|
+
p,
|
|
195
|
+
salt=salt,
|
|
196
|
+
n=params.get("n", 16384),
|
|
197
|
+
r=params.get("r", 8),
|
|
198
|
+
p=params.get("p", 1),
|
|
199
|
+
dklen=length,
|
|
200
|
+
maxmem=params.get("maxmem", 0),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
raise AlgorithmError(f"Unsupported key derivation algorithm: {algorithm}")
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Digital signatures (Ed25519, ECDSA, RSA-PSS)."""
|
|
2
|
+
|
|
3
|
+
from typing import Tuple, Union
|
|
4
|
+
|
|
5
|
+
from .exceptions import AlgorithmError, InvalidKeyError, MissingDependencyError, SignatureError
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from cryptography.exceptions import InvalidSignature
|
|
9
|
+
from cryptography.hazmat.primitives import serialization
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519 as _ed25519
|
|
11
|
+
|
|
12
|
+
_HAS_CRYPTOGRAPHY = True
|
|
13
|
+
except Exception: # pragma: no cover
|
|
14
|
+
_HAS_CRYPTOGRAPHY = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _require_crypto():
|
|
18
|
+
if not _HAS_CRYPTOGRAPHY:
|
|
19
|
+
raise MissingDependencyError(
|
|
20
|
+
"Digital signatures require cryptography. Install: pip install 'crypto-toolkit-py[crypto]'"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _to_bytes(value: Union[str, bytes]) -> bytes:
|
|
25
|
+
return value.encode("utf-8") if isinstance(value, str) else value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def generate_keypair(algorithm: str = "ed25519") -> Tuple[bytes, bytes]:
|
|
29
|
+
"""Generate a private/public key pair for the requested algorithm."""
|
|
30
|
+
_require_crypto()
|
|
31
|
+
if algorithm != "ed25519":
|
|
32
|
+
raise AlgorithmError("Only ed25519 is currently supported")
|
|
33
|
+
|
|
34
|
+
private = _ed25519.Ed25519PrivateKey.generate()
|
|
35
|
+
public = private.public_key()
|
|
36
|
+
return (
|
|
37
|
+
private.private_bytes(
|
|
38
|
+
encoding=serialization.Encoding.Raw,
|
|
39
|
+
format=serialization.PrivateFormat.Raw,
|
|
40
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
41
|
+
),
|
|
42
|
+
public.public_bytes(
|
|
43
|
+
encoding=serialization.Encoding.Raw,
|
|
44
|
+
format=serialization.PublicFormat.Raw,
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ed25519(message: Union[str, bytes], private_key: bytes) -> bytes:
|
|
50
|
+
"""Sign ``message`` with an Ed25519 private key."""
|
|
51
|
+
_require_crypto()
|
|
52
|
+
if len(private_key) != 32:
|
|
53
|
+
raise InvalidKeyError("Ed25519 private key must be 32 bytes")
|
|
54
|
+
|
|
55
|
+
sk = _ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
|
|
56
|
+
return sk.sign(_to_bytes(message))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def verify(
|
|
60
|
+
signature: bytes,
|
|
61
|
+
message: Union[str, bytes],
|
|
62
|
+
public_key: bytes,
|
|
63
|
+
algorithm: str = "ed25519",
|
|
64
|
+
) -> bool:
|
|
65
|
+
"""Verify a signature against a message and public key."""
|
|
66
|
+
_require_crypto()
|
|
67
|
+
if algorithm != "ed25519":
|
|
68
|
+
raise AlgorithmError("Only ed25519 is currently supported")
|
|
69
|
+
if len(public_key) != 32:
|
|
70
|
+
raise InvalidKeyError("Ed25519 public key must be 32 bytes")
|
|
71
|
+
|
|
72
|
+
vk = _ed25519.Ed25519PublicKey.from_public_bytes(public_key)
|
|
73
|
+
try:
|
|
74
|
+
vk.verify(signature, _to_bytes(message))
|
|
75
|
+
return True
|
|
76
|
+
except InvalidSignature:
|
|
77
|
+
return False
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
raise SignatureError("Signature verification failed") from exc
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crypto-toolkit-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Modern Cryptography & Hashing Toolkit - a misuse-resistant, high-level cryptography library for Python
|
|
5
|
+
Author-email: Parthiv Rawat <parthiv05022000@gmail.com>
|
|
6
|
+
Maintainer-email: Parthiv Rawat <parthiv05022000@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/parthivrawat/crypto-toolkit-py
|
|
9
|
+
Project-URL: Documentation, https://github.com/parthivrawat/crypto-toolkit-py/tree/main/python#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/parthivrawat/crypto-toolkit-py
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/parthivrawat/crypto-toolkit-py/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/parthivrawat/crypto-toolkit-py/blob/main/python/CHANGELOG.md
|
|
13
|
+
Keywords: crypto,cryptography,hash,hashing,hmac,password,encryption,argon2,ed25519,security
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Security
|
|
25
|
+
Classifier: Topic :: Security :: Cryptography
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: crypto
|
|
32
|
+
Requires-Dist: cryptography>=43.0.0; extra == "crypto"
|
|
33
|
+
Provides-Extra: argon2
|
|
34
|
+
Requires-Dist: argon2-cffi>=23.1.0; extra == "argon2"
|
|
35
|
+
Provides-Extra: bcrypt
|
|
36
|
+
Requires-Dist: bcrypt>=4.0.0; extra == "bcrypt"
|
|
37
|
+
Provides-Extra: full
|
|
38
|
+
Requires-Dist: cryptography>=43.0.0; extra == "full"
|
|
39
|
+
Requires-Dist: argon2-cffi>=23.1.0; extra == "full"
|
|
40
|
+
Requires-Dist: bcrypt>=4.0.0; extra == "full"
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# Modern Cryptography & Hashing Toolkit (Python)
|
|
49
|
+
|
|
50
|
+
A misuse-resistant, high-level cryptography library with safe defaults, constant-time verification, and a clear API for hashing, password hashing, key derivation, symmetric encryption, HMAC, and digital signatures.
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Safe hashing**: SHA-256, SHA-384, SHA-512, SHA-3, BLAKE2 (MD5 and SHA-1 rejected)
|
|
55
|
+
- **HMAC**: Constant-time verification
|
|
56
|
+
- **Password hashing**: PBKDF2-SHA256, scrypt, Argon2id, bcrypt with adaptive costs
|
|
57
|
+
- **Key derivation**: PBKDF2 and scrypt
|
|
58
|
+
- **Symmetric encryption**: AES-256-GCM or ChaCha20-Poly1305 with automatic nonce management
|
|
59
|
+
- **Digital signatures**: Ed25519
|
|
60
|
+
- **Zero runtime dependencies** for core hashing, HMAC, and key derivation
|
|
61
|
+
- **Optional extras** for AEAD encryption and signatures (`[crypto]`), Argon2id (`[argon2]`), and bcrypt (`[bcrypt]`)
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install crypto-toolkit-py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
With all optional dependencies:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install "crypto-toolkit-py[full]"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from crypto_toolkit import hash, password, encrypt, sign
|
|
79
|
+
|
|
80
|
+
# Safe, deterministic hashing
|
|
81
|
+
sha256 = hash.string('hello world', algorithm='sha-256')
|
|
82
|
+
print(sha256)
|
|
83
|
+
|
|
84
|
+
mac = hash.hmac('secret-key', 'message', algorithm='sha-256')
|
|
85
|
+
assert hash.verify_hmac(mac, 'secret-key', 'message', algorithm='sha-256')
|
|
86
|
+
|
|
87
|
+
# Password hashing
|
|
88
|
+
ph = password.hash('user-password')
|
|
89
|
+
assert password.verify('user-password', ph)
|
|
90
|
+
|
|
91
|
+
# Key derivation
|
|
92
|
+
salt = b'16-or-more-random-bytes'
|
|
93
|
+
key = password.derive('passphrase', salt=salt, length=32)
|
|
94
|
+
|
|
95
|
+
# Symmetric encryption (requires cryptography)
|
|
96
|
+
ciphertext = encrypt.symmetric('sensitive data', key=key)
|
|
97
|
+
plaintext = encrypt.decrypt(ciphertext, key=key)
|
|
98
|
+
assert plaintext == 'sensitive data'
|
|
99
|
+
|
|
100
|
+
# Digital signatures (requires cryptography)
|
|
101
|
+
private_key, public_key = sign.generate_keypair()
|
|
102
|
+
signature = sign.ed25519('message', private_key=private_key)
|
|
103
|
+
assert sign.verify(signature, 'message', public_key=public_key)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cd implementations/security/crypto-toolkit/python
|
|
110
|
+
pip install -e ".[dev,full]"
|
|
111
|
+
pytest test_crypto_toolkit.py -v
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT License
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
2
|
+
LICENSE
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
setup.py
|
|
7
|
+
crypto_toolkit/__init__.py
|
|
8
|
+
crypto_toolkit/encrypt.py
|
|
9
|
+
crypto_toolkit/exceptions.py
|
|
10
|
+
crypto_toolkit/hash.py
|
|
11
|
+
crypto_toolkit/password.py
|
|
12
|
+
crypto_toolkit/py.typed
|
|
13
|
+
crypto_toolkit/sign.py
|
|
14
|
+
crypto_toolkit_py.egg-info/PKG-INFO
|
|
15
|
+
crypto_toolkit_py.egg-info/SOURCES.txt
|
|
16
|
+
crypto_toolkit_py.egg-info/dependency_links.txt
|
|
17
|
+
crypto_toolkit_py.egg-info/requires.txt
|
|
18
|
+
crypto_toolkit_py.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
[argon2]
|
|
3
|
+
argon2-cffi>=23.1.0
|
|
4
|
+
|
|
5
|
+
[bcrypt]
|
|
6
|
+
bcrypt>=4.0.0
|
|
7
|
+
|
|
8
|
+
[crypto]
|
|
9
|
+
cryptography>=43.0.0
|
|
10
|
+
|
|
11
|
+
[dev]
|
|
12
|
+
pytest>=7.0.0
|
|
13
|
+
pytest-cov>=4.0.0
|
|
14
|
+
mypy>=1.0.0
|
|
15
|
+
ruff>=0.1.0
|
|
16
|
+
|
|
17
|
+
[full]
|
|
18
|
+
cryptography>=43.0.0
|
|
19
|
+
argon2-cffi>=23.1.0
|
|
20
|
+
bcrypt>=4.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crypto_toolkit
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "crypto-toolkit-py"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Modern Cryptography & Hashing Toolkit - a misuse-resistant, high-level cryptography library for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Parthiv Rawat", email = "parthiv05022000@gmail.com"}
|
|
12
|
+
]
|
|
13
|
+
maintainers = [
|
|
14
|
+
{name = "Parthiv Rawat", email = "parthiv05022000@gmail.com"}
|
|
15
|
+
]
|
|
16
|
+
license = "MIT"
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Security",
|
|
29
|
+
"Topic :: Security :: Cryptography",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
keywords = [
|
|
34
|
+
"crypto",
|
|
35
|
+
"cryptography",
|
|
36
|
+
"hash",
|
|
37
|
+
"hashing",
|
|
38
|
+
"hmac",
|
|
39
|
+
"password",
|
|
40
|
+
"encryption",
|
|
41
|
+
"argon2",
|
|
42
|
+
"ed25519",
|
|
43
|
+
"security",
|
|
44
|
+
]
|
|
45
|
+
requires-python = ">=3.8"
|
|
46
|
+
dependencies = []
|
|
47
|
+
|
|
48
|
+
[project.optional-dependencies]
|
|
49
|
+
crypto = ["cryptography>=43.0.0"]
|
|
50
|
+
argon2 = ["argon2-cffi>=23.1.0"]
|
|
51
|
+
bcrypt = ["bcrypt>=4.0.0"]
|
|
52
|
+
full = ["cryptography>=43.0.0", "argon2-cffi>=23.1.0", "bcrypt>=4.0.0"]
|
|
53
|
+
dev = [
|
|
54
|
+
"pytest>=7.0.0",
|
|
55
|
+
"pytest-cov>=4.0.0",
|
|
56
|
+
"mypy>=1.0.0",
|
|
57
|
+
"ruff>=0.1.0",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[project.urls]
|
|
61
|
+
Homepage = "https://github.com/parthivrawat/crypto-toolkit-py"
|
|
62
|
+
Documentation = "https://github.com/parthivrawat/crypto-toolkit-py/tree/main/python#readme"
|
|
63
|
+
Repository = "https://github.com/parthivrawat/crypto-toolkit-py"
|
|
64
|
+
"Bug Tracker" = "https://github.com/parthivrawat/crypto-toolkit-py/issues"
|
|
65
|
+
Changelog = "https://github.com/parthivrawat/crypto-toolkit-py/blob/main/python/CHANGELOG.md"
|
|
66
|
+
|
|
67
|
+
[tool.setuptools]
|
|
68
|
+
packages = ["crypto_toolkit"]
|
|
69
|
+
include-package-data = true
|
|
70
|
+
|
|
71
|
+
[tool.setuptools.package-data]
|
|
72
|
+
crypto_toolkit = ["py.typed"]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["test_crypto_toolkit.py"]
|
|
76
|
+
python_files = ["test_*.py"]
|
|
77
|
+
python_classes = ["Test*"]
|
|
78
|
+
python_functions = ["test_*"]
|
|
79
|
+
addopts = "-v"
|
|
80
|
+
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
python_version = "3.8"
|
|
83
|
+
warn_return_any = true
|
|
84
|
+
warn_unused_configs = true
|
|
85
|
+
disallow_untyped_defs = false
|