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.
@@ -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,7 @@
1
+ include README.md
2
+ include LICENSE
3
+ include CHANGELOG.md
4
+ include pyproject.toml
5
+ recursive-include crypto_toolkit *.py py.typed
6
+ recursive-exclude * __pycache__
7
+ recursive-exclude * *.py[co]
@@ -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,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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """
2
+ Setup configuration for crypto-toolkit.
3
+
4
+ This file exists for backward compatibility with older build tools.
5
+ Modern installations should use pyproject.toml.
6
+ """
7
+
8
+ from setuptools import setup
9
+
10
+ setup()