Skip to content

Commit 5cee0cb

Browse files
committed
Added codec: esab46
1 parent 1ddd7bb commit 5cee0cb

6 files changed

Lines changed: 124 additions & 58 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
363363
- [X] `citrix`: aka Citrix CTX1 password encoding
364364
- [X] `columnar`: aka Columnar Transposition Cipher (keyword-ordered columns ; a second keyword gives the double transposition)
365365
- [X] `enigma`: aka Enigma machine (Enigma I, M3 and M4, with reflector, rotors, rings, positions and plugboard)
366+
- [X] `esab46`: aka ESAB-46 (Base64 with a shuffled alphabet, from online JavaScript tools)
366367
- [X] `foursquare`: aka Four-square Cipher (bigrams substituted with two keyed squares and two plain ones)
367368
- [X] `fractionated-morse`: aka Fractionated Morse Cipher (Morse code split into groups of three symbols, each replaced by a letter of a keyed alphabet)
368369
- [X] `gronsfeld`: aka Gronsfeld Cipher (variant of Vigenere Cipher with a numeric key)

‎docs/pages/enc/crypto.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,23 @@ The settings are given with the codec name as `enigma-[reflector]-[rotors]-[ring
380380

381381
-----
382382

383+
### ESAB-46
384+
385+
This toy cipher ("Base-64" reversed) comes from online JavaScript encryption tools. Like [ATOM-128](#atom-128), it is Base64 with a shuffled alphabet, whose 65th character (`/`) is used for padding, and the text is first escaped with JavaScript's `escape()`, then unescaped after decoding.
386+
387+
**Codec** | **Conversions** | **Aliases** | **Comment**
388+
:---: | :---: | --- | ---
389+
`esab46` | text <-> ESAB-46 ciphertext | `esab-46`, `esb46`, `esb_46` |
390+
391+
```python
392+
>>> codext.encode("this is a test", "esab46")
393+
'9sRZ8i0ivsVj6z5gLy0iv4xV8nw/'
394+
>>> codext.decode("9sRZ8i0ivsVj6z5gLy0iv4xV8nw/", "esab-46")
395+
'this is a test'
396+
```
397+
398+
-----
399+
383400
### Four-square Cipher
384401

385402
The Four-square cipher, invented by Felix Delastelle, substitutes pairs of letters using four squares arranged in a 2x2 matrix: the upper-left and lower-right ones hold the alphabet in order, while the upper-right and lower-left ones are filled with two keywords. The first letter of each pair is located in the upper-left square and the second one in the lower-right square ; they are replaced by the letters at the other corners of the rectangle they form (in the upper-right square, then in the lower-left one).

‎src/codext/crypto/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .citrix import *
1515
from .columnar import *
1616
from .enigma import *
17+
from .esab46 import *
1718
from .foursquare import *
1819
from .fractionatedmorse import *
1920
from .hill import *

‎src/codext/crypto/_jsbase64.py‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Base64 variants, shared by the toy ciphers of online JavaScript encryption tools (ATOM-128, ESAB-46).
3+
4+
These ciphers are Base64 with a shuffled alphabet of 65 characters, whose last one is used for padding. As in the
5+
original JavaScript, the text is first escaped with JavaScript's escape() (e.g. " " becoming "%20" and "é" becoming
6+
"%E9"), and unescaped after decoding ; ciphertexts made without this escaping (i.e. from the UTF-8 bytes of the text)
7+
are then decoded the same way.
8+
"""
9+
import builtins
10+
import re
11+
from base64 import b64decode, b64encode
12+
from binascii import Error as B64Error
13+
from string import ascii_letters, ascii_lowercase as LC, ascii_uppercase as UC, digits
14+
15+
from ..__common__ import ensure_str, exc_name, handle_error
16+
17+
18+
B64 = UC + LC + digits + "+/="
19+
# characters left as is by JavaScript's escape()
20+
_SAFE = set(ascii_letters + digits + "@*_+-./")
21+
22+
23+
def js_escape(text):
24+
""" Escape a string like JavaScript's escape() (characters beyond the BMP being escaped as surrogate pairs). """
25+
r = ""
26+
for c in text:
27+
if c in _SAFE:
28+
r += c
29+
elif ord(c) < 256:
30+
r += f"%{ord(c):02X}"
31+
else:
32+
u = c.encode("utf-16-be", "surrogatepass")
33+
r += "".join(f"%u{u[i:i+2].hex().upper()}" for i in range(0, len(u), 2))
34+
return r
35+
36+
37+
def js_unescape(text):
38+
""" Unescape a string like JavaScript's unescape() (surrogate pairs being joined). """
39+
s = re.sub(r"%u([0-9a-fA-F]{4})|%([0-9a-fA-F]{2})", lambda m: chr(int(m.group(1) or m.group(2), 16)), text)
40+
return s.encode("utf-16-be", "surrogatepass").decode("utf-16-be", "replace")
41+
42+
43+
def js_base64(name, alphabet):
44+
""" Make the encoding and decoding functions of a Base64 variant, given its alphabet (with the padding character
45+
last). """
46+
enc_map = str.maketrans(B64, alphabet)
47+
48+
def encode(text, errors="strict"):
49+
return b64encode(js_escape(ensure_str(text)).encode()).decode().translate(enc_map), len(text)
50+
51+
def decode(text, errors="strict"):
52+
_h, t, r = handle_error(name, errors, decode=True), re.sub(r"\s", "", ensure_str(text)), ""
53+
for i, c in enumerate(t):
54+
if c in alphabet:
55+
r += B64[alphabet.index(c)]
56+
else:
57+
# invalid characters are dropped (as the original JavaScript does) when not in strict mode
58+
_h(c, i)
59+
try:
60+
r = b64decode(r, validate=True)
61+
except B64Error:
62+
if errors == "strict":
63+
raise getattr(builtins, f"{exc_name(name)}DecodeError")(f"'{name}' codec can't decode: incorrect "
64+
"padding")
65+
# otherwise, the padding is fixed, dropping an incomplete last character
66+
r = r.replace("=", "")
67+
r = r[:len(r) - (len(r) % 4 == 1)]
68+
r = b64decode(r + "=" * (-len(r) % 4))
69+
return js_unescape(r.decode("latin-1")), len(text)
70+
71+
return encode, decode

‎src/codext/crypto/atom128.py‎

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,8 @@
1414
1515
Reference: https://github.com/gungunfebrianza/Mastering-Cryptography/blob/master/ATOM-128/ATOM-128.js
1616
"""
17-
import re
18-
from base64 import b64decode, b64encode
19-
from binascii import Error as B64Error
20-
from string import ascii_letters, ascii_lowercase as LC, ascii_uppercase as UC, digits
21-
22-
from ..__common__ import *
17+
from ._jsbase64 import js_base64
18+
from ..__common__ import add
2319

2420

2521
__examples__ = {
@@ -30,55 +26,5 @@
3026
__guess__ = ["atom128"]
3127

3228

33-
B64 = UC + LC + digits + "+/="
34-
ATOM128 = "/128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC"
35-
# characters left as is by JavaScript's escape()
36-
_SAFE = set(ascii_letters + digits + "@*_+-./")
37-
38-
39-
def js_escape(text):
40-
""" Escape a string like JavaScript's escape() (characters beyond the BMP being escaped as surrogate pairs). """
41-
r = ""
42-
for c in text:
43-
if c in _SAFE:
44-
r += c
45-
elif ord(c) < 256:
46-
r += f"%{ord(c):02X}"
47-
else:
48-
u = c.encode("utf-16-be", "surrogatepass")
49-
r += "".join(f"%u{u[i:i+2].hex().upper()}" for i in range(0, len(u), 2))
50-
return r
51-
52-
53-
def js_unescape(text):
54-
""" Unescape a string like JavaScript's unescape() (surrogate pairs being joined). """
55-
s = re.sub(r"%u([0-9a-fA-F]{4})|%([0-9a-fA-F]{2})", lambda m: chr(int(m.group(1) or m.group(2), 16)), text)
56-
return s.encode("utf-16-be", "surrogatepass").decode("utf-16-be", "replace")
57-
58-
59-
def atom128_encode(text, errors="strict"):
60-
r = b64encode(js_escape(ensure_str(text)).encode()).decode()
61-
return r.translate(str.maketrans(B64, ATOM128)), len(text)
62-
63-
64-
def atom128_decode(text, errors="strict"):
65-
_h, t, r = handle_error("atom128", errors, decode=True), re.sub(r"\s", "", ensure_str(text)), ""
66-
for i, c in enumerate(t):
67-
if c in ATOM128:
68-
r += B64[ATOM128.index(c)]
69-
else:
70-
# invalid characters are dropped (as the original JavaScript does) when not in strict mode
71-
_h(c, i)
72-
try:
73-
r = b64decode(r, validate=True)
74-
except B64Error:
75-
if errors == "strict":
76-
raise Atom128DecodeError("'atom128' codec can't decode: incorrect padding") # noqa: F821
77-
# otherwise, the padding is fixed, dropping an incomplete last character
78-
r = r.replace("=", "")
79-
r = r[:len(r) - (len(r) % 4 == 1)]
80-
r = b64decode(r + "=" * (-len(r) % 4))
81-
return js_unescape(r.decode("latin-1")), len(text)
82-
83-
84-
add("atom128", atom128_encode, atom128_decode, r"^atom[-_]?128$", printables_rate=1., expansion_factor=(1.6, .3))
29+
add("atom128", *js_base64("atom128", "/128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC"),
30+
r"^atom[-_]?128$", printables_rate=1., expansion_factor=(1.6, .3))

‎src/codext/crypto/esab46.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# -*- coding: UTF-8 -*-
2+
"""ESAB-46 Codec - ESAB-46 content encoding.
3+
4+
ESAB-46 ("Base-64" reversed) is a toy cipher found on online JavaScript encryption tools. It is Base64 with a shuffled
5+
alphabet, whose 65th character ("/") is used for padding. As in the original JavaScript, the text is first escaped with
6+
JavaScript's escape() (e.g. " " becoming "%20" and "é" becoming "%E9"), and unescaped after decoding ; ciphertexts made
7+
without this escaping (i.e. from the UTF-8 bytes of the text) are then decoded the same way.
8+
9+
This codec:
10+
- en/decodes strings from str to str
11+
- en/decodes strings from bytes to bytes
12+
- decodes file content to str (read)
13+
- encodes file content from str to bytes (write)
14+
15+
Reference: https://crypo.tonylao.com/esab46c.htm
16+
"""
17+
from ._jsbase64 import js_base64
18+
from ..__common__ import add
19+
20+
21+
__examples__ = {
22+
'enc(esab46|esab-46|esb46|esb_46)': {'this is a test': "9sRZ8i0ivsVj6z5gLy0iv4xV8nw/", 'test': "9s1j9A//", '': ""},
23+
'dec(esab46)': {'9s1j9A//': "test", '9s1j 9A//': "test", '9s1j9A/': None, '9s1j9A/%': None},
24+
'enc-dec(esab46)': ["this is a test", "Hello, World! 1234", "\xe9t\xe9 ☃ \U0001F600", "100% sure", "a"],
25+
}
26+
__guess__ = ["esab46"]
27+
28+
29+
add("esab46", *js_base64("esab46", "ABCDqrs456tuvNOPwxyz012KLM3789=+QRSTUVWXYZabcdefghijklmnopEFGHIJ/"),
30+
r"^esa?b[-_]?46$", printables_rate=1., expansion_factor=(1.6, .3))

0 commit comments

Comments
 (0)