Skip to content

Commit e61eacd

Browse files
committed
Added codecs: arabica2rs, chinzo72c, japooc2s, zong22
1 parent 88df782 commit e61eacd

10 files changed

Lines changed: 442 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
354354
- [X] `adfgx`: aka ADFGX Cipher (Polybius square and columnar transposition) ; `adfgvx` for its 6x6 variant
355355
- [X] `aer256`: aka AER-256 (toy cipher with float arithmetic, from online JavaScript tools)
356356
- [X] `affine`: aka Affine Cipher
357+
- [X] `arabica-2rs`: aka ARABICA-2RS (URI-encoded text substituted by Arabic letters, from online JavaScript tools)
357358
- [X] `armon64`: aka ARMON-64 (variant of AER-256 with a fourth operation)
358359
- [X] `atbash`: aka Atbash Cipher
359360
- [X] `atom128`: aka ATOM-128 (Base64 with a shuffled alphabet, from online JavaScript tools)
@@ -364,6 +365,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
364365
- [X] `beaufort`: aka Beaufort Cipher (variant of Vigenere Cipher)
365366
- [X] `bifid`: aka Bifid Cipher (Delastelle's fractionation with a 5x5 Polybius square) ; `trifid` for its 3x3x3 cube variant
366367
- [X] `chaocipher`: aka Chaocipher (two alphabets permuted after each letter)
368+
- [X] `chinzo-72c`: aka CHINZO-72C (URI-encoded text substituted by Chinese characters, from online JavaScript tools)
367369
- [X] `cisco-type7`: aka Cisco IOS type 7 password encoding
368370
- [X] `citrix`: aka Citrix CTX1 password encoding
369371
- [X] `columnar`: aka Columnar Transposition Cipher (keyword-ordered columns ; a second keyword gives the double transposition)
@@ -376,6 +378,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
376378
- [X] `gila7`: aka GILA7 (Base64 with a shuffled alphabet, from online JavaScript tools)
377379
- [X] `gronsfeld`: aka Gronsfeld Cipher (variant of Vigenere Cipher with a numeric key)
378380
- [X] `hill`: aka Hill Cipher (blocks of letters multiplied by a key matrix modulo 26)
381+
- [X] `japoo-c2s`: aka JAPOO-C2S (URI-encoded text substituted by Chinese and Japanese characters, from online JavaScript tools)
379382
- [X] `nihilist`: aka Nihilist Cipher (sums of the Polybius coordinates of the text and of a repeated key)
380383
- [X] `playfair`: aka Playfair Cipher
381384
- [X] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)
@@ -393,6 +396,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
393396
- [X] `vigenere`: aka Vigenere Cipher
394397
- [X] `xorN`: XOR with a single byte (*N* belongs to [1,255])
395398
- [X] `zara128`: aka ZARA-128 (variant of AER-256 with additions only and smaller blocks)
399+
- [X] `zong22`: aka ZONG22 (Base64 with a shuffled alphabet, from online JavaScript tools)
396400

397401
> :warning: Crypto functions are of course definitely **NOT** encoding functions ; they are implemented for leveraging the `.encode(...)` API from `codecs`.
398402

‎docs/pages/enc/crypto.md‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ This codec implements the Affine monoalphabetic substitution cipher. It is param
9797

9898
-----
9999

100+
### ARABICA-2RS
101+
102+
This toy cipher comes from online JavaScript encryption tools. The text is encoded like JavaScript's `encodeURIComponent()` does (e.g. a whitespace becomes `%20`), then each of its characters is substituted by an Arabic letter (or a similar character), the ciphertext ending with `ش`. The original JavaScript inserts random spaces, which are ignored when decoding ; this codec gives a ciphertext without spaces.
103+
104+
**Codec** | **Conversions** | **Aliases** | **Comment**
105+
:---: | :---: | --- | ---
106+
`arabica-2rs` | text <-> ARABICA-2RS ciphertext | `arabica2rs`, `arabica_2rs` | the left-to-right marks around `ݣ` are optional when decoding
107+
108+
```python
109+
>>> codext.encode("this is a test", "arabica-2rs")
110+
'حرقطﺿأﺹقطﺿأﺹغﺿأﺹحبطحش'
111+
>>> codext.decode("حرقطﺿأﺹقطﺿأﺹغﺿأﺹحبطحش", "arabica2rs")
112+
'this is a test'
113+
```
114+
115+
-----
116+
100117
### ARMON-64
101118

102119
This toy cipher, a variant of [AER-256](#aer-256), comes from online JavaScript encryption tools. It works the same way, but with a fourth operation (the number is multiplied by 0.01 times the code of the key character) and with `+` as the separator.
@@ -298,6 +315,27 @@ The Chaocipher was invented by John F. Byrne in 1918 and only disclosed in 2010.
298315

299316
-----
300317

318+
### CHINZO-72C
319+
320+
This toy cipher comes from online JavaScript encryption tools. The pairs of letters `oo`, `ii`, `ee`, `tt` and `rr` are first replaced by Chinese characters, then the text is encoded like JavaScript's `encodeURIComponent()` does and each of its characters is substituted by a Chinese character followed by a space, the ciphertext ending with `吴`.
321+
322+
**Codec** | **Conversions** | **Aliases** | **Comment**
323+
:---: | :---: | --- | ---
324+
`chinzo-72c` | text <-> CHINZO-72C ciphertext | `chinzo72c`, `chinzo_72c` | spaces ignored when decoding
325+
326+
!!! warning "Pairs of letters"
327+
328+
As decoding gives back the pairs of letters, a text holding one of the 5 Chinese characters replacing them (`讲`, `邻`, `头`, `会` and `歷`) is not decoded as it was.
329+
330+
```python
331+
>>> codext.encode("this is a test", "chinzo-72c")
332+
'岛 拉 玛 夫 题 防 呒 玛 夫 题 防 呒 法 题 防 呒 岛 伐 夫 岛 吴'
333+
>>> codext.decode("岛 拉 玛 夫 题 防 呒 玛 夫 题 防 呒 法 题 防 呒 岛 伐 夫 岛 吴", "chinzo72c")
334+
'this is a test'
335+
```
336+
337+
-----
338+
301339
### Cisco Type 7
302340

303341
This implements the reversible Cisco IOS type 7 password encoding: each byte is XORed with a fixed key, starting at an offset given by the salt (from 0 to 15), and the result is the salt as two decimal digits followed by the XORed bytes in hexadecimal. Decoding reads the salt from the input.
@@ -528,6 +566,23 @@ The letters (A=0, ..., Z=25) are taken in blocks of n, each block being multipli
528566

529567
-----
530568

569+
### JAPOO-C2S
570+
571+
This toy cipher comes from online JavaScript encryption tools. The text is encoded like JavaScript's `encodeURIComponent()` does, then each of its characters is substituted by a Chinese or Japanese character followed by a space, some pairs of identical characters (e.g. `貴 貴 `) being then replaced by another character (e.g. `詞 `), and the ciphertext ends with `类`.
572+
573+
**Codec** | **Conversions** | **Aliases** | **Comment**
574+
:---: | :---: | --- | ---
575+
`japoo-c2s` | text <-> JAPOO-C2S ciphertext | `japooc2s`, `japoo_c2s` | spaces ignored when decoding
576+
577+
```python
578+
>>> codext.encode("this is a test", "japoo-c2s")
579+
'徒 卵 語 簡 場 誰 貴 語 簡 場 誰 貴 是 場 誰 貴 徒 或 簡 徒 类'
580+
>>> codext.decode("徒 卵 語 簡 場 誰 貴 語 簡 場 誰 貴 是 場 誰 貴 徒 或 簡 徒 类", "japooc2s")
581+
'this is a test'
582+
```
583+
584+
-----
585+
531586
### Nihilist Cipher
532587

533588
Each letter of the text and of the key is replaced by its coordinates (row and column, from 1 to 5) in a 5x5 Polybius square (I and J sharing a cell), read as a two-digit number. The ciphertext is made of the sums of the numbers of the text and of the repeated key, separated by spaces. The square is filled with an optional keyword (without duplicates), then with the remaining letters of the alphabet. When encoding, whitespaces are ignored ; when decoding, the numbers are separated by whitespaces.
@@ -836,3 +891,20 @@ This toy cipher, a variant of [AER-256](#aer-256), comes from online JavaScript
836891
>>> codext.decode("428 413 427 428", "zara128-016359")
837892
'test'
838893
```
894+
895+
-----
896+
897+
### ZONG22
898+
899+
This toy cipher comes from online JavaScript encryption tools. It is Base64 with a shuffled alphabet, whose 65th character (`2`) is used for padding. As in the original JavaScript, the text is first escaped with JavaScript's `escape()` (e.g. a whitespace becomes `%20`), and unescaped after decoding ; ciphertexts made from the UTF-8 bytes of the text (without escaping) are decoded as well.
900+
901+
**Codec** | **Conversions** | **Aliases** | **Comment**
902+
:---: | :---: | --- | ---
903+
`zong22` | text <-> ZONG22 ciphertext | `zong-22`, `zong_22` |
904+
905+
```python
906+
>>> codext.encode("this is a test", "zong22")
907+
'PyglpexeXyNFwd0QIbxeXfYNpz52'
908+
>>> codext.decode("PyglpexeXyNFwd0QIbxeXfYNpz52", "zong-22")
909+
'this is a test'
910+
```

‎src/codext/crypto/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .adfgx import *
33
from .aer256 import *
44
from .affine import *
5+
from .arabica2rs import *
56
from .armon64 import *
67
from .atbash import *
78
from .atom128 import *
@@ -10,6 +11,7 @@
1011
from .bazeries import *
1112
from .bifid import *
1213
from .chaocipher import *
14+
from .chinzo72c import *
1315
from .cisco import *
1416
from .citrix import *
1517
from .columnar import *
@@ -21,6 +23,7 @@
2123
from .fractionatedmorse import *
2224
from .gila7 import *
2325
from .hill import *
26+
from .japooc2s import *
2427
from .nihilist import *
2528
from .playfair import *
2629
from .phillips import *
@@ -36,3 +39,4 @@
3639
from .vigenere import *
3740
from .xor import *
3841
from .zara128 import *
42+
from .zong22 import *

‎src/codext/crypto/_jsbase64.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# -*- coding: UTF-8 -*-
22
"""Base64 variants, shared by the toy ciphers of online JavaScript encryption tools (ATOM-128, ESAB-46, FERON-74,
3-
GILA7).
3+
GILA7, ZONG22).
44
55
These ciphers are Base64 with a shuffled alphabet of 65 characters, whose last one is used for padding. As in the
66
original JavaScript, the text is first escaped with JavaScript's escape() (e.g. " " becoming "%20" and "é" becoming
77
"%E9"), and unescaped after decoding ; ciphertexts made without this escaping (i.e. from the UTF-8 bytes of the text)
8-
are then decoded the same way.
8+
are decoded as well, except that an ASCII text holding escape sequences (e.g. "%41") is then unescaped.
99
"""
1010
import builtins
1111
import re
@@ -67,6 +67,13 @@ def decode(text, errors="strict"):
6767
r = r.replace("=", "")
6868
r = r[:len(r) - (len(r) % 4 == 1)]
6969
r = b64decode(r + "=" * (-len(r) % 4))
70+
# escape() only gives ASCII characters, hence non-ASCII bytes come from a ciphertext made from the UTF-8 bytes
71+
# of the text, that is not to be unescaped
72+
if not r.isascii():
73+
try:
74+
return r.decode("utf-8"), len(text)
75+
except UnicodeDecodeError:
76+
pass
7077
return js_unescape(r.decode("latin-1")), len(text)
7178

7279
return encode, decode

‎src/codext/crypto/_jsuri.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# -*- coding: UTF-8 -*-
2+
"""URI encoding, shared by the toy ciphers of online JavaScript encryption tools that substitute the characters of the
3+
text encoded with JavaScript's encodeURIComponent() (ARABICA-2RS, CHINZO-72C, JAPOO-C2S).
4+
5+
The text is encoded like encodeURIComponent() does (the UTF-8 bytes of the characters other than the letters, digits
6+
and "-_.!~*'()" being written as "%XX"), hence it is made of 72 distinct characters, each one being substituted by a
7+
character (or a short string) of the cipher ; decoding substitutes them back and decodes the text like
8+
decodeURIComponent() does, malformed escape sequences and invalid UTF-8 being reported (or, with a non-strict errors
9+
handling, replaced).
10+
"""
11+
import builtins
12+
import re
13+
from string import ascii_letters, digits
14+
from urllib.parse import quote, unquote
15+
16+
from ..__common__ import ensure_str, exc_name, handle_error
17+
18+
19+
# characters left as is by encodeURIComponent(), and the characters of its output
20+
URI_SAFE = "-_.!~*'()"
21+
URI_CHARS = ascii_letters + digits + URI_SAFE + "%"
22+
23+
24+
def uri_encode(text):
25+
""" Encode a string like JavaScript's encodeURIComponent(). """
26+
return quote(ensure_str(text), safe=URI_SAFE)
27+
28+
29+
def uri_decode(text, ename, errors="strict"):
30+
""" Decode a string like JavaScript's decodeURIComponent(), with the given errors handling. """
31+
if errors == "strict":
32+
if (m := re.search(r"%(?![0-9A-Fa-f]{2})", text)):
33+
handle_error(ename, errors, decode=True, kind="escape sequence")(text[m.start():m.start()+3], m.start())
34+
try:
35+
return unquote(text, errors="strict")
36+
except UnicodeDecodeError as e:
37+
exc = getattr(builtins, f"{exc_name(ename)}DecodeError")
38+
raise exc(f"'{ename}' codec can't decode: invalid UTF-8 ({e.reason})") from None
39+
# with a non-strict errors handling, malformed escape sequences are kept as is and invalid UTF-8 is replaced
40+
return unquote(text, errors="replace")
41+
42+
43+
def substitution(ename, table, decode_table=None):
44+
""" Make the encoding and decoding functions of a cipher substituting the characters of the URI-encoded text, given
45+
the substitutes of these characters. """
46+
dtable = decode_table or {v: k for k, v in table.items()}
47+
48+
def encode(text):
49+
return [table[c] for c in uri_encode(text)]
50+
51+
def decode(tokens, errors="strict"):
52+
_h = handle_error(ename, errors, decode=True)
53+
return uri_decode("".join(dtable[t] if t in dtable else _h(t, i) for i, t in enumerate(tokens)), ename, errors)
54+
55+
return encode, decode

‎src/codext/crypto/arabica2rs.py‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# -*- coding: UTF-8 -*-
2+
"""ARABICA-2RS Codec - ARABICA-2RS content encryption.
3+
4+
ARABICA-2RS is a toy cipher found on online JavaScript encryption tools. The text is encoded like JavaScript's
5+
encodeURIComponent() does, then each of its characters (written as 5 digits in base 3 by the original JavaScript) is
6+
substituted by an Arabic letter (or a similar character), the ciphertext ending with "ش". The original JavaScript
7+
reverses the text before and after the substitution (which cancel each other) and inserts random spaces, which are
8+
ignored when decoding ; this codec gives a ciphertext without spaces, and encodes a text holding "ش" (that the
9+
original JavaScript leaves as is). When decoding, the left-to-right marks around "ݣ" (easily lost) are optional.
10+
11+
This codec:
12+
- en/decodes strings from str to str
13+
- en/decodes strings from bytes to bytes
14+
- decodes file content to str (read)
15+
- encodes file content from str to bytes (write)
16+
17+
Reference: https://github.com/gungunfebrianza/Mastering-Cryptography/blob/master/ARABICA2RS/arabperkasaahaha.js
18+
"""
19+
import re
20+
21+
from ._jsuri import substitution
22+
from ..__common__ import *
23+
24+
25+
__examples__ = {
26+
'enc(arabica-2rs|arabica2rs|arabica_2rs)': {
27+
'test': '\u062d\u0628\u0637\u062d\u0634',
28+
'Hello, World!':
29+
'\u06f1\u0628\u0639\u0639\u0645\ufebf\u0623\u06cc\ufebf\u0623\ufeb9\ufe9b\u0645\u064a\u0639\u062e\ufb90'
30+
'\u0634',
31+
'5': '\u200e\u0763\u200e\u0634',
32+
'': '',
33+
},
34+
'dec(arabica-2rs)': {
35+
'\u062d\u0628\u0637\u062d\u0634': 'test',
36+
'\u06f1\u0628 \u0639 \u0639\u0645 \ufebf\u0623 \u06cc\ufebf\u0623 \ufeb9 \ufe9b\u0645\u064a\u0639\u062e\ufb90'
37+
'\u0634':
38+
'Hello, World!',
39+
'\u0763\u0634': '5',
40+
'\u062d\u0628X\u0634': None,
41+
'\ufebf\u0634': None,
42+
},
43+
'enc-dec(arabica-2rs)': ['', 'test', 'Hello, World!', 'caf\xe9 \u20ac \U0001f600 \u0634', "0123456789-_.!~*'()%"],
44+
}
45+
__guess__ = ["arabica-2rs"]
46+
47+
48+
# substitutes of the characters of the URI-encoded text, given by their codes in base 3 (as in the original JavaScript)
49+
ARABICA = [
50+
("10121", "غ"), ("10122", "ظ"), ("10200", "ذ"), ("10201", "خ"), ("10202", "ب"), ("10210", "ت"),
51+
("10211", "س"), ("10212", "ر"), ("10220", "ق"), ("10221", "ض"), ("10222", "ف"), ("11000", "ع"),
52+
("11001", "ص"), ("11002", "ن"), ("11010", "م"), ("11011", "ل"), ("11012", "ك"), ("11020", "ي"),
53+
("11021", "ط"), ("11022", "ح"), ("11100", "ز"), ("11101", "و"), ("11102", "ه"), ("11110", "د"),
54+
("11111", "ج"), ("11112", "ﺵ"), ("01210", "ﺹ"), ("01211", "ﻃ"), ("01212", "أ"), ("01220", "ا"),
55+
("01221", "ـ"), ("01222", "\u200eݣ\u200e"), ("02000", "چ"), ("02001", "گ"), ("02002", "آ"), ("02010", "ة"),
56+
("02102", "ى"), ("02110", "ک"), ("02111", "ی"), ("02112", "پ"), ("02120", "۴"), ("02121", "۸"),
57+
("02122", "۶"), ("02200", "۱"), ("02201", "ﭻ"), ("02202", "ئ"), ("02210", "ە"), ("02211", "ﻼ"),
58+
("02212", "ﻻ"), ("02220", "\u0651"), ("02221", "\u0650"), ("02222", "ژ"), ("10000", "ڨ"), ("10001", "ڜ"),
59+
("10002", "ﷲ"), ("10010", "ﺑ"), ("10011", "ﭘ"), ("10012", "ﺗ"), ("10020", "ﺛ"), ("10021", "ﺟ"),
60+
("10022", "ﺣ"), ("10100", "ﺡ"), ("01101", "ﺿ"), ("01111", "ﻏ"), ("01112", "ﻓ"), ("01200", "ﻑ"),
61+
("01120", "ﻕ"), ("01020", "ﮐ"), ("10112", "ﻝ"), ("01201", "ﻡ"), ("01110", "ﻥ"), ("11200", "ﻳ"),
62+
]
63+
_TABLE = {chr(int(code, 3)): s for code, s in ARABICA}
64+
_encode, _decode = substitution("arabica-2rs", _TABLE, {s.strip("\u200e"): c for c, s in _TABLE.items()})
65+
66+
67+
def arabica_encode(text, errors="strict"):
68+
t = ensure_str(text)
69+
return "".join(_encode(t)) + "\u0634" if t else "", len(text)
70+
71+
72+
def arabica_decode(text, errors="strict"):
73+
t = re.sub(r"[\s\u200e]+", "", ensure_str(text)).replace("\u0634", "")
74+
return _decode(t, errors), len(text)
75+
76+
77+
add("arabica-2rs", arabica_encode, arabica_decode, r"^arabica[-_]?2rs$", examples=__examples__, guess=__guess__)

0 commit comments

Comments
 (0)