Skip to content

Commit ade8643

Browse files
committed
Added codec: aer256
1 parent 0541e6d commit ade8643

5 files changed

Lines changed: 154 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
347347
#### [Cryptography](https://python-codext.readthedocs.io/en/latest/enc/crypto.html)
348348

349349
- [X] `adfgx`: aka ADFGX Cipher (Polybius square and columnar transposition) ; `adfgvx` for its 6x6 variant
350+
- [X] `aer256`: aka AER-256 (toy cipher with float arithmetic, from online JavaScript tools)
350351
- [X] `affine`: aka Affine Cipher
351352
- [X] `atbash`: aka Atbash Cipher
352353
- [X] `autoclave`: aka Autoclave/Autokey Cipher (variant of Vigenere Cipher)

‎docs/pages/enc/crypto.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,27 @@ The codec name holds the optional square key, then the mandatory transposition k
4747

4848
-----
4949

50+
### AER-256
51+
52+
This toy cipher comes from online JavaScript encryption tools. The text is split into blocks of half the key length (rounded up), each block being read as a number made of its character codes. For each character of the key, its code is added to, divides or is subtracted from this number in turn, and the resulting floating-point numbers are written as JavaScript would and separated by `, `.
53+
54+
**Codec** | **Conversions** | **Aliases** | **Comment**
55+
:---: | :---: | --- | ---
56+
`aer256` | text <-> AER-256 ciphertext | `aer-256`, `aer_256` | parameterized with a mandatory key of at least 3 characters, e.g. `aer256-secret` ; only characters with a code below 256
57+
58+
!!! warning "Precision"
59+
60+
As the computations are done with double-precision floats, as in JavaScript, keys of more than 12 characters give blocks that are too large to be decrypted exactly.
61+
62+
```python
63+
>>> codext.encode("this is a test", "aer256-key")
64+
'175.10891089108912, 147.33663366336634, -37.79207920792079, 171.86138613861385, 126.23762376237624, 175.0792079207921, 172.69306930693068'
65+
>>> codext.decode("175.10891089108912, 147.33663366336634", "aer256-key")
66+
'this'
67+
```
68+
69+
-----
70+
5071
### Affine Cipher
5172

5273
This codec implements the Affine monoalphabetic substitution cipher. It is parametrizable with a mask for generating the alphabet and the parameters `a` and `b`. By default, it uses mask "`lus`" and parameters `a=1` and `b=2` but it can be set as in the examples hereafter.

‎src/codext/crypto/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: UTF-8 -*-
22
from .adfgx import *
3+
from .aer256 import *
34
from .affine import *
45
from .atbash import *
56
from .bacon import *

‎src/codext/crypto/aer256.py‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# -*- coding: UTF-8 -*-
2+
"""AER-256 Codec - AER-256 content encryption.
3+
4+
AER-256 is a toy cipher found on online JavaScript encryption tools. The text is split into blocks of half the key
5+
length (rounded up), each block being read as a big-endian number of its character codes. For each character of the key
6+
(at least 3), its code is added to, divides or is subtracted from this number in turn (in floating-point arithmetic),
7+
and the resulting numbers are written as JavaScript would (e.g. "123.456") and separated by ", ". Decryption applies
8+
the inverse operations in reverse order and rounds the result. As numbers are double-precision floats, blocks of more
9+
than 6 characters (i.e. keys of more than 12 characters) lose precision and generally cannot be fully decrypted. Only
10+
characters with a code below 256 can be encrypted, and null characters at the start of a block are lost.
11+
12+
This codec:
13+
- en/decodes strings from str to str
14+
- en/decodes strings from bytes to bytes
15+
- decodes file content to str (read)
16+
- encodes file content from str to bytes (write)
17+
18+
Reference: https://github.com/gungunfebrianza/Mastering-Cryptography/blob/master/AER-256/AER-256.js
19+
"""
20+
from math import floor, isfinite
21+
22+
from ..__common__ import *
23+
24+
25+
__examples__ = {
26+
'enc(aer256|aer256-ab)': None,
27+
'enc(aer256-key|aer-256_key)': {'this is a test': "175.10891089108912, 147.33663366336634, -37.79207920792079, "
28+
"171.86138613861385, 126.23762376237624, 175.0792079207921, "
29+
"172.69306930693068"},
30+
'dec(aer256-key)': {'175.10891089108912, 147.33663366336634': "this", '175.1, x': None},
31+
'enc-dec(aer256-key|aer256-secret|aer256-K3y!|aer256-123456789012)': ["", "this is a test", "Hello, World! 123",
32+
"\xe9t\xe9 \xff"],
33+
}
34+
35+
36+
def _js_str(x):
37+
""" Convert a float to a string like JavaScript's Number.prototype.toString. """
38+
x = float(x)
39+
if x != x:
40+
return "NaN"
41+
if x < 0:
42+
return "-" + _js_str(-x)
43+
if x == 0:
44+
return "0"
45+
if not isfinite(x):
46+
return "Infinity"
47+
# shortest digits that round-trip (as with repr) and decimal exponent n such that x = 0.digits * 10^n
48+
m, _, e = repr(x).partition("e")
49+
i, _, f = m.partition(".")
50+
d = (i + f.rstrip("0")) if f != "0" else i
51+
n = len(i) + int(e or 0)
52+
k = len(d.lstrip("0"))
53+
n -= len(d) - k
54+
d = d.lstrip("0")
55+
if k <= n <= 21:
56+
return d + "0" * (n - k)
57+
if 0 < n <= 21:
58+
return d[:n] + "." + d[n:]
59+
if -6 < n <= 0:
60+
return "0." + "0" * -n + d
61+
return d[0] + ("." + d[1:] if k > 1 else "") + "e" + "+-"[n < 1] + str(abs(n - 1))
62+
63+
64+
def __check_key(key):
65+
key = str(key or "")
66+
if len(key) < 3:
67+
raise LookupError("Bad parameter for encoding 'aer256': key must be at least 3 characters long")
68+
return [ord(c) for c in key], (len(key) + 1) // 2
69+
70+
71+
def aer256_encode(key=None):
72+
def encode(text, errors="strict"):
73+
k, n = __check_key(key)
74+
_h, t = handle_error("aer256", errors), ensure_str(text)
75+
t = "".join(c for c in "".join(c if ord(c) < 256 else _h(c, i) for i, c in enumerate(t)) if ord(c) < 256)
76+
r = []
77+
for i in range(0, len(t), n):
78+
v = float(int("".join(f"{ord(c):02x}" for c in t[i:i+n]), 16))
79+
for j, kc in enumerate(k):
80+
if j % 3 == 0:
81+
v += kc
82+
elif j % 3 == 1:
83+
v /= kc
84+
else:
85+
v -= kc
86+
r.append(_js_str(v))
87+
return ", ".join(r), len(text)
88+
return encode
89+
90+
91+
def aer256_decode(key=None):
92+
def decode(text, errors="strict"):
93+
k, _ = __check_key(key)
94+
_h, t, r = handle_error("aer256", errors, decode=True, kind="number"), ensure_str(text), ""
95+
for i, s in enumerate(t.split(", ") if t else []):
96+
try:
97+
v = float(s)
98+
except ValueError:
99+
r += _h(s, i)
100+
continue
101+
for j in range(len(k) - 1, -1, -1):
102+
if j % 3 == 0:
103+
v -= k[j]
104+
elif j % 3 == 1:
105+
v *= k[j]
106+
else:
107+
v += k[j]
108+
# Math.round, then the hexadecimal digits of the number (nothing when it is not positive)
109+
v = floor(v) + (v - floor(v) >= .5) if isfinite(v) else 0
110+
h = f"{v:x}" if v > 0 else ""
111+
h = "0" * (len(h) % 2) + h
112+
r += "".join(chr(int(h[j:j+2], 16)) for j in range(0, len(h), 2))
113+
return r, len(text)
114+
return decode
115+
116+
117+
add("aer256", aer256_encode, aer256_decode, r"^aer[-_]?256(?:[-_](\S*))?$", guess=None)

‎tests/test_manual.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ def test_codec_sms(self):
7979

8080

8181
class ManualTestCase(TestCase):
82+
def test_codec_aer256(self):
83+
from codext.crypto.aer256 import _js_str
84+
# numbers formatted as with JavaScript's String(...), as given by Node.js
85+
for x, r in [(0., "0"), (-0., "0"), (1e21, "1e+21"), (123456789012345680000., "123456789012345680000"),
86+
(1e-6, "0.000001"), (1e-7, "1e-7"), (-1.2345e-7, "-1.2345e-7"), (2**60, "1152921504606847000"),
87+
(float("nan"), "NaN"), (float("-inf"), "-Infinity"), (.1, "0.1")]:
88+
self.assertEqual(_js_str(x), r)
89+
# numbers that are not positive after decryption give no character, as with the original JavaScript
90+
self.assertEqual(codecs.decode("NaN, -1e+21, 175.10891089108912", "aer256-key"), "th")
91+
self.assertEqual(codecs.decode("x, 175.10891089108912", "aer256-key", "ignore"), "th")
92+
# with a long key, blocks exceed the precision of floats
93+
self.assertNotEqual(codecs.decode(codecs.encode("this is a test", k := "aer256-thisisaverylongkey"), k),
94+
"this is a test")
95+
8296
def test_codec_affine(self):
8397
STR = "this is a test"
8498
AFF1 = "vjkubkubcbvguv"

0 commit comments

Comments
 (0)