neva256 0.1.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.
- neva256-0.1.0/PKG-INFO +111 -0
- neva256-0.1.0/README.md +92 -0
- neva256-0.1.0/neva256/__init__.py +315 -0
- neva256-0.1.0/neva256/cli.py +83 -0
- neva256-0.1.0/neva256/py.typed +0 -0
- neva256-0.1.0/neva256.egg-info/PKG-INFO +111 -0
- neva256-0.1.0/neva256.egg-info/SOURCES.txt +12 -0
- neva256-0.1.0/neva256.egg-info/dependency_links.txt +1 -0
- neva256-0.1.0/neva256.egg-info/entry_points.txt +2 -0
- neva256-0.1.0/neva256.egg-info/requires.txt +3 -0
- neva256-0.1.0/neva256.egg-info/top_level.txt +1 -0
- neva256-0.1.0/pyproject.toml +37 -0
- neva256-0.1.0/setup.cfg +4 -0
- neva256-0.1.0/tests/test_neva.py +160 -0
neva256-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: neva256
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NEVA-256: experimental self-synchronizing cipher (NOT for production secrets)
|
|
5
|
+
Author: EWX-AI
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ewxai/ewxai
|
|
8
|
+
Project-URL: Repository, https://github.com/ewxai/ewxai
|
|
9
|
+
Keywords: cipher,experimental,self-synchronizing,educational,ctf
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Security :: Cryptography
|
|
14
|
+
Classifier: Topic :: Education
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Provides-Extra: fast
|
|
18
|
+
Requires-Dist: neva256-native<0.2,>=0.1.0; extra == "fast"
|
|
19
|
+
|
|
20
|
+
# neva256 — NEVA-256 for Python
|
|
21
|
+
|
|
22
|
+
Port bit-exact dari implementasi TypeScript di `src/neva/neva.ts`.
|
|
23
|
+
Ciphertext yang dihasilkan **kompatibel lintas bahasa** (diverifikasi lewat
|
|
24
|
+
known-answer test + uji silang Python ↔ Node).
|
|
25
|
+
|
|
26
|
+
> ⚠️ **EKSPERIMENTAL.** NEVA belum pernah dianalisis kriptografer. Jangan pakai
|
|
27
|
+
> untuk melindungi data rahasia sungguhan — gunakan AES-256-GCM, libsodium,
|
|
28
|
+
> atau `cryptography` untuk produksi. Cocok untuk: belajar, CTF, obfuscation,
|
|
29
|
+
> watermark, dan eksperimen.
|
|
30
|
+
|
|
31
|
+
## Instalasi
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install neva256 # pure Python (jalan di mana saja)
|
|
35
|
+
pip install "neva256[fast]" # + akselerator Rust/PyO3 (~360x lebih cepat)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Dari repo ini:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd python && pip install -e . # pure Python
|
|
42
|
+
cd python/rust && pip install maturin && maturin develop --release # + native
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Backend native dipakai otomatis bila tersedia, dan jatuh kembali ke pure Python
|
|
46
|
+
kalau tidak. Eksplisit: `NevaCipher(key, nonce, use_native=True/False)`; cek
|
|
47
|
+
`cipher.backend` (`"native"` atau `"python"`) dan konstanta `NATIVE_AVAILABLE`.
|
|
48
|
+
|
|
49
|
+
## Pemakaian
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from neva256 import NevaCipher, generate_key, generate_nonce
|
|
53
|
+
|
|
54
|
+
key = generate_key() # simpan ini (32 byte)
|
|
55
|
+
nonce = generate_nonce() # acak per pesan (16 byte)
|
|
56
|
+
|
|
57
|
+
cipher = NevaCipher(key, nonce)
|
|
58
|
+
|
|
59
|
+
blob = cipher.encrypt_text("halo dunia 🌪️") # string base64 (ct || tag)
|
|
60
|
+
print(cipher.decrypt_text(blob)) # 'halo dunia 🌪️'
|
|
61
|
+
|
|
62
|
+
# bytes
|
|
63
|
+
data = b"\x00\x01\x02"
|
|
64
|
+
sealed = cipher.encrypt_bytes(data)
|
|
65
|
+
assert cipher.decrypt_bytes(sealed) == data
|
|
66
|
+
|
|
67
|
+
# mode toleran kerusakan: 1 byte rusak hanya merusak ~5 byte sekitarnya
|
|
68
|
+
partial = cipher.decrypt_bytes(corrupted, verify=False)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Salah kunci atau ciphertext yang diubah → `NevaError`
|
|
72
|
+
(`integrity check failed`).
|
|
73
|
+
|
|
74
|
+
## CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
neva256 keygen
|
|
78
|
+
neva256 enc "rahasia" --key <64-hex>
|
|
79
|
+
neva256 dec "<nonceHex>:<base64>" --key <64-hex>
|
|
80
|
+
echo "teks dari pipe" | neva256 enc - --key <64-hex>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Test
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cd python
|
|
87
|
+
python -m unittest discover -s tests -v
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Test mencakup known-answer vectors dari implementasi TypeScript, round-trip
|
|
91
|
+
(a–4 KB), deteksi tamper, validasi ukuran key/nonce, dan sifat
|
|
92
|
+
self-synchronizing.
|
|
93
|
+
|
|
94
|
+
## Catatan keamanan
|
|
95
|
+
|
|
96
|
+
Lihat `src/neva/README.md` untuk matriks serangan lengkap: differential per
|
|
97
|
+
round, kurva sensitivitas rotasi, hill-climbing, SMT/Z3, dan temuan
|
|
98
|
+
equivalent-key pada versi 1-swirl. Ringkasnya: implementasi ini memakai
|
|
99
|
+
**4 swirl** dengan margin satu round di atas ambang kebersihan statistik
|
|
100
|
+
terukur — tapi statusnya tetap eksperimental.
|
|
101
|
+
|
|
102
|
+
## Performa (terukur di aarch64)
|
|
103
|
+
|
|
104
|
+
| Backend | Encrypt 1 MB | Throughput |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| Pure Python | 31.3 s | 0.03 MB/s |
|
|
107
|
+
| **Native (Rust/PyO3)** | **87 ms** | **11.5 MB/s** |
|
|
108
|
+
|
|
109
|
+
Native ~360× lebih cepat dan melepas GIL: 4 thread paralel = 2.5× speedup pada
|
|
110
|
+
mesin uji. Implementasi TypeScript ~3 MB/s (native Rust ~4× lebih cepat dari itu).
|
|
111
|
+
Untuk teks/payload kecil, pure Python sudah cukup.
|
neva256-0.1.0/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# neva256 — NEVA-256 for Python
|
|
2
|
+
|
|
3
|
+
Port bit-exact dari implementasi TypeScript di `src/neva/neva.ts`.
|
|
4
|
+
Ciphertext yang dihasilkan **kompatibel lintas bahasa** (diverifikasi lewat
|
|
5
|
+
known-answer test + uji silang Python ↔ Node).
|
|
6
|
+
|
|
7
|
+
> ⚠️ **EKSPERIMENTAL.** NEVA belum pernah dianalisis kriptografer. Jangan pakai
|
|
8
|
+
> untuk melindungi data rahasia sungguhan — gunakan AES-256-GCM, libsodium,
|
|
9
|
+
> atau `cryptography` untuk produksi. Cocok untuk: belajar, CTF, obfuscation,
|
|
10
|
+
> watermark, dan eksperimen.
|
|
11
|
+
|
|
12
|
+
## Instalasi
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install neva256 # pure Python (jalan di mana saja)
|
|
16
|
+
pip install "neva256[fast]" # + akselerator Rust/PyO3 (~360x lebih cepat)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Dari repo ini:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd python && pip install -e . # pure Python
|
|
23
|
+
cd python/rust && pip install maturin && maturin develop --release # + native
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Backend native dipakai otomatis bila tersedia, dan jatuh kembali ke pure Python
|
|
27
|
+
kalau tidak. Eksplisit: `NevaCipher(key, nonce, use_native=True/False)`; cek
|
|
28
|
+
`cipher.backend` (`"native"` atau `"python"`) dan konstanta `NATIVE_AVAILABLE`.
|
|
29
|
+
|
|
30
|
+
## Pemakaian
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from neva256 import NevaCipher, generate_key, generate_nonce
|
|
34
|
+
|
|
35
|
+
key = generate_key() # simpan ini (32 byte)
|
|
36
|
+
nonce = generate_nonce() # acak per pesan (16 byte)
|
|
37
|
+
|
|
38
|
+
cipher = NevaCipher(key, nonce)
|
|
39
|
+
|
|
40
|
+
blob = cipher.encrypt_text("halo dunia 🌪️") # string base64 (ct || tag)
|
|
41
|
+
print(cipher.decrypt_text(blob)) # 'halo dunia 🌪️'
|
|
42
|
+
|
|
43
|
+
# bytes
|
|
44
|
+
data = b"\x00\x01\x02"
|
|
45
|
+
sealed = cipher.encrypt_bytes(data)
|
|
46
|
+
assert cipher.decrypt_bytes(sealed) == data
|
|
47
|
+
|
|
48
|
+
# mode toleran kerusakan: 1 byte rusak hanya merusak ~5 byte sekitarnya
|
|
49
|
+
partial = cipher.decrypt_bytes(corrupted, verify=False)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Salah kunci atau ciphertext yang diubah → `NevaError`
|
|
53
|
+
(`integrity check failed`).
|
|
54
|
+
|
|
55
|
+
## CLI
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
neva256 keygen
|
|
59
|
+
neva256 enc "rahasia" --key <64-hex>
|
|
60
|
+
neva256 dec "<nonceHex>:<base64>" --key <64-hex>
|
|
61
|
+
echo "teks dari pipe" | neva256 enc - --key <64-hex>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Test
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cd python
|
|
68
|
+
python -m unittest discover -s tests -v
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Test mencakup known-answer vectors dari implementasi TypeScript, round-trip
|
|
72
|
+
(a–4 KB), deteksi tamper, validasi ukuran key/nonce, dan sifat
|
|
73
|
+
self-synchronizing.
|
|
74
|
+
|
|
75
|
+
## Catatan keamanan
|
|
76
|
+
|
|
77
|
+
Lihat `src/neva/README.md` untuk matriks serangan lengkap: differential per
|
|
78
|
+
round, kurva sensitivitas rotasi, hill-climbing, SMT/Z3, dan temuan
|
|
79
|
+
equivalent-key pada versi 1-swirl. Ringkasnya: implementasi ini memakai
|
|
80
|
+
**4 swirl** dengan margin satu round di atas ambang kebersihan statistik
|
|
81
|
+
terukur — tapi statusnya tetap eksperimental.
|
|
82
|
+
|
|
83
|
+
## Performa (terukur di aarch64)
|
|
84
|
+
|
|
85
|
+
| Backend | Encrypt 1 MB | Throughput |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| Pure Python | 31.3 s | 0.03 MB/s |
|
|
88
|
+
| **Native (Rust/PyO3)** | **87 ms** | **11.5 MB/s** |
|
|
89
|
+
|
|
90
|
+
Native ~360× lebih cepat dan melepas GIL: 4 thread paralel = 2.5× speedup pada
|
|
91
|
+
mesin uji. Implementasi TypeScript ~3 MB/s (native Rust ~4× lebih cepat dari itu).
|
|
92
|
+
Untuk teks/payload kecil, pure Python sudah cukup.
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""NEVA-256 — Non-Equilibrium Vortex Algorithm (Python port).
|
|
2
|
+
|
|
3
|
+
A bit-exact Python port of the TypeScript implementation in
|
|
4
|
+
``src/neva/neva.ts``. Ciphertexts are interoperable between both
|
|
5
|
+
implementations (verified by known-answer tests and round-trip tests).
|
|
6
|
+
|
|
7
|
+
EXPERIMENTAL / EDUCATIONAL — NEVA has not been analyzed by cryptographers.
|
|
8
|
+
Do not use it to protect real secrets; use AES-256-GCM / libsodium instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from typing import Optional, Union
|
|
15
|
+
|
|
16
|
+
try: # optional Rust accelerator (neva256-native)
|
|
17
|
+
import neva256_native as _native
|
|
18
|
+
except ImportError: # pragma: no cover - pure Python fallback
|
|
19
|
+
_native = None
|
|
20
|
+
|
|
21
|
+
NATIVE_AVAILABLE = _native is not None
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"KEY_BYTES",
|
|
25
|
+
"NONCE_BYTES",
|
|
26
|
+
"TAG_BYTES",
|
|
27
|
+
"VERSION",
|
|
28
|
+
"NATIVE_AVAILABLE",
|
|
29
|
+
"NevaError",
|
|
30
|
+
"NevaCipher",
|
|
31
|
+
"generate_key",
|
|
32
|
+
"generate_nonce",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
VERSION = "NEVA-256/0.1.0"
|
|
37
|
+
|
|
38
|
+
KEY_BYTES = 32
|
|
39
|
+
NONCE_BYTES = 16
|
|
40
|
+
TAG_BYTES = 16
|
|
41
|
+
|
|
42
|
+
_GOLDEN = 0x9E3779B9
|
|
43
|
+
_SEED_SECOND = 0x85EBCA6B
|
|
44
|
+
|
|
45
|
+
_SALT_VORTEX = 0x1
|
|
46
|
+
_SALT_COMET = 0x7E3
|
|
47
|
+
_SALT_TAG = 0xC0FFEE
|
|
48
|
+
|
|
49
|
+
_M32 = 0xFFFFFFFF
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NevaError(Exception):
|
|
53
|
+
"""Raised for invalid keys, tampered ciphertext, or malformed input."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _m32(x: int) -> int:
|
|
57
|
+
return x & _M32
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _rotl(x: int, r: int) -> int:
|
|
61
|
+
r &= 31
|
|
62
|
+
return _m32((x << r) | (x >> (32 - r)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _swirl(w: list, rnd: int) -> None:
|
|
66
|
+
"""One ARX swirl over an 8-lane state (in place)."""
|
|
67
|
+
for i in range(8):
|
|
68
|
+
a = w[i]
|
|
69
|
+
b = w[(i + 1) & 7]
|
|
70
|
+
c = w[(i + 3) & 7]
|
|
71
|
+
r = 1 + ((a >> ((rnd + i) & 7)) % 31)
|
|
72
|
+
w[i] = _m32(_rotl(_m32(a ^ b), r) + c + _m32((rnd + i) * _GOLDEN))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _seed_words(salt: int) -> list:
|
|
76
|
+
return [
|
|
77
|
+
_m32(0x6E657661 ^ salt),
|
|
78
|
+
_m32(0x2D766F72 ^ _m32(salt * _GOLDEN)),
|
|
79
|
+
0x74657820,
|
|
80
|
+
0x636F7265,
|
|
81
|
+
_m32(0x0E3D25C1 ^ salt),
|
|
82
|
+
0x656C6961,
|
|
83
|
+
_m32(0x24BD08E5 ^ _m32(salt * 0x27D4EB2F)),
|
|
84
|
+
0x1A2B3C4D,
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _absorb_bytes(v: list, data: bytes) -> None:
|
|
89
|
+
acc = 0
|
|
90
|
+
count = 0
|
|
91
|
+
for byte in data:
|
|
92
|
+
acc = _m32(acc | (byte << (8 * (count & 3))))
|
|
93
|
+
count += 1
|
|
94
|
+
if (count & 3) == 0:
|
|
95
|
+
lane = (count >> 2) & 7
|
|
96
|
+
v[lane] = _m32(v[lane] ^ acc ^ _m32(count * _GOLDEN))
|
|
97
|
+
_swirl(v, lane & 7)
|
|
98
|
+
acc = 0
|
|
99
|
+
if count & 3:
|
|
100
|
+
lane = count & 7
|
|
101
|
+
v[lane] = _m32(v[lane] ^ acc ^ _m32((count + 1) * _GOLDEN))
|
|
102
|
+
_swirl(v, lane & 7)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _schedule(key: bytes, nonce: bytes, salt: int) -> list:
|
|
106
|
+
v = _seed_words(salt)
|
|
107
|
+
_swirl(v, salt & 7)
|
|
108
|
+
v[0] = _m32(v[0] ^ _m32((salt + 1) * _GOLDEN))
|
|
109
|
+
_absorb_bytes(v, key)
|
|
110
|
+
_swirl(v, 1)
|
|
111
|
+
_swirl(v, 2)
|
|
112
|
+
_absorb_bytes(v, nonce)
|
|
113
|
+
for i in range(8):
|
|
114
|
+
_swirl(v, 3 + (i & 7))
|
|
115
|
+
return v
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _stream_byte(core: list, reg: int, pos: int) -> int:
|
|
119
|
+
w = core.copy()
|
|
120
|
+
w[0] = _m32(w[0] ^ reg ^ _m32((pos + 1) * _GOLDEN))
|
|
121
|
+
w[1] = _m32(w[1] ^ (reg >> 16) ^ _m32((pos + 1) * _SEED_SECOND))
|
|
122
|
+
w[2] = _m32(w[2] + pos)
|
|
123
|
+
w[7] = _m32(w[7] ^ reg ^ _m32(reg << 13))
|
|
124
|
+
_swirl(w, 0)
|
|
125
|
+
_swirl(w, 3)
|
|
126
|
+
_swirl(w, 6)
|
|
127
|
+
_swirl(w, 9)
|
|
128
|
+
return (w[(pos * 5) & 7] >> ((pos & 3) * 8)) & 0xFF
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _vortex_layer(data: bytes, core: list, decoding: bool = False) -> bytes:
|
|
132
|
+
out = bytearray(len(data))
|
|
133
|
+
reg = 0
|
|
134
|
+
for i in range(len(data)):
|
|
135
|
+
sb = _stream_byte(core, reg, i)
|
|
136
|
+
c = (data[i] ^ sb) & 0xFF
|
|
137
|
+
out[i] = c
|
|
138
|
+
feed = data[i] if decoding else c
|
|
139
|
+
reg = _m32((reg >> 8) | (feed << 24))
|
|
140
|
+
return bytes(out)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _make_permutation(n: int, seeds: list) -> list:
|
|
144
|
+
rng = list(seeds)
|
|
145
|
+
if (rng[0] | rng[1] | rng[2] | rng[3]) == 0:
|
|
146
|
+
rng[0] = _GOLDEN
|
|
147
|
+
|
|
148
|
+
def nxt() -> int:
|
|
149
|
+
t = rng[3]
|
|
150
|
+
w = rng[0]
|
|
151
|
+
rng[3] = rng[2]
|
|
152
|
+
rng[2] = rng[1]
|
|
153
|
+
rng[1] = w
|
|
154
|
+
t = _m32(t ^ _m32(t << 11))
|
|
155
|
+
t = _m32(t ^ (t >> 8))
|
|
156
|
+
rng[0] = _m32(t ^ w ^ (w >> 19))
|
|
157
|
+
return rng[0]
|
|
158
|
+
|
|
159
|
+
p = list(range(n))
|
|
160
|
+
for i in range(n - 1, 0, -1):
|
|
161
|
+
j = nxt() % (i + 1)
|
|
162
|
+
p[i], p[j] = p[j], p[i]
|
|
163
|
+
return p
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _compute_tag(tag_state: list, data: bytes, length: int) -> bytes:
|
|
167
|
+
v = list(tag_state)
|
|
168
|
+
_absorb_bytes(v, data)
|
|
169
|
+
len_digest = bytes(
|
|
170
|
+
[length & 0xFF, (length >> 8) & 0xFF, (length >> 16) & 0xFF, (length >> 24) & 0xFF, 0x5A]
|
|
171
|
+
)
|
|
172
|
+
_absorb_bytes(v, len_digest)
|
|
173
|
+
for i in range(8):
|
|
174
|
+
_swirl(v, (i + 1) & 7)
|
|
175
|
+
out = bytearray(16)
|
|
176
|
+
for i in range(4):
|
|
177
|
+
out[i * 4] = v[i] & 0xFF
|
|
178
|
+
out[i * 4 + 1] = (v[i] >> 8) & 0xFF
|
|
179
|
+
out[i * 4 + 2] = (v[i] >> 16) & 0xFF
|
|
180
|
+
out[i * 4 + 3] = (v[i] >> 24) & 0xFF
|
|
181
|
+
return bytes(out)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _as_bytes(value: Union[bytes, bytearray, str], what: str) -> bytes:
|
|
185
|
+
if isinstance(value, str):
|
|
186
|
+
value = value.encode()
|
|
187
|
+
if not isinstance(value, (bytes, bytearray)):
|
|
188
|
+
raise NevaError(f"{what} must be bytes or str")
|
|
189
|
+
return bytes(value)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class NevaCipher:
|
|
193
|
+
"""NEVA-256 text/binary cipher.
|
|
194
|
+
|
|
195
|
+
>>> import os
|
|
196
|
+
>>> c = NevaCipher(os.urandom(32), os.urandom(16))
|
|
197
|
+
>>> blob = c.encrypt_text("halo dunia")
|
|
198
|
+
>>> c.decrypt_text(blob)
|
|
199
|
+
'halo dunia'
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
key: Union[bytes, bytearray, str],
|
|
205
|
+
nonce: Optional[Union[bytes, bytearray, str]] = None,
|
|
206
|
+
*,
|
|
207
|
+
use_native: Optional[bool] = None,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Create a cipher.
|
|
210
|
+
|
|
211
|
+
``use_native``: ``None`` (default) uses the native Rust backend when
|
|
212
|
+
installed and falls back to pure Python; ``True`` requires native;
|
|
213
|
+
``False`` forces the pure Python implementation.
|
|
214
|
+
"""
|
|
215
|
+
key = _as_bytes(key, "key")
|
|
216
|
+
nonce = bytes(NONCE_BYTES) if nonce is None else _as_bytes(nonce, "nonce")
|
|
217
|
+
if len(key) != KEY_BYTES:
|
|
218
|
+
raise NevaError(f"key must be exactly {KEY_BYTES} bytes (got {len(key)})")
|
|
219
|
+
if len(nonce) != NONCE_BYTES:
|
|
220
|
+
raise NevaError(f"nonce must be exactly {NONCE_BYTES} bytes (got {len(nonce)})")
|
|
221
|
+
self.nonce = nonce
|
|
222
|
+
self._vortex_core = _schedule(key, nonce, _SALT_VORTEX)
|
|
223
|
+
self._comet_seeds = list(_schedule(key, nonce, _SALT_COMET)[:4])
|
|
224
|
+
self._tag_state = _schedule(key, nonce, _SALT_TAG)
|
|
225
|
+
|
|
226
|
+
self._native = None
|
|
227
|
+
self.backend = "python"
|
|
228
|
+
if use_native is True and not NATIVE_AVAILABLE:
|
|
229
|
+
raise NevaError("native backend requested but neva256-native is not installed")
|
|
230
|
+
if use_native is not False and NATIVE_AVAILABLE:
|
|
231
|
+
try:
|
|
232
|
+
self._native = _native.NativeCipher(key, nonce)
|
|
233
|
+
self.backend = "native"
|
|
234
|
+
except Exception: # pragma: no cover - fall back defensively
|
|
235
|
+
if use_native is True:
|
|
236
|
+
raise
|
|
237
|
+
|
|
238
|
+
def encrypt_bytes(self, data: Union[bytes, bytearray, str]) -> bytes:
|
|
239
|
+
"""Encrypt bytes -> ``ciphertext || 16-byte tag``."""
|
|
240
|
+
data = _as_bytes(data, "data")
|
|
241
|
+
if self._native is not None:
|
|
242
|
+
return bytes(self._native.encrypt_bytes(data))
|
|
243
|
+
n = len(data)
|
|
244
|
+
masked = _vortex_layer(data, self._vortex_core)
|
|
245
|
+
if n > 1:
|
|
246
|
+
p = _make_permutation(n, self._comet_seeds)
|
|
247
|
+
payload = bytearray(n)
|
|
248
|
+
for i in range(n):
|
|
249
|
+
payload[i] = masked[p[i]]
|
|
250
|
+
payload = bytes(payload)
|
|
251
|
+
else:
|
|
252
|
+
payload = masked
|
|
253
|
+
return payload + _compute_tag(self._tag_state, payload, n)
|
|
254
|
+
|
|
255
|
+
def decrypt_bytes(self, blob: Union[bytes, bytearray, str], verify: bool = True) -> bytes:
|
|
256
|
+
"""Decrypt bytes produced by :meth:`encrypt_bytes`.
|
|
257
|
+
|
|
258
|
+
With ``verify=False`` the divergence tag is ignored and corruption stays
|
|
259
|
+
local (~5 bytes per damaged byte) thanks to self-synchronization.
|
|
260
|
+
"""
|
|
261
|
+
blob = _as_bytes(blob, "blob")
|
|
262
|
+
if len(blob) < TAG_BYTES:
|
|
263
|
+
raise NevaError("ciphertext too short: missing divergence tag")
|
|
264
|
+
if self._native is not None:
|
|
265
|
+
try:
|
|
266
|
+
return bytes(self._native.decrypt_bytes(blob, verify))
|
|
267
|
+
except NevaError:
|
|
268
|
+
raise
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
raise NevaError(str(exc)) from None
|
|
271
|
+
n = len(blob) - TAG_BYTES
|
|
272
|
+
payload = blob[:n]
|
|
273
|
+
tag = blob[n:]
|
|
274
|
+
if verify and not _constant_eq(_compute_tag(self._tag_state, payload, n), tag):
|
|
275
|
+
raise NevaError("integrity check failed: ciphertext was modified")
|
|
276
|
+
if n > 1:
|
|
277
|
+
p = _make_permutation(n, self._comet_seeds)
|
|
278
|
+
masked = bytearray(n)
|
|
279
|
+
for i in range(n):
|
|
280
|
+
masked[p[i]] = payload[i]
|
|
281
|
+
masked = bytes(masked)
|
|
282
|
+
else:
|
|
283
|
+
masked = payload
|
|
284
|
+
return _vortex_layer(masked, self._vortex_core, decoding=True)
|
|
285
|
+
|
|
286
|
+
def encrypt_text(self, text: str) -> str:
|
|
287
|
+
"""Encrypt UTF-8 text to a standard base64 string."""
|
|
288
|
+
import base64
|
|
289
|
+
|
|
290
|
+
return base64.b64encode(self.encrypt_bytes(text.encode())).decode()
|
|
291
|
+
|
|
292
|
+
def decrypt_text(self, blob: str, verify: bool = True) -> str:
|
|
293
|
+
"""Decrypt a base64 string produced by :meth:`encrypt_text`."""
|
|
294
|
+
import base64
|
|
295
|
+
|
|
296
|
+
return self.decrypt_bytes(base64.b64decode(blob), verify=verify).decode()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _constant_eq(a: bytes, b: bytes) -> bool:
|
|
300
|
+
if len(a) != len(b):
|
|
301
|
+
return False
|
|
302
|
+
diff = 0
|
|
303
|
+
for x, y in zip(a, b):
|
|
304
|
+
diff |= x ^ y
|
|
305
|
+
return diff == 0
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def generate_key() -> bytes:
|
|
309
|
+
"""Return a random 256-bit key."""
|
|
310
|
+
return os.urandom(KEY_BYTES)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def generate_nonce() -> bytes:
|
|
314
|
+
"""Return a random 128-bit nonce."""
|
|
315
|
+
return os.urandom(NONCE_BYTES)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""NEVA-256 command line interface (mirrors the TypeScript CLI)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import (
|
|
9
|
+
KEY_BYTES,
|
|
10
|
+
NONCE_BYTES,
|
|
11
|
+
NevaCipher,
|
|
12
|
+
NevaError,
|
|
13
|
+
generate_key,
|
|
14
|
+
generate_nonce,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
USAGE = """NEVA-256 — text encryption (experimental, not for real secrets)
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
neva256 keygen generate a random 256-bit key (hex)
|
|
21
|
+
neva256 enc <text|-> --key <hex> encrypt text ("-" = stdin)
|
|
22
|
+
neva256 dec <blob|-> --key <hex> decrypt a <nonceHex>:<base64> blob
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _hex_to_bytes(value: str, expected: int, what: str) -> bytes:
|
|
27
|
+
try:
|
|
28
|
+
raw = bytes.fromhex(value)
|
|
29
|
+
except ValueError as exc:
|
|
30
|
+
raise NevaError(f"{what} must be hex") from exc
|
|
31
|
+
if len(raw) != expected:
|
|
32
|
+
raise NevaError(f"{what} must be exactly {expected * 2} hex characters")
|
|
33
|
+
return raw
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _read_input(value: str) -> str:
|
|
37
|
+
return sys.stdin.read() if value == "-" else value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(argv: list | None = None) -> int:
|
|
41
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
42
|
+
if not args or args[0] in ("help", "-h", "--help"):
|
|
43
|
+
print(USAGE, end="")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
cmd = args[0]
|
|
47
|
+
try:
|
|
48
|
+
if cmd == "keygen":
|
|
49
|
+
print(generate_key().hex())
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
if cmd in ("enc", "dec"):
|
|
53
|
+
if len(args) < 2:
|
|
54
|
+
raise NevaError(f"missing argument for {cmd} (use '-' for stdin)")
|
|
55
|
+
if "--key" not in args:
|
|
56
|
+
raise NevaError("missing --key <64-hex-chars>")
|
|
57
|
+
key = _hex_to_bytes(args[args.index("--key") + 1], KEY_BYTES, "key")
|
|
58
|
+
value = _read_input(args[1])
|
|
59
|
+
|
|
60
|
+
if cmd == "enc":
|
|
61
|
+
nonce = generate_nonce()
|
|
62
|
+
cipher = NevaCipher(key, nonce)
|
|
63
|
+
blob = cipher.encrypt_bytes(value.encode())
|
|
64
|
+
print(f"{nonce.hex()}:{base64.b64encode(blob).decode()}")
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
if ":" not in value:
|
|
68
|
+
raise NevaError("bad blob: expected <nonceHex>:<base64>")
|
|
69
|
+
nonce_hex, b64 = value.strip().split(":", 1)
|
|
70
|
+
cipher = NevaCipher(key, _hex_to_bytes(nonce_hex, NONCE_BYTES, "nonce"))
|
|
71
|
+
sys.stdout.write(cipher.decrypt_bytes(base64.b64decode(b64)).decode())
|
|
72
|
+
if sys.stdout.isatty():
|
|
73
|
+
sys.stdout.write("\n")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
raise NevaError(f"unknown command: {cmd} (try: help)")
|
|
77
|
+
except NevaError as exc:
|
|
78
|
+
print(f"error: NEVA: {exc}", file=sys.stderr)
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__": # pragma: no cover
|
|
83
|
+
raise SystemExit(main())
|
|
File without changes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: neva256
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NEVA-256: experimental self-synchronizing cipher (NOT for production secrets)
|
|
5
|
+
Author: EWX-AI
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ewxai/ewxai
|
|
8
|
+
Project-URL: Repository, https://github.com/ewxai/ewxai
|
|
9
|
+
Keywords: cipher,experimental,self-synchronizing,educational,ctf
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Security :: Cryptography
|
|
14
|
+
Classifier: Topic :: Education
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Provides-Extra: fast
|
|
18
|
+
Requires-Dist: neva256-native<0.2,>=0.1.0; extra == "fast"
|
|
19
|
+
|
|
20
|
+
# neva256 — NEVA-256 for Python
|
|
21
|
+
|
|
22
|
+
Port bit-exact dari implementasi TypeScript di `src/neva/neva.ts`.
|
|
23
|
+
Ciphertext yang dihasilkan **kompatibel lintas bahasa** (diverifikasi lewat
|
|
24
|
+
known-answer test + uji silang Python ↔ Node).
|
|
25
|
+
|
|
26
|
+
> ⚠️ **EKSPERIMENTAL.** NEVA belum pernah dianalisis kriptografer. Jangan pakai
|
|
27
|
+
> untuk melindungi data rahasia sungguhan — gunakan AES-256-GCM, libsodium,
|
|
28
|
+
> atau `cryptography` untuk produksi. Cocok untuk: belajar, CTF, obfuscation,
|
|
29
|
+
> watermark, dan eksperimen.
|
|
30
|
+
|
|
31
|
+
## Instalasi
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install neva256 # pure Python (jalan di mana saja)
|
|
35
|
+
pip install "neva256[fast]" # + akselerator Rust/PyO3 (~360x lebih cepat)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Dari repo ini:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd python && pip install -e . # pure Python
|
|
42
|
+
cd python/rust && pip install maturin && maturin develop --release # + native
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Backend native dipakai otomatis bila tersedia, dan jatuh kembali ke pure Python
|
|
46
|
+
kalau tidak. Eksplisit: `NevaCipher(key, nonce, use_native=True/False)`; cek
|
|
47
|
+
`cipher.backend` (`"native"` atau `"python"`) dan konstanta `NATIVE_AVAILABLE`.
|
|
48
|
+
|
|
49
|
+
## Pemakaian
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from neva256 import NevaCipher, generate_key, generate_nonce
|
|
53
|
+
|
|
54
|
+
key = generate_key() # simpan ini (32 byte)
|
|
55
|
+
nonce = generate_nonce() # acak per pesan (16 byte)
|
|
56
|
+
|
|
57
|
+
cipher = NevaCipher(key, nonce)
|
|
58
|
+
|
|
59
|
+
blob = cipher.encrypt_text("halo dunia 🌪️") # string base64 (ct || tag)
|
|
60
|
+
print(cipher.decrypt_text(blob)) # 'halo dunia 🌪️'
|
|
61
|
+
|
|
62
|
+
# bytes
|
|
63
|
+
data = b"\x00\x01\x02"
|
|
64
|
+
sealed = cipher.encrypt_bytes(data)
|
|
65
|
+
assert cipher.decrypt_bytes(sealed) == data
|
|
66
|
+
|
|
67
|
+
# mode toleran kerusakan: 1 byte rusak hanya merusak ~5 byte sekitarnya
|
|
68
|
+
partial = cipher.decrypt_bytes(corrupted, verify=False)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Salah kunci atau ciphertext yang diubah → `NevaError`
|
|
72
|
+
(`integrity check failed`).
|
|
73
|
+
|
|
74
|
+
## CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
neva256 keygen
|
|
78
|
+
neva256 enc "rahasia" --key <64-hex>
|
|
79
|
+
neva256 dec "<nonceHex>:<base64>" --key <64-hex>
|
|
80
|
+
echo "teks dari pipe" | neva256 enc - --key <64-hex>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Test
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cd python
|
|
87
|
+
python -m unittest discover -s tests -v
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Test mencakup known-answer vectors dari implementasi TypeScript, round-trip
|
|
91
|
+
(a–4 KB), deteksi tamper, validasi ukuran key/nonce, dan sifat
|
|
92
|
+
self-synchronizing.
|
|
93
|
+
|
|
94
|
+
## Catatan keamanan
|
|
95
|
+
|
|
96
|
+
Lihat `src/neva/README.md` untuk matriks serangan lengkap: differential per
|
|
97
|
+
round, kurva sensitivitas rotasi, hill-climbing, SMT/Z3, dan temuan
|
|
98
|
+
equivalent-key pada versi 1-swirl. Ringkasnya: implementasi ini memakai
|
|
99
|
+
**4 swirl** dengan margin satu round di atas ambang kebersihan statistik
|
|
100
|
+
terukur — tapi statusnya tetap eksperimental.
|
|
101
|
+
|
|
102
|
+
## Performa (terukur di aarch64)
|
|
103
|
+
|
|
104
|
+
| Backend | Encrypt 1 MB | Throughput |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| Pure Python | 31.3 s | 0.03 MB/s |
|
|
107
|
+
| **Native (Rust/PyO3)** | **87 ms** | **11.5 MB/s** |
|
|
108
|
+
|
|
109
|
+
Native ~360× lebih cepat dan melepas GIL: 4 thread paralel = 2.5× speedup pada
|
|
110
|
+
mesin uji. Implementasi TypeScript ~3 MB/s (native Rust ~4× lebih cepat dari itu).
|
|
111
|
+
Untuk teks/payload kecil, pure Python sudah cukup.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
neva256/__init__.py
|
|
4
|
+
neva256/cli.py
|
|
5
|
+
neva256/py.typed
|
|
6
|
+
neva256.egg-info/PKG-INFO
|
|
7
|
+
neva256.egg-info/SOURCES.txt
|
|
8
|
+
neva256.egg-info/dependency_links.txt
|
|
9
|
+
neva256.egg-info/entry_points.txt
|
|
10
|
+
neva256.egg-info/requires.txt
|
|
11
|
+
neva256.egg-info/top_level.txt
|
|
12
|
+
tests/test_neva.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
neva256
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "neva256"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "NEVA-256: experimental self-synchronizing cipher (NOT for production secrets)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "EWX-AI" }]
|
|
13
|
+
keywords = ["cipher", "experimental", "self-synchronizing", "educational", "ctf"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Security :: Cryptography",
|
|
19
|
+
"Topic :: Education",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
fast = ["neva256-native>=0.1.0,<0.2"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/ewxai/ewxai"
|
|
28
|
+
Repository = "https://github.com/ewxai/ewxai"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
neva256 = "neva256.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
packages = ["neva256"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
neva256 = ["py.typed"]
|
neva256-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Tests for the NEVA-256 Python port.
|
|
2
|
+
|
|
3
|
+
Includes known-answer vectors produced by the TypeScript implementation
|
|
4
|
+
(``src/neva/neva.ts``) to guarantee bit-exact cross-language compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
from neva256 import (
|
|
11
|
+
KEY_BYTES,
|
|
12
|
+
NATIVE_AVAILABLE,
|
|
13
|
+
NONCE_BYTES,
|
|
14
|
+
TAG_BYTES,
|
|
15
|
+
NevaCipher,
|
|
16
|
+
NevaError,
|
|
17
|
+
generate_key,
|
|
18
|
+
generate_nonce,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Known-answer vectors generated with the TypeScript implementation.
|
|
22
|
+
KAT_KEY = bytes.fromhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
|
23
|
+
KAT_NONCE = bytes.fromhex("f0e1d2c3b4a5968778695a4b3c2d1e0f")
|
|
24
|
+
KAT = [
|
|
25
|
+
("", "N4YwZ1bf1M/ydRjWfgXtMA=="),
|
|
26
|
+
("a", "FnerAJQGbirLgGg+pxB2JO0="),
|
|
27
|
+
("halo dunia", "yLbIIB8GBvVN+R0JO/vueBpPXGpT9Bnigj0="),
|
|
28
|
+
("NEVA-256 🌪️ unicode test", "x1Fz4ZaaeaEA/H7hTxePjeO3ObyTGArR9QpBswSUJBnVmmsOcRAy1A4lvyrC"),
|
|
29
|
+
(
|
|
30
|
+
"x" * 64,
|
|
31
|
+
"tkR0wQ9QyBKIEtmBrq3ESLIQs5wvO9vhIILU2RODOg98v2ZvGsXs3/1UuLnEIhRz9dLQAcfhjeF01EOQG8oYfQvxx7cDgzUMKfXrpSTmiAs=",
|
|
32
|
+
),
|
|
33
|
+
("0123456789abcdef", "3M5USVJYDuNHgNJpm6xQJekFSrvWnSgHbVWP4/jKutc="),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestKnownAnswers(unittest.TestCase):
|
|
38
|
+
def test_matches_typescript_vectors(self):
|
|
39
|
+
for text, expected in KAT:
|
|
40
|
+
with self.subTest(text=text[:20]):
|
|
41
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE)
|
|
42
|
+
self.assertEqual(cipher.encrypt_text(text), expected)
|
|
43
|
+
self.assertEqual(NevaCipher(KAT_KEY, KAT_NONCE).decrypt_text(expected), text)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestRoundTrip(unittest.TestCase):
|
|
47
|
+
def test_many_sizes(self):
|
|
48
|
+
for size in (0, 1, 2, 5, 16, 31, 32, 255, 1000, 4096):
|
|
49
|
+
with self.subTest(size=size):
|
|
50
|
+
data = os.urandom(size)
|
|
51
|
+
cipher = NevaCipher(generate_key(), generate_nonce())
|
|
52
|
+
self.assertEqual(cipher.decrypt_bytes(cipher.encrypt_bytes(data)), data)
|
|
53
|
+
|
|
54
|
+
def test_unicode_text(self):
|
|
55
|
+
cipher = NevaCipher(generate_key(), generate_nonce())
|
|
56
|
+
text = "Halo dunia! 🌪️ NEVA-256 — vortex 👾 émojis + ascii"
|
|
57
|
+
self.assertEqual(cipher.decrypt_text(cipher.encrypt_text(text)), text)
|
|
58
|
+
|
|
59
|
+
def test_deterministic(self):
|
|
60
|
+
key, nonce, data = generate_key(), generate_nonce(), os.urandom(64)
|
|
61
|
+
a = NevaCipher(key, nonce).encrypt_bytes(data)
|
|
62
|
+
b = NevaCipher(key, nonce).encrypt_bytes(data)
|
|
63
|
+
self.assertEqual(a, b)
|
|
64
|
+
|
|
65
|
+
def test_nonce_changes_output(self):
|
|
66
|
+
key, data = generate_key(), os.urandom(64)
|
|
67
|
+
a = NevaCipher(key, generate_nonce()).encrypt_bytes(data)
|
|
68
|
+
b = NevaCipher(key, generate_nonce()).encrypt_bytes(data)
|
|
69
|
+
self.assertNotEqual(a, b)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestSecurityProperties(unittest.TestCase):
|
|
73
|
+
def test_wrong_key_rejected(self):
|
|
74
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE)
|
|
75
|
+
blob = cipher.encrypt_bytes(b"secret")
|
|
76
|
+
other = NevaCipher(generate_key(), KAT_NONCE)
|
|
77
|
+
with self.assertRaises(NevaError):
|
|
78
|
+
other.decrypt_bytes(blob)
|
|
79
|
+
|
|
80
|
+
def test_tampering_detected(self):
|
|
81
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE)
|
|
82
|
+
blob = bytearray(cipher.encrypt_bytes(os.urandom(512)))
|
|
83
|
+
blob[100] ^= 0xFF
|
|
84
|
+
with self.assertRaises(NevaError):
|
|
85
|
+
cipher.decrypt_bytes(bytes(blob))
|
|
86
|
+
|
|
87
|
+
def test_resilient_mode_localizes_damage(self):
|
|
88
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE)
|
|
89
|
+
data = os.urandom(10000)
|
|
90
|
+
blob = bytearray(cipher.encrypt_bytes(data))
|
|
91
|
+
blob[5000] ^= 0x40
|
|
92
|
+
out = cipher.decrypt_bytes(bytes(blob), verify=False)
|
|
93
|
+
damaged = sum(1 for i in range(len(data)) if out[i] != data[i])
|
|
94
|
+
self.assertLessEqual(damaged, 6)
|
|
95
|
+
|
|
96
|
+
def test_size_validation(self):
|
|
97
|
+
with self.assertRaises(NevaError):
|
|
98
|
+
NevaCipher(bytes(KEY_BYTES - 1))
|
|
99
|
+
with self.assertRaises(NevaError):
|
|
100
|
+
NevaCipher(generate_key(), bytes(NONCE_BYTES + 1))
|
|
101
|
+
|
|
102
|
+
def test_short_ciphertext_rejected(self):
|
|
103
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE)
|
|
104
|
+
with self.assertRaises(NevaError):
|
|
105
|
+
cipher.decrypt_bytes(b"too short")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestBackends(unittest.TestCase):
|
|
109
|
+
def test_pure_backend_can_be_forced(self):
|
|
110
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE, use_native=False)
|
|
111
|
+
self.assertEqual(cipher.backend, "python")
|
|
112
|
+
self.assertEqual(cipher.encrypt_text("abc"), NevaCipher(KAT_KEY, KAT_NONCE, use_native=False).encrypt_text("abc"))
|
|
113
|
+
|
|
114
|
+
@unittest.skipUnless(NATIVE_AVAILABLE, "neva256-native not installed")
|
|
115
|
+
def test_native_backend_selected(self):
|
|
116
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE, use_native=True)
|
|
117
|
+
self.assertEqual(cipher.backend, "native")
|
|
118
|
+
for text, expected in KAT:
|
|
119
|
+
self.assertEqual(cipher.encrypt_text(text), expected)
|
|
120
|
+
|
|
121
|
+
@unittest.skipUnless(NATIVE_AVAILABLE, "neva256-native not installed")
|
|
122
|
+
def test_native_matches_pure_python_exactly(self):
|
|
123
|
+
key, nonce = generate_key(), generate_nonce()
|
|
124
|
+
native = NevaCipher(key, nonce, use_native=True)
|
|
125
|
+
pure = NevaCipher(key, nonce, use_native=False)
|
|
126
|
+
for size in (0, 1, 2, 31, 255, 1000, 4096):
|
|
127
|
+
with self.subTest(size=size):
|
|
128
|
+
data = os.urandom(size)
|
|
129
|
+
self.assertEqual(native.encrypt_bytes(data), pure.encrypt_bytes(data))
|
|
130
|
+
|
|
131
|
+
@unittest.skipUnless(NATIVE_AVAILABLE, "neva256-native not installed")
|
|
132
|
+
def test_cross_backend_round_trip(self):
|
|
133
|
+
key, nonce = generate_key(), generate_nonce()
|
|
134
|
+
native = NevaCipher(key, nonce, use_native=True)
|
|
135
|
+
pure = NevaCipher(key, nonce, use_native=False)
|
|
136
|
+
data = os.urandom(500)
|
|
137
|
+
self.assertEqual(pure.decrypt_bytes(native.encrypt_bytes(data)), data)
|
|
138
|
+
self.assertEqual(native.decrypt_bytes(pure.encrypt_bytes(data)), data)
|
|
139
|
+
|
|
140
|
+
@unittest.skipUnless(NATIVE_AVAILABLE, "neva256-native not installed")
|
|
141
|
+
def test_native_reports_tampering_as_neva_error(self):
|
|
142
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE, use_native=True)
|
|
143
|
+
blob = bytearray(cipher.encrypt_bytes(b"secret data"))
|
|
144
|
+
blob[20] ^= 0x01
|
|
145
|
+
with self.assertRaises(NevaError):
|
|
146
|
+
cipher.decrypt_bytes(bytes(blob))
|
|
147
|
+
|
|
148
|
+
@unittest.skipUnless(NATIVE_AVAILABLE, "neva256-native not installed")
|
|
149
|
+
def test_native_resilient_mode(self):
|
|
150
|
+
cipher = NevaCipher(KAT_KEY, KAT_NONCE, use_native=True)
|
|
151
|
+
data = os.urandom(4096)
|
|
152
|
+
blob = bytearray(cipher.encrypt_bytes(data))
|
|
153
|
+
blob[2048] ^= 0x40
|
|
154
|
+
out = cipher.decrypt_bytes(bytes(blob), verify=False)
|
|
155
|
+
damaged = sum(1 for i in range(len(data)) if out[i] != data[i])
|
|
156
|
+
self.assertLessEqual(damaged, 6)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
unittest.main()
|