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 +17 -0
- jpassende/_version.py +3 -0
- jpassende/core.py +274 -0
- jpassende/datatypes.py +22 -0
- jpassende/enums.py +14 -0
- jpassende/exceptions.py +4 -0
- jpassende/mixins/__init__.py +3 -0
- jpassende/mixins/aead.py +141 -0
- jpassende/mixins/block.py +171 -0
- jpassende/mixins/derivation.py +169 -0
- jpassende/mixins/stream.py +185 -0
- jpassende/utils.py +31 -0
- jpassende-1.0.0.dist-info/METADATA +197 -0
- jpassende-1.0.0.dist-info/RECORD +17 -0
- jpassende-1.0.0.dist-info/WHEEL +5 -0
- jpassende-1.0.0.dist-info/licenses/NOTICE +2 -0
- jpassende-1.0.0.dist-info/top_level.txt +1 -0
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
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()
|
jpassende/exceptions.py
ADDED
jpassende/mixins/aead.py
ADDED
|
@@ -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
|
+
[](https://www.python.org/downloads/)
|
|
26
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
27
|
+
[](https://github.com/psf/black)
|
|
28
|
+
[](https://pypi.org/project/jpassende/)
|
|
29
|
+
[](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 @@
|
|
|
1
|
+
jpassende
|