jpassende 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jpassende-1.0.0/NOTICE +2 -0
- jpassende-1.0.0/PKG-INFO +197 -0
- jpassende-1.0.0/README.md +173 -0
- jpassende-1.0.0/jpassende/__init__.py +17 -0
- jpassende-1.0.0/jpassende/_version.py +3 -0
- jpassende-1.0.0/jpassende/core.py +274 -0
- jpassende-1.0.0/jpassende/datatypes.py +22 -0
- jpassende-1.0.0/jpassende/enums.py +14 -0
- jpassende-1.0.0/jpassende/exceptions.py +4 -0
- jpassende-1.0.0/jpassende/mixins/__init__.py +3 -0
- jpassende-1.0.0/jpassende/mixins/aead.py +141 -0
- jpassende-1.0.0/jpassende/mixins/block.py +171 -0
- jpassende-1.0.0/jpassende/mixins/derivation.py +169 -0
- jpassende-1.0.0/jpassende/mixins/stream.py +185 -0
- jpassende-1.0.0/jpassende/utils.py +31 -0
- jpassende-1.0.0/jpassende.egg-info/PKG-INFO +197 -0
- jpassende-1.0.0/jpassende.egg-info/SOURCES.txt +20 -0
- jpassende-1.0.0/jpassende.egg-info/dependency_links.txt +1 -0
- jpassende-1.0.0/jpassende.egg-info/requires.txt +1 -0
- jpassende-1.0.0/jpassende.egg-info/top_level.txt +1 -0
- jpassende-1.0.0/pyproject.toml +47 -0
- jpassende-1.0.0/setup.cfg +4 -0
jpassende-1.0.0/NOTICE
ADDED
jpassende-1.0.0/PKG-INFO
ADDED
|
@@ -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,173 @@
|
|
|
1
|
+
[](https://www.python.org/downloads/)
|
|
2
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
3
|
+
[](https://github.com/psf/black)
|
|
4
|
+
[](https://pypi.org/project/jpassende/)
|
|
5
|
+
[](https://pypi.org/project/jpassende/)
|
|
6
|
+
|
|
7
|
+
<br>
|
|
8
|
+
|
|
9
|
+
<img src="docs/images/jpassende-logo.png" alt="jpassende">
|
|
10
|
+
|
|
11
|
+
<br>
|
|
12
|
+
|
|
13
|
+
**jpassende** is a high‑performance, multi‑pattern cryptographic library for Python that goes far beyond standard encryption. It offers 14 unique patterns spanning AEAD, stream ciphers, block ciphers, and key derivation – all wrapped in a simple, consistent API. Every pattern uses its own distinct combination of algorithms and constructions, making your ciphertext immediately recognisable and self‑describing.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick Start – Encrypt & Decrypt in Two Lines
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from jpassende import JPassende
|
|
21
|
+
|
|
22
|
+
jp = JPassende()
|
|
23
|
+
|
|
24
|
+
result = jp.encode("Hello, World!", "vail", key="my_secret")
|
|
25
|
+
print(result.encoded)
|
|
26
|
+
|
|
27
|
+
original = jp.decode(result.encoded, "vail", key="my_secret")
|
|
28
|
+
print(original.decoded)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Main Capabilities
|
|
34
|
+
|
|
35
|
+
**· AEAD Patterns** – vail (AES‑GCM), phnx (ChaCha20‑Poly1305), nixl (ChaCha20 + independent HMAC). All three provide authenticated encryption with associated data (AAD) support.
|
|
36
|
+
|
|
37
|
+
**· Stream Patterns** – strx (BLAKE2‑based keystream), rvrs (SHA‑3 feedback mode), lfsr (dual‑state SHA‑3 / BLAKE2 generator). Byte‑by‑byte encryption without padding, ideal for streaming data.
|
|
38
|
+
|
|
39
|
+
**· Block Patterns** – aegs (AES‑CTR + HMAC), cblk (AES‑CBC + HMAC), cfbb (AES‑CFB‑128 + HMAC), ofbb (AES‑OFB + HMAC). Standard block cipher modes, each individually authenticated.
|
|
40
|
+
|
|
41
|
+
**· Derivation Patterns** – hkdf (HMAC‑based Extract‑and‑Expand), scrt (scrypt), pbk2 (PBKDF2‑SHA‑512), blk3 (Merkle‑tree commitment). Password hashing, key material generation, and data integrity commitments.
|
|
42
|
+
|
|
43
|
+
**· Security Layers** – Every pattern supports three selectable security layers: STANDARD (300k PBKDF2 iterations), FORTIFIED (600k), and QUANTUM (1.2M). You control the trade‑off between speed and brute‑force resistance.
|
|
44
|
+
|
|
45
|
+
**· Binary & Text I/O** – output_raw returns bytes instead of base‑encoded strings. input_raw accepts raw bytes directly, so you can encrypt binary files, images, or any byte sequence.
|
|
46
|
+
|
|
47
|
+
**· Cross‑Instance Decryption** – Packages carry all the metadata (magic, version, pattern ID, salt, nonce) needed for decryption. Any JPassende instance anywhere can decrypt, provided it has the same key.
|
|
48
|
+
|
|
49
|
+
**· Self‑Describing Packages** – The binary format includes a magic header, version byte, pattern identifier, and optional AAD. No more guessing which algorithm was used.
|
|
50
|
+
|
|
51
|
+
**· LRU Key Cache** – PBKDF2 derivations are cached (thread‑safe LRU) to avoid redundant work when the same password is reused.
|
|
52
|
+
|
|
53
|
+
**· Invalid Package Detection** – A dedicated InvalidPackageError is raised when the package structure, magic, or version is invalid.
|
|
54
|
+
|
|
55
|
+
**· Zero Plaintext Password Storage** – Cache keys are derived from a BLAKE2b hash of (password + salt + parameters), never from the password itself.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Pattern Status
|
|
60
|
+
|
|
61
|
+
The patterns nixl, strx, rvrs, lfsr, aegs, cblk, cfbb, ofbb, hkdf, scrt, pbk2, and blk3 are custom constructions created exclusively for jpassende. They are currently experimental and under active development – their internal design may evolve as we gather feedback and perform further security analysis. The patterns vail (AES‑256‑GCM) and phnx (ChaCha20‑Poly1305) use standardized, well‑vetted algorithms and are considered stable. If you plan to use the experimental patterns in production, we strongly recommend performing your own security review and staying updated with new releases.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install jpassende
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
jpassende depends only on pycryptodome (≥ 3.18) and Python's standard library.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## More Examples
|
|
76
|
+
|
|
77
|
+
Encrypting Binary Data (input_raw / output_raw)
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from jpassende import JPassende
|
|
81
|
+
|
|
82
|
+
jp = JPassende()
|
|
83
|
+
image = open("photo.png", "rb").read()
|
|
84
|
+
|
|
85
|
+
enc_pkg = jp.encode(image, "nixl", key="secret", input_raw=True, output_raw=True)
|
|
86
|
+
|
|
87
|
+
dec_bytes = jp.decode(enc_pkg.encoded, "nixl", key="secret",
|
|
88
|
+
input_raw=True, output_raw=True).decoded
|
|
89
|
+
|
|
90
|
+
with open("photo_decrypted.png", "wb") as f:
|
|
91
|
+
f.write(dec_bytes)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Choosing a Security Layer
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from jpassende import JPassende, SecurityLayer
|
|
98
|
+
|
|
99
|
+
jp = JPassende()
|
|
100
|
+
|
|
101
|
+
result = jp.encode("Sensitive data", "phnx", key="strong",
|
|
102
|
+
layer=SecurityLayer.QUANTUM)
|
|
103
|
+
print(result.layer)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Using AAD (Additional Authenticated Data)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
aad = b"user-id:12345"
|
|
110
|
+
result = jp.encode("Hello", "vail", key="secret", aad=aad)
|
|
111
|
+
decoded = jp.decode(result.encoded, "vail", key="secret", aad=aad)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Key Derivation – HKDF
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
derived = jp.encode("master-seed", "hkdf", key="secret", length=32)
|
|
118
|
+
print(derived.encoded[:30] + "...")
|
|
119
|
+
|
|
120
|
+
verified = jp.decode(derived.encoded, "hkdf", key="secret")
|
|
121
|
+
print(verified.decoded[:20] + "...")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## List All Available Patterns
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from jpassende import JPassende
|
|
128
|
+
|
|
129
|
+
print(JPassende.PATTERNS.keys())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Error Handling
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from jpassende import JPassende, InvalidPackageError
|
|
138
|
+
|
|
139
|
+
jp = JPassende()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
jp.decode("not-valid-data", "vail", key="secret")
|
|
143
|
+
except InvalidPackageError as e:
|
|
144
|
+
print(f"Package error: {e}")
|
|
145
|
+
except ValueError as e:
|
|
146
|
+
print(f"Other error: {e}")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Issues and Contributions
|
|
152
|
+
|
|
153
|
+
Bug reports and feature requests are welcome via GitHub Issues. Pull requests should maintain the existing code style and include tests where appropriate.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Links
|
|
158
|
+
|
|
159
|
+
**· GitHub repository:**
|
|
160
|
+
https://github.com/JCode-JCode/jpassende
|
|
161
|
+
|
|
162
|
+
**· PyPI page:**
|
|
163
|
+
https://pypi.org/project/jpassende/
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
This project is licensed under the Apache License 2.0 – see the LICENSE file for details.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
Designed and built with love by **J Code**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright 2026 J Code
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .enums import SecurityLayer, PatternCategory
|
|
5
|
+
from .exceptions import InvalidPackageError
|
|
6
|
+
from .datatypes import CryptoResult, DecodeResult
|
|
7
|
+
from .core import JPassende
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"__version__",
|
|
11
|
+
"SecurityLayer",
|
|
12
|
+
"PatternCategory",
|
|
13
|
+
"InvalidPackageError",
|
|
14
|
+
"CryptoResult",
|
|
15
|
+
"DecodeResult",
|
|
16
|
+
"JPassende",
|
|
17
|
+
]
|
|
@@ -0,0 +1,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())
|