jpassende 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.
jpassende-1.0.0/NOTICE ADDED
@@ -0,0 +1,2 @@
1
+ jpassende
2
+ Copyright 2026 J Code(Mohammadjavad Maleki Kaveh)
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.4
2
+ Name: jpassende
3
+ Version: 1.0.0
4
+ Summary: High-Performance Cryptographic Library with Custom Patterns
5
+ Author: J Code
6
+ Project-URL: Homepage, https://github.com/JCode-JCode/jpassende
7
+ Project-URL: Repository, https://github.com/JCode-JCode/jpassende
8
+ Keywords: cryptography,encryption,security,aead,stream-cipher,block-cipher,key-derivation,high-performance
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: NOTICE
22
+ Requires-Dist: pycryptodome>=3.18.0
23
+ Dynamic: license-file
24
+
25
+ [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/)
26
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
27
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
28
+ [![PyPI version](https://img.shields.io/pypi/v/jpassende)](https://pypi.org/project/jpassende/)
29
+ [![PyPI project](https://img.shields.io/badge/PyPI-jpassende-blue)](https://pypi.org/project/jpassende/)
30
+
31
+ <br>
32
+
33
+ <img src="docs/images/jpassende-logo.png" alt="jpassende">
34
+
35
+ <br>
36
+
37
+ **jpassende** is a high‑performance, multi‑pattern cryptographic library for Python that goes far beyond standard encryption. It offers 14 unique patterns spanning AEAD, stream ciphers, block ciphers, and key derivation – all wrapped in a simple, consistent API. Every pattern uses its own distinct combination of algorithms and constructions, making your ciphertext immediately recognisable and self‑describing.
38
+
39
+ ---
40
+
41
+ ## Quick Start – Encrypt & Decrypt in Two Lines
42
+
43
+ ```python
44
+ from jpassende import JPassende
45
+
46
+ jp = JPassende()
47
+
48
+ result = jp.encode("Hello, World!", "vail", key="my_secret")
49
+ print(result.encoded)
50
+
51
+ original = jp.decode(result.encoded, "vail", key="my_secret")
52
+ print(original.decoded)
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Main Capabilities
58
+
59
+ **· AEAD Patterns** – vail (AES‑GCM), phnx (ChaCha20‑Poly1305), nixl (ChaCha20 + independent HMAC). All three provide authenticated encryption with associated data (AAD) support.
60
+
61
+ **· Stream Patterns** – strx (BLAKE2‑based keystream), rvrs (SHA‑3 feedback mode), lfsr (dual‑state SHA‑3 / BLAKE2 generator). Byte‑by‑byte encryption without padding, ideal for streaming data.
62
+
63
+ **· Block Patterns** – aegs (AES‑CTR + HMAC), cblk (AES‑CBC + HMAC), cfbb (AES‑CFB‑128 + HMAC), ofbb (AES‑OFB + HMAC). Standard block cipher modes, each individually authenticated.
64
+
65
+ **· Derivation Patterns** – hkdf (HMAC‑based Extract‑and‑Expand), scrt (scrypt), pbk2 (PBKDF2‑SHA‑512), blk3 (Merkle‑tree commitment). Password hashing, key material generation, and data integrity commitments.
66
+
67
+ **· Security Layers** – Every pattern supports three selectable security layers: STANDARD (300k PBKDF2 iterations), FORTIFIED (600k), and QUANTUM (1.2M). You control the trade‑off between speed and brute‑force resistance.
68
+
69
+ **· Binary & Text I/O** – output_raw returns bytes instead of base‑encoded strings. input_raw accepts raw bytes directly, so you can encrypt binary files, images, or any byte sequence.
70
+
71
+ **· Cross‑Instance Decryption** – Packages carry all the metadata (magic, version, pattern ID, salt, nonce) needed for decryption. Any JPassende instance anywhere can decrypt, provided it has the same key.
72
+
73
+ **· Self‑Describing Packages** – The binary format includes a magic header, version byte, pattern identifier, and optional AAD. No more guessing which algorithm was used.
74
+
75
+ **· LRU Key Cache** – PBKDF2 derivations are cached (thread‑safe LRU) to avoid redundant work when the same password is reused.
76
+
77
+ **· Invalid Package Detection** – A dedicated InvalidPackageError is raised when the package structure, magic, or version is invalid.
78
+
79
+ **· Zero Plaintext Password Storage** – Cache keys are derived from a BLAKE2b hash of (password + salt + parameters), never from the password itself.
80
+
81
+ ---
82
+
83
+ ## Pattern Status
84
+
85
+ The patterns nixl, strx, rvrs, lfsr, aegs, cblk, cfbb, ofbb, hkdf, scrt, pbk2, and blk3 are custom constructions created exclusively for jpassende. They are currently experimental and under active development – their internal design may evolve as we gather feedback and perform further security analysis. The patterns vail (AES‑256‑GCM) and phnx (ChaCha20‑Poly1305) use standardized, well‑vetted algorithms and are considered stable. If you plan to use the experimental patterns in production, we strongly recommend performing your own security review and staying updated with new releases.
86
+
87
+ ---
88
+
89
+ ## Installation
90
+
91
+ ```bash
92
+ pip install jpassende
93
+ ```
94
+
95
+ jpassende depends only on pycryptodome (≥ 3.18) and Python's standard library.
96
+
97
+ ---
98
+
99
+ ## More Examples
100
+
101
+ Encrypting Binary Data (input_raw / output_raw)
102
+
103
+ ```python
104
+ from jpassende import JPassende
105
+
106
+ jp = JPassende()
107
+ image = open("photo.png", "rb").read()
108
+
109
+ enc_pkg = jp.encode(image, "nixl", key="secret", input_raw=True, output_raw=True)
110
+
111
+ dec_bytes = jp.decode(enc_pkg.encoded, "nixl", key="secret",
112
+ input_raw=True, output_raw=True).decoded
113
+
114
+ with open("photo_decrypted.png", "wb") as f:
115
+ f.write(dec_bytes)
116
+ ```
117
+
118
+ ## Choosing a Security Layer
119
+
120
+ ```python
121
+ from jpassende import JPassende, SecurityLayer
122
+
123
+ jp = JPassende()
124
+
125
+ result = jp.encode("Sensitive data", "phnx", key="strong",
126
+ layer=SecurityLayer.QUANTUM)
127
+ print(result.layer)
128
+ ```
129
+
130
+ ## Using AAD (Additional Authenticated Data)
131
+
132
+ ```python
133
+ aad = b"user-id:12345"
134
+ result = jp.encode("Hello", "vail", key="secret", aad=aad)
135
+ decoded = jp.decode(result.encoded, "vail", key="secret", aad=aad)
136
+ ```
137
+
138
+ ## Key Derivation – HKDF
139
+
140
+ ```python
141
+ derived = jp.encode("master-seed", "hkdf", key="secret", length=32)
142
+ print(derived.encoded[:30] + "...")
143
+
144
+ verified = jp.decode(derived.encoded, "hkdf", key="secret")
145
+ print(verified.decoded[:20] + "...")
146
+ ```
147
+
148
+ ## List All Available Patterns
149
+
150
+ ```python
151
+ from jpassende import JPassende
152
+
153
+ print(JPassende.PATTERNS.keys())
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Error Handling
159
+
160
+ ```python
161
+ from jpassende import JPassende, InvalidPackageError
162
+
163
+ jp = JPassende()
164
+
165
+ try:
166
+ jp.decode("not-valid-data", "vail", key="secret")
167
+ except InvalidPackageError as e:
168
+ print(f"Package error: {e}")
169
+ except ValueError as e:
170
+ print(f"Other error: {e}")
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Issues and Contributions
176
+
177
+ Bug reports and feature requests are welcome via GitHub Issues. Pull requests should maintain the existing code style and include tests where appropriate.
178
+
179
+ ---
180
+
181
+ ## Links
182
+
183
+ **· GitHub repository:**
184
+ https://github.com/JCode-JCode/jpassende
185
+
186
+ **· PyPI page:**
187
+ https://pypi.org/project/jpassende/
188
+
189
+ ---
190
+
191
+ ## License
192
+
193
+ This project is licensed under the Apache License 2.0 – see the LICENSE file for details.
194
+
195
+ ---
196
+
197
+ Designed and built with love by **J Code**
@@ -0,0 +1,173 @@
1
+ [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/)
2
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
3
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
4
+ [![PyPI version](https://img.shields.io/pypi/v/jpassende)](https://pypi.org/project/jpassende/)
5
+ [![PyPI project](https://img.shields.io/badge/PyPI-jpassende-blue)](https://pypi.org/project/jpassende/)
6
+
7
+ <br>
8
+
9
+ <img src="docs/images/jpassende-logo.png" alt="jpassende">
10
+
11
+ <br>
12
+
13
+ **jpassende** is a high‑performance, multi‑pattern cryptographic library for Python that goes far beyond standard encryption. It offers 14 unique patterns spanning AEAD, stream ciphers, block ciphers, and key derivation – all wrapped in a simple, consistent API. Every pattern uses its own distinct combination of algorithms and constructions, making your ciphertext immediately recognisable and self‑describing.
14
+
15
+ ---
16
+
17
+ ## Quick Start – Encrypt & Decrypt in Two Lines
18
+
19
+ ```python
20
+ from jpassende import JPassende
21
+
22
+ jp = JPassende()
23
+
24
+ result = jp.encode("Hello, World!", "vail", key="my_secret")
25
+ print(result.encoded)
26
+
27
+ original = jp.decode(result.encoded, "vail", key="my_secret")
28
+ print(original.decoded)
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Main Capabilities
34
+
35
+ **· AEAD Patterns** – vail (AES‑GCM), phnx (ChaCha20‑Poly1305), nixl (ChaCha20 + independent HMAC). All three provide authenticated encryption with associated data (AAD) support.
36
+
37
+ **· Stream Patterns** – strx (BLAKE2‑based keystream), rvrs (SHA‑3 feedback mode), lfsr (dual‑state SHA‑3 / BLAKE2 generator). Byte‑by‑byte encryption without padding, ideal for streaming data.
38
+
39
+ **· Block Patterns** – aegs (AES‑CTR + HMAC), cblk (AES‑CBC + HMAC), cfbb (AES‑CFB‑128 + HMAC), ofbb (AES‑OFB + HMAC). Standard block cipher modes, each individually authenticated.
40
+
41
+ **· Derivation Patterns** – hkdf (HMAC‑based Extract‑and‑Expand), scrt (scrypt), pbk2 (PBKDF2‑SHA‑512), blk3 (Merkle‑tree commitment). Password hashing, key material generation, and data integrity commitments.
42
+
43
+ **· Security Layers** – Every pattern supports three selectable security layers: STANDARD (300k PBKDF2 iterations), FORTIFIED (600k), and QUANTUM (1.2M). You control the trade‑off between speed and brute‑force resistance.
44
+
45
+ **· Binary & Text I/O** – output_raw returns bytes instead of base‑encoded strings. input_raw accepts raw bytes directly, so you can encrypt binary files, images, or any byte sequence.
46
+
47
+ **· Cross‑Instance Decryption** – Packages carry all the metadata (magic, version, pattern ID, salt, nonce) needed for decryption. Any JPassende instance anywhere can decrypt, provided it has the same key.
48
+
49
+ **· Self‑Describing Packages** – The binary format includes a magic header, version byte, pattern identifier, and optional AAD. No more guessing which algorithm was used.
50
+
51
+ **· LRU Key Cache** – PBKDF2 derivations are cached (thread‑safe LRU) to avoid redundant work when the same password is reused.
52
+
53
+ **· Invalid Package Detection** – A dedicated InvalidPackageError is raised when the package structure, magic, or version is invalid.
54
+
55
+ **· Zero Plaintext Password Storage** – Cache keys are derived from a BLAKE2b hash of (password + salt + parameters), never from the password itself.
56
+
57
+ ---
58
+
59
+ ## Pattern Status
60
+
61
+ The patterns nixl, strx, rvrs, lfsr, aegs, cblk, cfbb, ofbb, hkdf, scrt, pbk2, and blk3 are custom constructions created exclusively for jpassende. They are currently experimental and under active development – their internal design may evolve as we gather feedback and perform further security analysis. The patterns vail (AES‑256‑GCM) and phnx (ChaCha20‑Poly1305) use standardized, well‑vetted algorithms and are considered stable. If you plan to use the experimental patterns in production, we strongly recommend performing your own security review and staying updated with new releases.
62
+
63
+ ---
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install jpassende
69
+ ```
70
+
71
+ jpassende depends only on pycryptodome (≥ 3.18) and Python's standard library.
72
+
73
+ ---
74
+
75
+ ## More Examples
76
+
77
+ Encrypting Binary Data (input_raw / output_raw)
78
+
79
+ ```python
80
+ from jpassende import JPassende
81
+
82
+ jp = JPassende()
83
+ image = open("photo.png", "rb").read()
84
+
85
+ enc_pkg = jp.encode(image, "nixl", key="secret", input_raw=True, output_raw=True)
86
+
87
+ dec_bytes = jp.decode(enc_pkg.encoded, "nixl", key="secret",
88
+ input_raw=True, output_raw=True).decoded
89
+
90
+ with open("photo_decrypted.png", "wb") as f:
91
+ f.write(dec_bytes)
92
+ ```
93
+
94
+ ## Choosing a Security Layer
95
+
96
+ ```python
97
+ from jpassende import JPassende, SecurityLayer
98
+
99
+ jp = JPassende()
100
+
101
+ result = jp.encode("Sensitive data", "phnx", key="strong",
102
+ layer=SecurityLayer.QUANTUM)
103
+ print(result.layer)
104
+ ```
105
+
106
+ ## Using AAD (Additional Authenticated Data)
107
+
108
+ ```python
109
+ aad = b"user-id:12345"
110
+ result = jp.encode("Hello", "vail", key="secret", aad=aad)
111
+ decoded = jp.decode(result.encoded, "vail", key="secret", aad=aad)
112
+ ```
113
+
114
+ ## Key Derivation – HKDF
115
+
116
+ ```python
117
+ derived = jp.encode("master-seed", "hkdf", key="secret", length=32)
118
+ print(derived.encoded[:30] + "...")
119
+
120
+ verified = jp.decode(derived.encoded, "hkdf", key="secret")
121
+ print(verified.decoded[:20] + "...")
122
+ ```
123
+
124
+ ## List All Available Patterns
125
+
126
+ ```python
127
+ from jpassende import JPassende
128
+
129
+ print(JPassende.PATTERNS.keys())
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Error Handling
135
+
136
+ ```python
137
+ from jpassende import JPassende, InvalidPackageError
138
+
139
+ jp = JPassende()
140
+
141
+ try:
142
+ jp.decode("not-valid-data", "vail", key="secret")
143
+ except InvalidPackageError as e:
144
+ print(f"Package error: {e}")
145
+ except ValueError as e:
146
+ print(f"Other error: {e}")
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Issues and Contributions
152
+
153
+ Bug reports and feature requests are welcome via GitHub Issues. Pull requests should maintain the existing code style and include tests where appropriate.
154
+
155
+ ---
156
+
157
+ ## Links
158
+
159
+ **· GitHub repository:**
160
+ https://github.com/JCode-JCode/jpassende
161
+
162
+ **· PyPI page:**
163
+ https://pypi.org/project/jpassende/
164
+
165
+ ---
166
+
167
+ ## License
168
+
169
+ This project is licensed under the Apache License 2.0 – see the LICENSE file for details.
170
+
171
+ ---
172
+
173
+ Designed and built with love by **J Code**
@@ -0,0 +1,17 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from ._version import __version__
4
+ from .enums import SecurityLayer, PatternCategory
5
+ from .exceptions import InvalidPackageError
6
+ from .datatypes import CryptoResult, DecodeResult
7
+ from .core import JPassende
8
+
9
+ __all__ = [
10
+ "__version__",
11
+ "SecurityLayer",
12
+ "PatternCategory",
13
+ "InvalidPackageError",
14
+ "CryptoResult",
15
+ "DecodeResult",
16
+ "JPassende",
17
+ ]
@@ -0,0 +1,3 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ __version__ = "1.0.0"
@@ -0,0 +1,274 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import hashlib
4
+ import base64
5
+ import hmac
6
+ import struct
7
+ import time
8
+ import logging
9
+ import threading
10
+ from collections import OrderedDict
11
+ from typing import Union, Optional, List, Tuple
12
+
13
+ from Crypto.Protocol.KDF import PBKDF2, HKDF
14
+ from Crypto.Hash import SHA512, SHA256
15
+
16
+ from ._version import __version__
17
+ from .enums import SecurityLayer, PatternCategory
18
+ from .exceptions import InvalidPackageError
19
+ from .datatypes import CryptoResult, DecodeResult
20
+ from . import utils
21
+
22
+ from .mixins.aead import AeadMixin
23
+ from .mixins.stream import StreamMixin
24
+ from .mixins.block import BlockMixin
25
+ from .mixins.derivation import DerivationMixin
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class JPassende(AeadMixin, StreamMixin, BlockMixin, DerivationMixin):
31
+ PATTERNS = {
32
+ 'vail': {'category': PatternCategory.AEAD},
33
+ 'phnx': {'category': PatternCategory.AEAD},
34
+ 'nixl': {'category': PatternCategory.AEAD},
35
+ 'strx': {'category': PatternCategory.STREAM},
36
+ 'rvrs': {'category': PatternCategory.STREAM},
37
+ 'lfsr': {'category': PatternCategory.STREAM},
38
+ 'aegs': {'category': PatternCategory.BLOCK},
39
+ 'cblk': {'category': PatternCategory.BLOCK},
40
+ 'cfbb': {'category': PatternCategory.BLOCK},
41
+ 'ofbb': {'category': PatternCategory.BLOCK},
42
+ 'hkdf': {'category': PatternCategory.DERIVATION},
43
+ 'scrt': {'category': PatternCategory.DERIVATION},
44
+ 'pbk2': {'category': PatternCategory.DERIVATION},
45
+ 'blk3': {'category': PatternCategory.DERIVATION},
46
+ }
47
+ PATTERN_IDS = {
48
+ 'vail': 0, 'phnx': 1, 'nixl': 2, 'strx': 3,
49
+ 'rvrs': 4, 'lfsr': 5, 'aegs': 6, 'cblk': 7,
50
+ 'cfbb': 8, 'ofbb': 9, 'hkdf': 10, 'scrt': 11,
51
+ 'pbk2': 12, 'blk3': 13
52
+ }
53
+ PATTERN_INDEX = PATTERN_IDS
54
+
55
+ def __init__(self, enable_logging: bool = False):
56
+ if enable_logging:
57
+ if not logger.handlers:
58
+ handler = logging.StreamHandler()
59
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
60
+ handler.setFormatter(formatter)
61
+ logger.addHandler(handler)
62
+ logger.setLevel(logging.DEBUG)
63
+ else:
64
+ logger.setLevel(logging.ERROR)
65
+
66
+ self._salt_size = 32
67
+ self._default_nonce = 12
68
+ self._mac_size = 32
69
+ self._MAGIC = b'jpas'
70
+ self._VERSION = 1
71
+
72
+ self._derive_cache: OrderedDict = OrderedDict()
73
+ self._cache_lock = threading.Lock()
74
+ self._max_cache_entries = 128
75
+
76
+ self._encryptors = {
77
+ 'vail': self.vail, 'phnx': self.phnx, 'nixl': self.nixl,
78
+ 'strx': self.strx, 'rvrs': self.rvrs, 'lfsr': self.lfsr,
79
+ 'aegs': self.aegs, 'cblk': self.cblk, 'cfbb': self.cfbb, 'ofbb': self.ofbb,
80
+ 'hkdf': self.hkdf, 'scrt': self.scrt, 'pbk2': self.pbk2, 'blk3': self.blk3,
81
+ }
82
+ self._decryptors = {
83
+ 'vail': self.dvail, 'phnx': self.dphnx, 'nixl': self.dnixl,
84
+ 'strx': self.dstrx, 'rvrs': self.drvrs, 'lfsr': self.dlfsr,
85
+ 'aegs': self.daegs, 'cblk': self.dcblk, 'cfbb': self.dcfbb, 'ofbb': self.dofbb,
86
+ 'hkdf': self.dhkdf, 'scrt': self.dscrt, 'pbk2': self.dpbk2, 'blk3': self.dblk3,
87
+ }
88
+
89
+ @staticmethod
90
+ def _to_bytes(data: Union[str, bytes]) -> bytes:
91
+ return utils.to_bytes(data)
92
+
93
+ @staticmethod
94
+ def _to_str(data: bytes) -> str:
95
+ return utils.to_str(data)
96
+
97
+ @staticmethod
98
+ def _secure_bytes(size: int = 32) -> bytes:
99
+ return utils.secure_bytes(size)
100
+
101
+ def _validate_nonempty(self, data: Union[str, bytes], name: str = "data"):
102
+ utils.validate_nonempty(data, name)
103
+
104
+ @staticmethod
105
+ def _validate_key(key: str, pattern: str = ""):
106
+ utils.validate_key(key, pattern)
107
+
108
+ @staticmethod
109
+ def _xor_bytes(a: bytes, b: bytes) -> bytes:
110
+ return utils.xor_bytes(a, b)
111
+
112
+ def _mac(self, key: bytes, data: bytes) -> bytes:
113
+ return utils.mac(key, data)
114
+
115
+ def _derive_key(self, password: str, salt: bytes, length: int = 32,
116
+ layer: SecurityLayer = SecurityLayer.STANDARD) -> bytes:
117
+ hash_input = password.encode() + salt + struct.pack('>IB', length, layer.value)
118
+ cache_key = hashlib.blake2b(hash_input, digest_size=16).digest()
119
+ with self._cache_lock:
120
+ if cache_key in self._derive_cache:
121
+ self._derive_cache.move_to_end(cache_key)
122
+ logger.debug("Cache hit for key derivation")
123
+ return self._derive_cache[cache_key]
124
+ iterations = {
125
+ SecurityLayer.STANDARD: 300_000,
126
+ SecurityLayer.FORTIFIED: 600_000,
127
+ SecurityLayer.QUANTUM: 1_200_000
128
+ }
129
+ logger.debug("Deriving key with PBKDF2 (iterations=%d)", iterations[layer])
130
+ derived = PBKDF2(password.encode(), salt, dkLen=length,
131
+ count=iterations[layer], hmac_hash_module=SHA512)
132
+ with self._cache_lock:
133
+ if len(self._derive_cache) >= self._max_cache_entries:
134
+ self._derive_cache.popitem(last=False)
135
+ self._derive_cache[cache_key] = derived
136
+ return derived
137
+
138
+ def _derive_keys(self, key: str, salt: bytes, layer: SecurityLayer) -> Tuple[bytes, bytes]:
139
+ base_key = self._derive_key(key, salt, 32, layer)
140
+ material = HKDF(base_key, 64, salt, SHA256, context=b'jpassende:keys:v2')
141
+ return material[:32], material[32:]
142
+
143
+ def _nonce_size_for(self, pattern: str) -> int:
144
+ """Return expected nonce size for a given pattern (bytes)."""
145
+ if pattern in ('vail', 'phnx', 'nixl'):
146
+ return 12
147
+ if pattern in ('aegs', 'cblk', 'cfbb', 'ofbb'):
148
+ return 16
149
+ if pattern in ('strx', 'rvrs', 'lfsr'):
150
+ return 16
151
+ if pattern in ('hkdf', 'scrt', 'pbk2', 'blk3'):
152
+ return 0
153
+ return self._default_nonce
154
+
155
+ # ---- pack/unpack helpers ----
156
+ def _pack(self, pattern: str, aad: Optional[bytes], salt: bytes,
157
+ nonce: bytes, ciphertext: bytes, mac_key: Optional[bytes] = None) -> bytes:
158
+ pattern_id = self.PATTERN_INDEX[pattern]
159
+ flags = 0x01 if aad else 0
160
+ header = self._MAGIC + bytes([self._VERSION, pattern_id, flags])
161
+ payload = header
162
+ if aad:
163
+ aad_block = struct.pack('>I', len(aad)) + aad
164
+ payload += aad_block
165
+ body = salt + nonce + ciphertext
166
+ payload += body
167
+ if mac_key:
168
+ payload += self._mac(mac_key, payload)
169
+ return payload
170
+
171
+ def _unpack(self, encoded: bytes, pattern: str) -> Tuple[Optional[bytes], bytes, bytes, bytes, Optional[bytes]]:
172
+ if len(encoded) < 7:
173
+ raise InvalidPackageError(" Invalid package – too short for header")
174
+ if encoded[:4] != self._MAGIC:
175
+ raise InvalidPackageError(" Invalid magic bytes")
176
+ if encoded[4] != self._VERSION:
177
+ raise InvalidPackageError(f" Unsupported version: {encoded[4]}")
178
+ if encoded[5] != self.PATTERN_INDEX[pattern]:
179
+ raise InvalidPackageError(" Pattern mismatch")
180
+ flags = encoded[6]
181
+ pos = 7
182
+ aad = None
183
+ if flags & 0x01:
184
+ if len(encoded) < pos + 4:
185
+ raise InvalidPackageError(" Invalid package – missing AAD length")
186
+ aad_len = struct.unpack('>I', encoded[pos:pos + 4])[0]
187
+ pos += 4
188
+ if len(encoded) < pos + aad_len:
189
+ raise InvalidPackageError(" Invalid package – truncated AAD")
190
+ aad = encoded[pos:pos + aad_len]
191
+ pos += aad_len
192
+ salt_size = self._salt_size
193
+ nonce_size = self._nonce_size_for(pattern)
194
+ mac_size = 0 if pattern in ('vail', 'phnx') else self._mac_size
195
+ total_body = len(encoded) - pos
196
+ if total_body < salt_size + nonce_size + mac_size:
197
+ raise InvalidPackageError(" Invalid package – body too short")
198
+ salt = encoded[pos:pos + salt_size]
199
+ pos += salt_size
200
+ nonce = encoded[pos:pos + nonce_size] if nonce_size > 0 else b''
201
+ pos += nonce_size
202
+ ciphertext_len = total_body - salt_size - nonce_size - mac_size
203
+ ciphertext = encoded[pos:pos + ciphertext_len]
204
+ pos += ciphertext_len
205
+ mac_val = encoded[pos:pos + mac_size] if mac_size > 0 else None
206
+ pos += mac_size if mac_size > 0 else 0
207
+
208
+ if pos != len(encoded):
209
+ raise InvalidPackageError(" Invalid package – trailing data after payload")
210
+ return aad, salt, nonce, ciphertext, mac_val
211
+
212
+ def _pack_derivation(self, pattern: str, aad: Optional[bytes],
213
+ salt: bytes, derived_data: bytes, verification: bytes) -> bytes:
214
+ pattern_id = self.PATTERN_INDEX[pattern]
215
+ flags = 0x01 if aad else 0
216
+ header = self._MAGIC + bytes([self._VERSION, pattern_id, flags])
217
+ aad_block = struct.pack('>I', len(aad)) + aad if aad else b''
218
+ return header + aad_block + salt + derived_data + verification
219
+
220
+ def _unpack_derivation(self, package: bytes, pattern: str) -> Tuple[Optional[bytes], bytes, bytes, bytes]:
221
+ if package[:4] != self._MAGIC or package[4] != self._VERSION or package[5] != self.PATTERN_INDEX[pattern]:
222
+ raise InvalidPackageError(" Header mismatch")
223
+ flags = package[6]
224
+ pos = 7
225
+ aad = None
226
+ if flags & 0x01:
227
+ if len(package) < pos + 4:
228
+ raise InvalidPackageError(" Invalid package – missing AAD length")
229
+ aad_len = struct.unpack('>I', package[pos:pos + 4])[0]
230
+ pos += 4
231
+ if len(package) < pos + aad_len:
232
+ raise InvalidPackageError(" Invalid package – truncated AAD")
233
+ aad = package[pos:pos + aad_len]
234
+ pos += aad_len
235
+ salt = package[pos:pos + self._salt_size]
236
+ pos += self._salt_size
237
+ derived = package[pos:-16]
238
+ verification = package[-16:]
239
+ if pos + len(derived) + 16 != len(package):
240
+ raise InvalidPackageError(" Invalid derivation package – trailing data")
241
+ return aad, salt, derived, verification
242
+
243
+ def encode(self, data: Union[str, bytes], pattern: str, key: str = "",
244
+ layer: SecurityLayer = SecurityLayer.STANDARD,
245
+ aad: Optional[bytes] = None,
246
+ output_raw: bool = False, input_raw: bool = False,
247
+ **kwargs) -> CryptoResult:
248
+ if pattern not in self.PATTERNS:
249
+ raise ValueError(f" Unknown pattern: {pattern}")
250
+ encryptor = self._encryptors[pattern]
251
+ t0 = time.perf_counter()
252
+ encoded = encryptor(data, key, aad=aad, layer=layer,
253
+ output_raw=output_raw, input_raw=input_raw, **kwargs)
254
+ elapsed = time.perf_counter() - t0
255
+ return CryptoResult(encoded=encoded, pattern=pattern, layer=layer.name,
256
+ needs_key=bool(key), elapsed=elapsed)
257
+
258
+ def decode(self, encoded: Union[str, bytes], pattern: str, key: str = "",
259
+ layer: SecurityLayer = SecurityLayer.STANDARD,
260
+ aad: Optional[bytes] = None,
261
+ output_raw: bool = False, input_raw: bool = False,
262
+ **kwargs) -> DecodeResult:
263
+ if pattern not in self.PATTERNS:
264
+ raise ValueError(f" Unknown pattern: {pattern}")
265
+ decryptor = self._decryptors[pattern]
266
+ t0 = time.perf_counter()
267
+ decoded = decryptor(encoded, key, aad=aad, layer=layer,
268
+ output_raw=output_raw, input_raw=input_raw, **kwargs)
269
+ elapsed = time.perf_counter() - t0
270
+ return DecodeResult(decoded=decoded, pattern=pattern, layer=layer.name,
271
+ verified=True, elapsed=elapsed)
272
+
273
+ def list_patterns(self) -> List[str]:
274
+ return list(self.PATTERNS.keys())