jpassende 1.0.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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
+ ]
jpassende/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ __version__ = "1.0.0"
jpassende/core.py ADDED
@@ -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())
jpassende/datatypes.py ADDED
@@ -0,0 +1,22 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from dataclasses import dataclass, field
4
+ from typing import Union
5
+ import time
6
+
7
+ @dataclass
8
+ class CryptoResult:
9
+ encoded: Union[str, bytes]
10
+ pattern: str
11
+ layer: str
12
+ needs_key: bool
13
+ timestamp: float = field(default_factory=time.time)
14
+ elapsed: float = 0.0
15
+
16
+ @dataclass
17
+ class DecodeResult:
18
+ decoded: Union[str, bytes]
19
+ pattern: str
20
+ layer: str
21
+ verified: bool
22
+ elapsed: float = 0.0
jpassende/enums.py ADDED
@@ -0,0 +1,14 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from enum import Enum, auto
4
+
5
+ class SecurityLayer(Enum):
6
+ STANDARD = auto()
7
+ FORTIFIED = auto()
8
+ QUANTUM = auto()
9
+
10
+ class PatternCategory(Enum):
11
+ AEAD = auto()
12
+ STREAM = auto()
13
+ BLOCK = auto()
14
+ DERIVATION = auto()
@@ -0,0 +1,4 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ class InvalidPackageError(ValueError):
4
+ pass
@@ -0,0 +1,3 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Mixins for jpassende cryptographic patterns
@@ -0,0 +1,141 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import base64
4
+ import logging
5
+ from typing import Union, Optional
6
+ from Crypto.Cipher import AES, ChaCha20, ChaCha20_Poly1305
7
+ import hmac
8
+
9
+ from ..enums import SecurityLayer
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class AeadMixin:
14
+ def vail(self, data: Union[str, bytes], key: str, aad: Optional[bytes] = None,
15
+ layer: SecurityLayer = SecurityLayer.STANDARD,
16
+ output_raw: bool = False, input_raw: bool = False) -> Union[str, bytes]:
17
+ if input_raw:
18
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
19
+ else:
20
+ self._validate_nonempty(data)
21
+ data_bytes = self._to_bytes(data)
22
+ self._validate_key(key, 'vail')
23
+ salt = self._secure_bytes(self._salt_size)
24
+ key_bytes = self._derive_key(key, salt, 32, layer)
25
+ nonce = self._secure_bytes(12)
26
+ cipher = AES.new(key_bytes, AES.MODE_GCM, nonce=nonce)
27
+ if aad:
28
+ cipher.update(aad)
29
+ ct, tag = cipher.encrypt_and_digest(data_bytes)
30
+ ct_tag = ct + tag
31
+ package = self._pack('vail', aad, salt, nonce, ct_tag)
32
+ logger.debug("vail: encrypted %d bytes", len(ct_tag))
33
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
34
+
35
+ def dvail(self, encoded: Union[str, bytes], key: str, aad: Optional[bytes] = None,
36
+ layer: SecurityLayer = SecurityLayer.STANDARD,
37
+ output_raw: bool = False, input_raw: bool = False) -> Union[str, bytes]:
38
+ if input_raw:
39
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
40
+ else:
41
+ if not encoded:
42
+ raise ValueError(" Encoded data cannot be empty.")
43
+ clean = encoded.strip() if isinstance(encoded, str) else encoded
44
+ package = clean if isinstance(clean, bytes) else base64.b85decode(clean.encode('utf-8'))
45
+ self._validate_key(key, 'dvail')
46
+ aad_pkg, salt, nonce, ct_tag, _ = self._unpack(package, 'vail')
47
+ if aad is not None and aad != aad_pkg:
48
+ raise ValueError(" AAD mismatch")
49
+ key_bytes = self._derive_key(key, salt, 32, layer)
50
+ cipher = AES.new(key_bytes, AES.MODE_GCM, nonce=nonce)
51
+ if aad_pkg:
52
+ cipher.update(aad_pkg)
53
+ try:
54
+ plain = cipher.decrypt_and_verify(ct_tag[:-16], ct_tag[-16:])
55
+ except (ValueError, KeyError):
56
+ raise ValueError(" GCM authentication failed")
57
+ logger.debug("dvail: decrypted %d bytes", len(plain))
58
+ return plain if output_raw else self._to_str(plain)
59
+
60
+ def phnx(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
61
+ output_raw=False, input_raw=False):
62
+ if input_raw:
63
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
64
+ else:
65
+ self._validate_nonempty(data)
66
+ data_bytes = self._to_bytes(data)
67
+ self._validate_key(key, 'phnx')
68
+ salt = self._secure_bytes(self._salt_size)
69
+ key_bytes = self._derive_key(key, salt, 32, layer)
70
+ nonce = self._secure_bytes(12)
71
+ cipher = ChaCha20_Poly1305.new(key=key_bytes, nonce=nonce)
72
+ if aad:
73
+ cipher.update(aad)
74
+ ct, tag = cipher.encrypt_and_digest(data_bytes)
75
+ ct_tag = ct + tag
76
+ package = self._pack('phnx', aad, salt, nonce, ct_tag)
77
+ logger.debug("phnx: encrypted %d bytes", len(ct_tag))
78
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
79
+
80
+ def dphnx(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
81
+ output_raw=False, input_raw=False):
82
+ if input_raw:
83
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
84
+ else:
85
+ if not encoded:
86
+ raise ValueError(" Encoded data cannot be empty.")
87
+ clean = encoded.strip() if isinstance(encoded, str) else encoded
88
+ package = clean if isinstance(clean, bytes) else base64.b85decode(clean.encode('utf-8'))
89
+ self._validate_key(key, 'dphnx')
90
+ aad_pkg, salt, nonce, ct_tag, _ = self._unpack(package, 'phnx')
91
+ if aad is not None and aad != aad_pkg:
92
+ raise ValueError(" AAD mismatch")
93
+ key_bytes = self._derive_key(key, salt, 32, layer)
94
+ cipher = ChaCha20_Poly1305.new(key=key_bytes, nonce=nonce)
95
+ if aad_pkg:
96
+ cipher.update(aad_pkg)
97
+ try:
98
+ plain = cipher.decrypt_and_verify(ct_tag[:-16], ct_tag[-16:])
99
+ except (ValueError, KeyError):
100
+ raise ValueError(" Poly1305 authentication failed")
101
+ logger.debug("dphnx: decrypted %d bytes", len(plain))
102
+ return plain if output_raw else self._to_str(plain)
103
+
104
+ def nixl(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
105
+ output_raw=False, input_raw=False):
106
+ if input_raw:
107
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
108
+ else:
109
+ self._validate_nonempty(data)
110
+ data_bytes = self._to_bytes(data)
111
+ self._validate_key(key, 'nixl')
112
+ salt = self._secure_bytes(self._salt_size)
113
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
114
+ nonce = self._secure_bytes(12) # 12 bytes – must match _nonce_size_for
115
+ cipher = ChaCha20.new(key=enc_key, nonce=nonce)
116
+ ct = cipher.encrypt(data_bytes)
117
+ package = self._pack('nixl', aad, salt, nonce, ct, mac_key=mac_key)
118
+ logger.debug("nixl: encrypted %d bytes", len(ct))
119
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
120
+
121
+ def dnixl(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
122
+ output_raw=False, input_raw=False):
123
+ if input_raw:
124
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
125
+ else:
126
+ if not encoded:
127
+ raise ValueError(" Encoded data cannot be empty.")
128
+ clean = encoded.strip() if isinstance(encoded, str) else encoded
129
+ package = clean if isinstance(clean, bytes) else base64.b85decode(clean.encode('utf-8'))
130
+ self._validate_key(key, 'dnixl')
131
+ aad_pkg, salt, nonce, ciphertext, mac_val = self._unpack(package, 'nixl')
132
+ if aad is not None and aad != aad_pkg:
133
+ raise ValueError(" AAD mismatch")
134
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
135
+ to_verify = package[:-len(mac_val)] if mac_val else package
136
+ if mac_val and not hmac.compare_digest(mac_val, self._mac(mac_key, to_verify)):
137
+ raise ValueError(" MAC verification failed")
138
+ cipher = ChaCha20.new(key=enc_key, nonce=nonce)
139
+ plain = cipher.decrypt(ciphertext)
140
+ logger.debug("dnixl: decrypted %d bytes", len(plain))
141
+ return plain if output_raw else self._to_str(plain)
@@ -0,0 +1,171 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import base64
4
+ import logging
5
+ from typing import Union, Optional
6
+ from Crypto.Cipher import AES
7
+ from Crypto.Util.Padding import pad, unpad
8
+ import hmac
9
+
10
+ from ..enums import SecurityLayer
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class BlockMixin:
15
+ def aegs(self, data: Union[str, bytes], key: str, aad: Optional[bytes] = None,
16
+ layer: SecurityLayer = SecurityLayer.STANDARD,
17
+ output_raw: bool = False, input_raw: bool = False) -> Union[str, bytes]:
18
+ if input_raw:
19
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
20
+ else:
21
+ self._validate_nonempty(data)
22
+ data_bytes = self._to_bytes(data)
23
+ self._validate_key(key, 'aegs')
24
+ salt = self._secure_bytes(self._salt_size)
25
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
26
+ iv = self._secure_bytes(16)
27
+ cipher = AES.new(enc_key, AES.MODE_CTR, nonce=b'', initial_value=iv)
28
+ padded = pad(data_bytes, AES.block_size)
29
+ ciphertext = cipher.encrypt(padded)
30
+ package = self._pack('aegs', aad, salt, iv, ciphertext, mac_key=mac_key)
31
+ logger.debug("aegs: encrypted %d bytes", len(ciphertext))
32
+ return package if output_raw else base64.b64encode(package).decode('utf-8')
33
+
34
+ def daegs(self, encoded: Union[str, bytes], key: str, aad: Optional[bytes] = None,
35
+ layer: SecurityLayer = SecurityLayer.STANDARD,
36
+ output_raw: bool = False, input_raw: bool = False) -> Union[str, bytes]:
37
+ if input_raw:
38
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
39
+ else:
40
+ if not encoded:
41
+ raise ValueError(" Encoded data cannot be empty.")
42
+ package = encoded if isinstance(encoded, bytes) else base64.b64decode(encoded.encode('utf-8'))
43
+ self._validate_key(key, 'daegs')
44
+ aad_pkg, salt, iv, ciphertext, mac_val = self._unpack(package, 'aegs')
45
+ if aad is not None and aad != aad_pkg:
46
+ raise ValueError(" AAD mismatch")
47
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
48
+ to_verify = package[:-len(mac_val)] if mac_val else package
49
+ if mac_val and not hmac.compare_digest(mac_val, self._mac(mac_key, to_verify)):
50
+ raise ValueError(" MAC verification failed")
51
+ cipher = AES.new(enc_key, AES.MODE_CTR, nonce=b'', initial_value=iv)
52
+ padded = cipher.decrypt(ciphertext)
53
+ plain = unpad(padded, AES.block_size)
54
+ logger.debug("daegs: decrypted %d bytes", len(plain))
55
+ return plain if output_raw else self._to_str(plain)
56
+
57
+ def cblk(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
58
+ output_raw=False, input_raw=False):
59
+ if input_raw:
60
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
61
+ else:
62
+ self._validate_nonempty(data)
63
+ data_bytes = self._to_bytes(data)
64
+ self._validate_key(key, 'cblk')
65
+ salt = self._secure_bytes(self._salt_size)
66
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
67
+ iv = self._secure_bytes(16)
68
+ cipher = AES.new(enc_key, AES.MODE_CBC, iv=iv)
69
+ padded = pad(data_bytes, AES.block_size)
70
+ ciphertext = cipher.encrypt(padded)
71
+ package = self._pack('cblk', aad, salt, iv, ciphertext, mac_key=mac_key)
72
+ logger.debug("cblk: encrypted %d bytes", len(ciphertext))
73
+ return package if output_raw else base64.b64encode(package).decode('utf-8')
74
+
75
+ def dcblk(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
76
+ output_raw=False, input_raw=False):
77
+ if input_raw:
78
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
79
+ else:
80
+ if not encoded:
81
+ raise ValueError(" Encoded data cannot be empty.")
82
+ package = encoded if isinstance(encoded, bytes) else base64.b64decode(encoded.encode('utf-8'))
83
+ self._validate_key(key, 'dcblk')
84
+ aad_pkg, salt, iv, ciphertext, mac_val = self._unpack(package, 'cblk')
85
+ if aad is not None and aad != aad_pkg:
86
+ raise ValueError(" AAD mismatch")
87
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
88
+ to_verify = package[:-len(mac_val)] if mac_val else package
89
+ if mac_val and not hmac.compare_digest(mac_val, self._mac(mac_key, to_verify)):
90
+ raise ValueError(" MAC verification failed")
91
+ cipher = AES.new(enc_key, AES.MODE_CBC, iv=iv)
92
+ padded = cipher.decrypt(ciphertext)
93
+ plain = unpad(padded, AES.block_size)
94
+ logger.debug("dcblk: decrypted %d bytes", len(plain))
95
+ return plain if output_raw else self._to_str(plain)
96
+
97
+ def cfbb(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
98
+ output_raw=False, input_raw=False):
99
+ if input_raw:
100
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
101
+ else:
102
+ self._validate_nonempty(data)
103
+ data_bytes = self._to_bytes(data)
104
+ self._validate_key(key, 'cfbb')
105
+ salt = self._secure_bytes(self._salt_size)
106
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
107
+ iv = self._secure_bytes(16)
108
+ cipher = AES.new(enc_key, AES.MODE_CFB, iv=iv, segment_size=128)
109
+ ciphertext = cipher.encrypt(data_bytes)
110
+ package = self._pack('cfbb', aad, salt, iv, ciphertext, mac_key=mac_key)
111
+ logger.debug("cfbb: encrypted %d bytes", len(ciphertext))
112
+ return package if output_raw else base64.b64encode(package).decode('utf-8')
113
+
114
+ def dcfbb(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
115
+ output_raw=False, input_raw=False):
116
+ if input_raw:
117
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
118
+ else:
119
+ if not encoded:
120
+ raise ValueError(" Encoded data cannot be empty.")
121
+ package = encoded if isinstance(encoded, bytes) else base64.b64decode(encoded.encode('utf-8'))
122
+ self._validate_key(key, 'dcfbb')
123
+ aad_pkg, salt, iv, ciphertext, mac_val = self._unpack(package, 'cfbb')
124
+ if aad is not None and aad != aad_pkg:
125
+ raise ValueError(" AAD mismatch")
126
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
127
+ to_verify = package[:-len(mac_val)] if mac_val else package
128
+ if mac_val and not hmac.compare_digest(mac_val, self._mac(mac_key, to_verify)):
129
+ raise ValueError(" MAC verification failed")
130
+ cipher = AES.new(enc_key, AES.MODE_CFB, iv=iv, segment_size=128)
131
+ plain = cipher.decrypt(ciphertext)
132
+ logger.debug("dcfbb: decrypted %d bytes", len(plain))
133
+ return plain if output_raw else self._to_str(plain)
134
+
135
+ def ofbb(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
136
+ output_raw=False, input_raw=False):
137
+ if input_raw:
138
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
139
+ else:
140
+ self._validate_nonempty(data)
141
+ data_bytes = self._to_bytes(data)
142
+ self._validate_key(key, 'ofbb')
143
+ salt = self._secure_bytes(self._salt_size)
144
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
145
+ iv = self._secure_bytes(16)
146
+ cipher = AES.new(enc_key, AES.MODE_OFB, iv=iv)
147
+ ciphertext = cipher.encrypt(data_bytes)
148
+ package = self._pack('ofbb', aad, salt, iv, ciphertext, mac_key=mac_key)
149
+ logger.debug("ofbb: encrypted %d bytes", len(ciphertext))
150
+ return package if output_raw else base64.b64encode(package).decode('utf-8')
151
+
152
+ def dofbb(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
153
+ output_raw=False, input_raw=False):
154
+ if input_raw:
155
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
156
+ else:
157
+ if not encoded:
158
+ raise ValueError(" Encoded data cannot be empty.")
159
+ package = encoded if isinstance(encoded, bytes) else base64.b64decode(encoded.encode('utf-8'))
160
+ self._validate_key(key, 'dofbb')
161
+ aad_pkg, salt, iv, ciphertext, mac_val = self._unpack(package, 'ofbb')
162
+ if aad is not None and aad != aad_pkg:
163
+ raise ValueError(" AAD mismatch")
164
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
165
+ to_verify = package[:-len(mac_val)] if mac_val else package
166
+ if mac_val and not hmac.compare_digest(mac_val, self._mac(mac_key, to_verify)):
167
+ raise ValueError(" MAC verification failed")
168
+ cipher = AES.new(enc_key, AES.MODE_OFB, iv=iv)
169
+ plain = cipher.decrypt(ciphertext)
170
+ logger.debug("dofbb: decrypted %d bytes", len(plain))
171
+ return plain if output_raw else self._to_str(plain)
@@ -0,0 +1,169 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import hashlib
4
+ import hmac
5
+ import base64
6
+ import struct
7
+ import logging
8
+ from typing import Union, Optional
9
+ from Crypto.Protocol.KDF import PBKDF2, scrypt, HKDF
10
+ from Crypto.Hash import SHA512, SHA256
11
+ from ..enums import SecurityLayer
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class DerivationMixin:
16
+ def hkdf(self, data: Union[str, bytes], key: str, length: int = 64,
17
+ aad: Optional[bytes] = None, layer: SecurityLayer = SecurityLayer.STANDARD,
18
+ output_raw: bool = False, input_raw: bool = False) -> Union[str, bytes]:
19
+ if input_raw:
20
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
21
+ else:
22
+ self._validate_nonempty(data)
23
+ data_bytes = self._to_bytes(data)
24
+ self._validate_key(key, 'hkdf')
25
+ max_len = 255 * hashlib.sha512().digest_size
26
+ if length > max_len:
27
+ raise ValueError(f" Requested HKDF output length ({length}) exceeds maximum allowed ({max_len})")
28
+ salt = self._secure_bytes(self._salt_size)
29
+ key_bytes = self._derive_key(key, salt, 32, layer)
30
+ derived = HKDF(data_bytes, length, salt, SHA512, context=b'jpassende:hkdf:v1')
31
+ verification = hashlib.blake2b(derived + key_bytes, digest_size=16).digest()
32
+ package = self._pack_derivation('hkdf', aad, salt, derived, verification)
33
+ logger.debug("hkdf: derived %d bytes", len(derived))
34
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
35
+
36
+ def dhkdf(self, encoded, key, length=64, aad=None, layer=SecurityLayer.STANDARD,
37
+ output_raw=False, input_raw=False):
38
+ if input_raw:
39
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
40
+ else:
41
+ if not encoded:
42
+ raise ValueError(" Encoded data cannot be empty.")
43
+ package = encoded if isinstance(encoded, bytes) else base64.b85decode(encoded.encode('utf-8'))
44
+ self._validate_key(key, 'dhkdf')
45
+ aad_pkg, salt, derived, verification = self._unpack_derivation(package, 'hkdf')
46
+ if aad is not None and aad != aad_pkg:
47
+ raise ValueError(" AAD mismatch")
48
+ key_bytes = self._derive_key(key, salt, 32, layer)
49
+ expected = hashlib.blake2b(derived + key_bytes, digest_size=16).digest()
50
+ if not hmac.compare_digest(verification, expected):
51
+ raise ValueError(" Verification failed")
52
+ logger.debug("dhkdf: verified %d bytes", len(derived))
53
+ return derived if output_raw else derived.hex()
54
+
55
+ def scrt(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
56
+ output_raw=False, input_raw=False):
57
+ if input_raw:
58
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
59
+ else:
60
+ self._validate_nonempty(data)
61
+ data_bytes = self._to_bytes(data)
62
+ self._validate_key(key, 'scrt')
63
+ salt = self._secure_bytes(self._salt_size)
64
+ key_bytes = self._derive_key(key, salt, 32, layer)
65
+ n = {SecurityLayer.STANDARD: 2**14, SecurityLayer.FORTIFIED: 2**17, SecurityLayer.QUANTUM: 2**20}[layer]
66
+ derived = scrypt(data_bytes, salt, key_len=64, N=n, r=8, p=1)
67
+ verification = hmac.digest(key_bytes, derived, hashlib.sha3_512)[:16]
68
+ package = self._pack_derivation('scrt', aad, salt, derived, verification)
69
+ logger.debug("scrt: derived %d bytes", len(derived))
70
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
71
+
72
+ def dscrt(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
73
+ output_raw=False, input_raw=False):
74
+ if input_raw:
75
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
76
+ else:
77
+ if not encoded:
78
+ raise ValueError(" Encoded data cannot be empty.")
79
+ package = encoded if isinstance(encoded, bytes) else base64.b85decode(encoded.encode('utf-8'))
80
+ self._validate_key(key, 'dscrt')
81
+ aad_pkg, salt, derived, verification = self._unpack_derivation(package, 'scrt')
82
+ if aad is not None and aad != aad_pkg:
83
+ raise ValueError(" AAD mismatch")
84
+ key_bytes = self._derive_key(key, salt, 32, layer)
85
+ expected = hmac.digest(key_bytes, derived, hashlib.sha3_512)[:16]
86
+ if not hmac.compare_digest(verification, expected):
87
+ raise ValueError(" Verification failed")
88
+ logger.debug("dscrt: verified %d bytes", len(derived))
89
+ return derived if output_raw else derived.hex()
90
+
91
+ def pbk2(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
92
+ output_raw=False, input_raw=False):
93
+ if input_raw:
94
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
95
+ else:
96
+ self._validate_nonempty(data)
97
+ data_bytes = self._to_bytes(data)
98
+ self._validate_key(key, 'pbk2')
99
+ salt = self._secure_bytes(self._salt_size)
100
+ iterations = {SecurityLayer.STANDARD: 300_000, SecurityLayer.FORTIFIED: 600_000,
101
+ SecurityLayer.QUANTUM: 1_200_000}[layer]
102
+ derived = PBKDF2(data_bytes, salt, dkLen=64, count=iterations, hmac_hash_module=SHA512)
103
+ key_bytes = self._derive_key(key, salt, 32, layer)
104
+ verification = hmac.digest(key_bytes, derived, hashlib.blake2s)[:16]
105
+ package = self._pack_derivation('pbk2', aad, salt, derived, verification)
106
+ logger.debug("pbk2: derived %d bytes", len(derived))
107
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
108
+
109
+ def dpbk2(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
110
+ output_raw=False, input_raw=False):
111
+ if input_raw:
112
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
113
+ else:
114
+ if not encoded:
115
+ raise ValueError(" Encoded data cannot be empty.")
116
+ package = encoded if isinstance(encoded, bytes) else base64.b85decode(encoded.encode('utf-8'))
117
+ self._validate_key(key, 'dpbk2')
118
+ aad_pkg, salt, derived, verification = self._unpack_derivation(package, 'pbk2')
119
+ if aad is not None and aad != aad_pkg:
120
+ raise ValueError(" AAD mismatch")
121
+ key_bytes = self._derive_key(key, salt, 32, layer)
122
+ expected = hmac.digest(key_bytes, derived, hashlib.blake2s)[:16]
123
+ if not hmac.compare_digest(verification, expected):
124
+ raise ValueError(" Verification failed")
125
+ logger.debug("dpbk2: verified %d bytes", len(derived))
126
+ return derived if output_raw else derived.hex()
127
+
128
+ def blk3(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
129
+ output_raw=False, input_raw=False):
130
+ if input_raw:
131
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
132
+ else:
133
+ self._validate_nonempty(data)
134
+ data_bytes = self._to_bytes(data)
135
+ self._validate_key(key, 'blk3')
136
+ salt = self._secure_bytes(self._salt_size)
137
+ key_bytes = self._derive_key(key, salt, 32, layer)
138
+ blocks = [data_bytes[i:i+64] for i in range(0, len(data_bytes), 64)]
139
+ tree = [hashlib.blake2b(block + key_bytes, digest_size=32).digest() for block in blocks]
140
+ while len(tree) > 1:
141
+ new_level = []
142
+ for i in range(0, len(tree), 2):
143
+ combined = tree[i] + (tree[i+1] if i+1 < len(tree) else tree[i])
144
+ new_level.append(hashlib.blake2b(combined + key_bytes, digest_size=32).digest())
145
+ tree = new_level
146
+ root = tree[0] if tree else hashlib.blake2b(key_bytes, digest_size=32).digest()
147
+ verification = hmac.digest(key_bytes, root + salt, hashlib.sha3_256)[:16]
148
+ package = self._pack_derivation('blk3', aad, salt, root, verification)
149
+ logger.debug("blk3: root commitment %d bytes", len(root))
150
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
151
+
152
+ def dblk3(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
153
+ output_raw=False, input_raw=False):
154
+ if input_raw:
155
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
156
+ else:
157
+ if not encoded:
158
+ raise ValueError(" Encoded data cannot be empty.")
159
+ package = encoded if isinstance(encoded, bytes) else base64.b85decode(encoded.encode('utf-8'))
160
+ self._validate_key(key, 'dblk3')
161
+ aad_pkg, salt, root, verification = self._unpack_derivation(package, 'blk3')
162
+ if aad is not None and aad != aad_pkg:
163
+ raise ValueError(" AAD mismatch")
164
+ key_bytes = self._derive_key(key, salt, 32, layer)
165
+ expected = hmac.digest(key_bytes, root + salt, hashlib.sha3_256)[:16]
166
+ if not hmac.compare_digest(verification, expected):
167
+ raise ValueError(" Verification failed")
168
+ logger.debug("dblk3: verified root %d bytes", len(root))
169
+ return root if output_raw else root.hex()
@@ -0,0 +1,185 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import hashlib
4
+ import base64
5
+ import hmac
6
+ import logging
7
+ from typing import Union, Optional
8
+ from ..enums import SecurityLayer
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class StreamMixin:
13
+ def strx(self, data: Union[str, bytes], key: str, aad: Optional[bytes] = None,
14
+ layer: SecurityLayer = SecurityLayer.STANDARD,
15
+ output_raw: bool = False, input_raw: bool = False) -> Union[str, bytes]:
16
+ if input_raw:
17
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
18
+ else:
19
+ self._validate_nonempty(data)
20
+ data_bytes = self._to_bytes(data)
21
+ self._validate_key(key, 'strx')
22
+ salt = self._secure_bytes(self._salt_size)
23
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
24
+ nonce = self._secure_bytes(16)
25
+ blake2b = hashlib.blake2b
26
+ xor = self._xor_bytes
27
+ result = bytearray()
28
+ counter = 0
29
+ dlen = len(data_bytes)
30
+ for i in range(0, dlen, 64):
31
+ chunk = data_bytes[i:i + 64]
32
+ keystream = blake2b(nonce + counter.to_bytes(16, 'little'),
33
+ key=enc_key, digest_size=64).digest()
34
+ result.extend(xor(chunk, keystream[:len(chunk)]))
35
+ counter += 1
36
+ ciphertext = bytes(result)
37
+ package = self._pack('strx', aad, salt, nonce, ciphertext, mac_key=mac_key)
38
+ logger.debug("strx: encrypted %d bytes", len(ciphertext))
39
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
40
+
41
+ def dstrx(self, encoded: Union[str, bytes], key: str, aad: Optional[bytes] = None,
42
+ layer: SecurityLayer = SecurityLayer.STANDARD,
43
+ output_raw: bool = False, input_raw: bool = False) -> Union[str, bytes]:
44
+ if input_raw:
45
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
46
+ else:
47
+ if not encoded:
48
+ raise ValueError(" Encoded data cannot be empty.")
49
+ package = encoded if isinstance(encoded, bytes) else base64.b85decode(encoded.encode('utf-8'))
50
+ self._validate_key(key, 'dstrx')
51
+ aad_pkg, salt, nonce, ciphertext, mac_val = self._unpack(package, 'strx')
52
+ if aad is not None and aad != aad_pkg:
53
+ raise ValueError(" AAD mismatch")
54
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
55
+ to_verify = package[:-len(mac_val)] if mac_val else package
56
+ if mac_val and not hmac.compare_digest(mac_val, self._mac(mac_key, to_verify)):
57
+ raise ValueError(" MAC verification failed")
58
+ blake2b = hashlib.blake2b
59
+ xor = self._xor_bytes
60
+ result = bytearray()
61
+ counter = 0
62
+ clen = len(ciphertext)
63
+ for i in range(0, clen, 64):
64
+ chunk = ciphertext[i:i + 64]
65
+ keystream = blake2b(nonce + counter.to_bytes(16, 'little'),
66
+ key=enc_key, digest_size=64).digest()
67
+ result.extend(xor(chunk, keystream[:len(chunk)]))
68
+ counter += 1
69
+ plain = bytes(result)
70
+ logger.debug("dstrx: decrypted %d bytes", len(plain))
71
+ return plain if output_raw else self._to_str(plain)
72
+
73
+ def rvrs(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
74
+ output_raw=False, input_raw=False):
75
+ if input_raw:
76
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
77
+ else:
78
+ self._validate_nonempty(data)
79
+ data_bytes = self._to_bytes(data)
80
+ self._validate_key(key, 'rvrs')
81
+ salt = self._secure_bytes(self._salt_size)
82
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
83
+ nonce = self._secure_bytes(16)
84
+ block_size = 32
85
+ sha3 = hashlib.sha3_256
86
+ xor = self._xor_bytes
87
+ result = bytearray()
88
+ prev = nonce
89
+ dlen = len(data_bytes)
90
+ for i in range(0, dlen, block_size):
91
+ chunk = data_bytes[i:i + block_size]
92
+ keystream = sha3(enc_key + prev).digest()[:len(chunk)]
93
+ cipher_chunk = xor(chunk, keystream)
94
+ result.extend(cipher_chunk)
95
+ prev = cipher_chunk
96
+ ciphertext = bytes(result)
97
+ package = self._pack('rvrs', aad, salt, nonce, ciphertext, mac_key=mac_key)
98
+ logger.debug("rvrs: encrypted %d bytes", len(ciphertext))
99
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
100
+
101
+ def drvrs(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
102
+ output_raw=False, input_raw=False):
103
+ if input_raw:
104
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
105
+ else:
106
+ if not encoded:
107
+ raise ValueError(" Encoded data cannot be empty.")
108
+ package = encoded if isinstance(encoded, bytes) else base64.b85decode(encoded.encode('utf-8'))
109
+ self._validate_key(key, 'drvrs')
110
+ aad_pkg, salt, nonce, ciphertext, mac_val = self._unpack(package, 'rvrs')
111
+ if aad is not None and aad != aad_pkg:
112
+ raise ValueError(" AAD mismatch")
113
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
114
+ to_verify = package[:-len(mac_val)] if mac_val else package
115
+ if mac_val and not hmac.compare_digest(mac_val, self._mac(mac_key, to_verify)):
116
+ raise ValueError(" MAC verification failed")
117
+ block_size = 32
118
+ sha3 = hashlib.sha3_256
119
+ xor = self._xor_bytes
120
+ result = bytearray()
121
+ prev = nonce
122
+ clen = len(ciphertext)
123
+ for i in range(0, clen, block_size):
124
+ chunk = ciphertext[i:i + block_size]
125
+ keystream = sha3(enc_key + prev).digest()[:len(chunk)]
126
+ plain_chunk = xor(chunk, keystream)
127
+ result.extend(plain_chunk)
128
+ prev = chunk
129
+ plain = bytes(result)
130
+ logger.debug("drvrs: decrypted %d bytes", len(plain))
131
+ return plain if output_raw else self._to_str(plain)
132
+
133
+ def lfsr(self, data, key, aad=None, layer=SecurityLayer.STANDARD,
134
+ output_raw=False, input_raw=False):
135
+ if input_raw:
136
+ data_bytes = data if isinstance(data, bytes) else data.encode('utf-8')
137
+ else:
138
+ self._validate_nonempty(data)
139
+ data_bytes = self._to_bytes(data)
140
+ self._validate_key(key, 'lfsr')
141
+ salt = self._secure_bytes(self._salt_size)
142
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
143
+ nonce = self._secure_bytes(16)
144
+ sha3 = hashlib.sha3_256
145
+ blake2b = hashlib.blake2b
146
+ stateA = sha3(enc_key + nonce).digest()
147
+ stateB = blake2b(enc_key + nonce, digest_size=32).digest()
148
+ result = bytearray()
149
+ for byte in data_bytes:
150
+ result.append(byte ^ (stateA[0] ^ stateB[0]))
151
+ stateA = sha3(stateA).digest()
152
+ stateB = blake2b(stateB, digest_size=32).digest()
153
+ ciphertext = bytes(result)
154
+ package = self._pack('lfsr', aad, salt, nonce, ciphertext, mac_key=mac_key)
155
+ logger.debug("lfsr: encrypted %d bytes", len(ciphertext))
156
+ return package if output_raw else base64.b85encode(package).decode('utf-8')
157
+
158
+ def dlfsr(self, encoded, key, aad=None, layer=SecurityLayer.STANDARD,
159
+ output_raw=False, input_raw=False):
160
+ if input_raw:
161
+ package = encoded if isinstance(encoded, bytes) else encoded.encode('utf-8')
162
+ else:
163
+ if not encoded:
164
+ raise ValueError(" Encoded data cannot be empty.")
165
+ package = encoded if isinstance(encoded, bytes) else base64.b85decode(encoded.encode('utf-8'))
166
+ self._validate_key(key, 'dlfsr')
167
+ aad_pkg, salt, nonce, ciphertext, mac_val = self._unpack(package, 'lfsr')
168
+ if aad is not None and aad != aad_pkg:
169
+ raise ValueError(" AAD mismatch")
170
+ enc_key, mac_key = self._derive_keys(key, salt, layer)
171
+ to_verify = package[:-len(mac_val)] if mac_val else package
172
+ if mac_val and not hmac.compare_digest(mac_val, self._mac(mac_key, to_verify)):
173
+ raise ValueError(" MAC verification failed")
174
+ sha3 = hashlib.sha3_256
175
+ blake2b = hashlib.blake2b
176
+ stateA = sha3(enc_key + nonce).digest()
177
+ stateB = blake2b(enc_key + nonce, digest_size=32).digest()
178
+ result = bytearray()
179
+ for byte in ciphertext:
180
+ result.append(byte ^ (stateA[0] ^ stateB[0]))
181
+ stateA = sha3(stateA).digest()
182
+ stateB = blake2b(stateB, digest_size=32).digest()
183
+ plain = bytes(result)
184
+ logger.debug("dlfsr: decrypted %d bytes", len(plain))
185
+ return plain if output_raw else self._to_str(plain)
jpassende/utils.py ADDED
@@ -0,0 +1,31 @@
1
+ # Copyright 2026 J Code
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import hashlib
4
+ import secrets
5
+ from typing import Union
6
+
7
+ def to_bytes(data: Union[str, bytes]) -> bytes:
8
+ return data.encode('utf-8') if isinstance(data, str) else data
9
+
10
+ def to_str(data: bytes) -> str:
11
+ return data.decode('utf-8') if isinstance(data, bytes) else data
12
+
13
+ def secure_bytes(size: int = 32) -> bytes:
14
+ return secrets.token_bytes(size)
15
+
16
+ def validate_nonempty(data: Union[str, bytes], name: str = "data"):
17
+ b = to_bytes(data)
18
+ if len(b) == 0:
19
+ raise ValueError(f" Input {name} cannot be empty.")
20
+
21
+ def validate_key(key: str, pattern: str = ""):
22
+ if not key:
23
+ raise ValueError(f" Key must not be empty for pattern '{pattern}'.")
24
+
25
+ def xor_bytes(a: bytes, b: bytes) -> bytes:
26
+ if len(a) != len(b):
27
+ raise ValueError(f" Length mismatch in XOR: {len(a)} vs {len(b)}")
28
+ return bytes(x ^ y for x, y in zip(a, b))
29
+
30
+ def mac(key: bytes, data: bytes) -> bytes:
31
+ return hashlib.blake2b(data, key=key, digest_size=32).digest()
@@ -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,17 @@
1
+ jpassende/__init__.py,sha256=4BNG9xq3Xwj74UysbbEMCxkQoDTsnBI-fMeOwHObMEE,429
2
+ jpassende/_version.py,sha256=auvrWpJXBBfRo-mNTNbK4eb88E9f5nUxM4PK02c429M,83
3
+ jpassende/core.py,sha256=UuOUqib1fEAL--h9r9u5_nH3IbWAo1xdqCxFwOrFhQ8,11892
4
+ jpassende/datatypes.py,sha256=MqAgfZA_dutVIkTz1yqSVSa9ceZgTssMX4o54pPHS_4,474
5
+ jpassende/enums.py,sha256=krBut1SLq_b_zEkp2v0TBIcdDdHEN5Hsl6X3wyJnQq4,294
6
+ jpassende/exceptions.py,sha256=NOFfcP-uLlzDMoXzdNrFPgITsetAUUVAAxbzrSx8TSI,109
7
+ jpassende/utils.py,sha256=xvPgjm2dF-z_jraGaBm9AKA9gxRlurRyO9oLiKiDJiY,1033
8
+ jpassende/mixins/__init__.py,sha256=8gJomFQIP8dO-2cEV5sw3yCkDGO3U_eRjVw-AWCrsLo,107
9
+ jpassende/mixins/aead.py,sha256=LyBsAVt2rJ603-25XbsYvG8z_47_L08TaQAi0c-RnQc,6912
10
+ jpassende/mixins/block.py,sha256=8r2mHV_2m8CHeUF7xGJ9WIHoEGf4Jnp3imoaMCuZhR4,8846
11
+ jpassende/mixins/derivation.py,sha256=QnLjR5OvMW8wmFOc4e6zz4X9vcLG7Oeup62hL9FAobM,9084
12
+ jpassende/mixins/stream.py,sha256=Hi2k12OSntKS8petUYjP-_Sy-VkY06Ywok9no24F3cg,8729
13
+ jpassende-1.0.0.dist-info/licenses/NOTICE,sha256=0cAPLxMWRM0zFHFjSGF2AO5_jMdmnkb-b-CHwo41pPY,59
14
+ jpassende-1.0.0.dist-info/METADATA,sha256=erd8mcxUfLDwnzYcmKsObenRMqxQRs656SnPyCbI1xo,7231
15
+ jpassende-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
+ jpassende-1.0.0.dist-info/top_level.txt,sha256=PHB9hNDtDWUPPBPBoqlpPTR3vjgluv3ImNpW-07gGuA,10
17
+ jpassende-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ jpassende
2
+ Copyright 2026 J Code(Mohammadjavad Maleki Kaveh)
@@ -0,0 +1 @@
1
+ jpassende