Skip to content

Commit dd4e923

Browse files
authored
Merge branch 'main' into copilot/add-playfair-encoding
2 parents a27258b + 3ffc107 commit dd4e923

23 files changed

Lines changed: 618 additions & 206 deletions

File tree

.github/workflows/python-package.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ jobs:
1818
runs-on: ubuntu-latest
1919
outputs:
2020
package: ${{ steps.pkg.outputs.package }}
21+
pypi_url: ${{ steps.pkg.outputs.pypi_url }}
2122
steps:
2223
- name: Compute package name from the repository's
2324
id: pkg
2425
run: |
2526
name="${GITHUB_REPOSITORY##*/}"
2627
echo "package=${name#python-}" >> $GITHUB_OUTPUT
28+
echo "pypi_url=https://pypi.org/p/${name#python-}" >> $GITHUB_OUTPUT
2729
build:
2830
needs: prepare
2931
runs-on: ${{ matrix.os }}
@@ -63,7 +65,7 @@ jobs:
6365
- uses: actions/checkout@v5
6466
with:
6567
fetch-depth: 0
66-
ref: ${{ github.head_ref || github.ref_name }}
68+
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.pull_request.head.sha || github.sha }}
6769
- name: Set up Python ${{ env.python_version }}
6870
uses: actions/setup-python@v6
6971
with:
@@ -96,6 +98,9 @@ jobs:
9698
git commit -m "Update coverage badge"
9799
git push origin coverage-badge --force
98100
deploy:
101+
environment:
102+
name: pypi
103+
url: ${{ needs.prepare.outputs.pypi_url }}
99104
runs-on: ubuntu-latest
100105
needs: [prepare, coverage]
101106
steps:

README.md

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ $ echo -en "test" | codext encode base100
4343
👫👜👪👫
4444
```
4545

46-
### Chaining codecs
46+
### :chains: Chaining codecs
4747

4848
```sh
4949
$ echo -en "Test string" | codext encode reverse
@@ -62,7 +62,7 @@ $ echo -en "AGTCAGTCAGTGAGAAAGTCAGTGAGAAAGTGAGTGAGAAAGTGAGTCAGTGAGAAAGTCAGAAAGTG
6262
test string
6363
```
6464

65-
### Using macros
65+
### :twisted_rightwards_arrows: Using macros
6666

6767
```sh
6868
$ codext add-macro my-encoding-chain gzip base63 lzma base64
@@ -79,7 +79,9 @@ $ codext list macros
7979
example-macro
8080
```
8181

82-
## :computer: Usage (base CLI tool) <a href="https://twitter.com/intent/tweet?text=UnBase%20-%20Decode%20any%20multi-layer%20base-encoded%20string.%0D%0APython%20tool%20for%20decoding%20any%20base-encoded%20string,%20even%20when%20encoded%20with%20multiple%20layers.%0D%0Ahttps%3a%2f%2fgithub%2ecom%2fdhondta%2fpython-codext%0D%0A&hashtags=python,base,encodings,codecs,cryptography,stegano,steganography,ctftools"><img src="https://img.shields.io/badge/Tweet%20(unbase)--lightgrey?logo=twitter&style=social" alt="Tweet on unbase" height="20"/></a>
82+
## :desktop_computer: Usage (`baseXX` CLI tools) <a href="https://twitter.com/intent/tweet?text=UnBase%20-%20Decode%20any%20multi-layer%20base-encoded%20string.%0D%0APython%20tool%20for%20decoding%20any%20base-encoded%20string,%20even%20when%20encoded%20with%20multiple%20layers.%0D%0Ahttps%3a%2f%2fgithub%2ecom%2fdhondta%2fpython-codext%0D%0A&hashtags=python,base,encodings,codecs,cryptography,stegano,steganography,ctftools"><img src="https://img.shields.io/badge/Tweet%20(unbase)--lightgrey?logo=twitter&style=social" alt="Tweet on unbase" height="20"/></a>
83+
84+
Playing with base encodings.
8385

8486
```session
8587
$ echo "Test string !" | base122
@@ -106,16 +108,73 @@ $ echo "Test string !" | base91 | base85 | base36 | base58-flickr | unbase -f Te
106108
Test string !
107109
```
108110

109-
## :computer: Usage (Python)
111+
## :computer: Usage (CLI)
112+
113+
Listing codecs.
114+
115+
```session
116+
$ codext list encodings
117+
a1z26 adler32 affine alternative-rot ascii
118+
atbash autoclave bacon barbie base
119+
base1 base2 base3 base4 base8
120+
<<snipped>>
121+
```
122+
123+
Finding a codec based on a name.
124+
125+
```session
126+
$ codext search bitcoin
127+
base58
128+
```
129+
130+
Encoding a string.
110131

111-
Getting the list of available codecs:
132+
```sesssion
133+
$ echo -en "This is a test" | codext encode polybius
134+
44232443 2443 11 44154344
135+
```
136+
137+
Encoding a file.
138+
139+
```session
140+
$ echo -en "this is a test" > to_be_encoded.txt
141+
$ codext encode base64 < to_be_encoded.txt > text.b64
142+
$ cat text.b64
143+
dGhpcyBpcyBhIHRlc3Q=
144+
```
145+
146+
Chaining codecs.
147+
148+
```session
149+
$ echo -en "mrdvm6teie6t2cq=" | codext encode upper | codext decode base32 | codext decode base64
150+
test
151+
```
152+
153+
Iteratively guessing decodings.
154+
155+
```session
156+
$ echo -en "test" | codext encode base64 gzip | codext guess
157+
Codecs: gzip
158+
dGVzdA==
159+
$ echo -en "test" | codext encode base64 gzip | codext guess gzip -i base
160+
Codecs: gzip, base64
161+
test
162+
```
163+
164+
165+
## :snake: Usage (Python)
166+
167+
Getting the list of available codecs.
112168

113169
```python
114170
>>> import codext
115171

116172
>>> codext.list()
117173
['ascii85', 'base85', 'base100', 'base122', ..., 'tomtom', 'dna', 'html', 'markdown', 'url', 'resistor', 'sms', 'whitespace', 'whitespace-after-before']
118174

175+
Playing with some base encodings.
176+
177+
```python
119178
>>> codext.encode("this is a test", "base58-bitcoin")
120179
'jo91waLQA1NNeBmZKUF'
121180

@@ -130,7 +189,21 @@ Getting the list of available codecs:
130189

131190
>>> codecs.decode("👫👟👠👪🐗👠👪🐗👘🐗👫👜👪👫", "base100")
132191
'this is a test'
192+
```
193+
194+
Playing with some cryptography-based codecs.
133195

196+
```python
197+
>>> codext.encode("This is a test !", "vigenere-MYSECRETKET")
198+
'Ffaw kj e mowm !'
199+
200+
>>> codext.encode("This is a test !", "autoclave-SECRET")
201+
'Llkj ml t amkb !'
202+
```
203+
204+
Encoding/decoding with various other codecs.
205+
206+
```python
134207
>>> for i in range(8):
135208
print(codext.encode("this is a test", "dna-%d" % (i + 1)))
136209
GTGAGCCAGCCGGTATACAAGCCGGTATACAAGCAGACAAGTGAGCGGGTATGTGA
@@ -158,30 +231,6 @@ CACTCGGTCGGCCATATGTTCGGCCATATGTTCGTCTGTTCACTCGCCCATACACT
158231
f.read()
159232
'this is a test'
160233

161-
>>> codext.decode("""
162-
=
163-
X
164-
:
165-
x
166-
n
167-
r
168-
y
169-
Y
170-
y
171-
p
172-
a
173-
`
174-
n
175-
|
176-
a
177-
o
178-
h
179-
`
180-
g
181-
o
182-
z """, "whitespace-after+before")
183-
'CSC{not_so_invisible}'
184-
185234
>>> print(codext.encode("An example test string", "baudot-tape"))
186235
***.**
187236
. *
@@ -282,15 +331,21 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
282331

283332
- [X] `affine`: aka Affine Cipher
284333
- [X] `atbash`: aka Atbash Cipher
334+
- [X] `autoclave`: aka Autoclave/Autokey Cipher (variant of Vigenere Cipher)
285335
- [X] `bacon`: aka Baconian Cipher
286336
- [X] `barbie-N`: aka Barbie Typewriter (*N* belongs to [1, 4])
337+
- [X] `beaufort`: aka Beaufort Cipher (variant of Vigenere Cipher)
287338
- [X] `citrix`: aka Citrix CTX1 password encoding
288339
- [X] `playfair`: aka Playfair Cipher
340+
- [X] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)
289341
- [X] `polybius`: aka Polybius Square Cipher
290342
- [X] `railfence`: aka Rail Fence Cipher
291343
- [X] `rotN`: aka Caesar cipher (*N* belongs to [1,25])
292344
- [X] `scytaleN`: encrypts using the number of letters on the rod (*N* belongs to [1,[)
293345
- [X] `shiftN`: shift ordinals (*N* belongs to [1,255])
346+
- [X] `trithemius`: aka Trithemius Cipher (variant of Vigenere Cipher)
347+
- [X] `vic`: aka VIC Cipher
348+
- [X] `vigenere`: aka Vigenere Cipher
294349
- [X] `xorN`: XOR with a single byte (*N* belongs to [1,255])
295350

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

docs/pages/enc/crypto.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ It implements the monoalphabetic substitution cipher used for the Hebrew alphabe
7171

7272
-----
7373

74+
### Autoclave/Autokey Cipher
75+
76+
This is a variant of the [Vigenere Cipher](#vigenere-cipher) using a key stream generated from the primer key and the message appended.
77+
78+
**Codec** | **Conversions** | **Aliases** | **Comment**
79+
:---: | :---: | --- | ---
80+
`autoclave` | text <-> Autoclave ciphertext | `autoclave-cipher`, `autokey` |
81+
82+
```python
83+
>>> codext.encode("This is a test !", "autoclave-test")
84+
'Mlal bz i lmkt !'
85+
>>> codext.decode("Mlal bz i lmkt !", "autokey_cipher-test")
86+
'This is a test !'
87+
```
88+
89+
-----
90+
7491
### Baconian Cipher
7592

7693
It support only letters.
@@ -109,6 +126,25 @@ It implements the cipher for its 4 different keys.
109126

110127
-----
111128

129+
### Beaufort Cipher
130+
131+
This is a variant of the [Vigenere Cipher](#vigenere-cipher). There is no default key, meaning that `beaufort` as the encoding scheme throws a `LookupError` indicating that the _key must be a non-empty alphabetic string_.
132+
133+
**Codec** | **Conversions** | **Aliases** | **Comment**
134+
:---: | :---: | --- | ---
135+
`beaufort` | text <-> Beaufort ciphertext | `beaufort-abcdef`, `beaufort_MySuperSecret` | key only consists of characters, not digits
136+
137+
```python
138+
>>> codext.encode("This is a test !", "beaufort-abababa")
139+
'Husj sj a hxii !'
140+
>>> codext.encode("This is a test !", "beaufort_MySuperSecret")
141+
'Trkc hm r zaky !'
142+
>>> codext.decode("Husj sj a hxii !", "vigenere-abababa")
143+
'This is a test !'
144+
```
145+
146+
-----
147+
112148
### Citrix CTX1
113149

114150
This implements the Citrix CTX1 password encoding algorithm.
@@ -126,6 +162,25 @@ This implements the Citrix CTX1 password encoding algorithm.
126162

127163
-----
128164

165+
### Phillips Cipher
166+
167+
This implements Phillips cipher, a polyalphabetic code using 8 grids generated with one keyword.
168+
169+
**Codec** | **Conversions** | **Aliases** | **Comment**
170+
:---: | :---: | --- | ---
171+
`phillips` | text <-> phillips ciphertext | `phillips-key`, `phillips_password`, ... |
172+
173+
```python
174+
>>> codext.encode("this is a test", "phillips_mysuperkey")
175+
'ZCNM NM E XKMVZ'
176+
>>> codext.encode("Another Test String", "phillips-PaSsWoRd")
177+
'SMEZKBE LBON OLEHQHV'
178+
>>> codext.decode("SMEZKBE LBON OLEHQHV", "phillips-password")
179+
'ANOTHER TEST STRINGX'
180+
```
181+
182+
-----
183+
129184
### Polybius Square Cipher
130185

131186
This implements the well-known Polybius Square cipher, using the square with the alphabet in normal order as the default. It can be used dynamically with a custom alphabet.

src/codext/VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.15.11
1+
1.16.3

src/codext/__common__.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: UTF-8 -*-
22
import _codecs
3+
import builtins
34
import codecs
45
import hashlib
56
import json
@@ -20,22 +21,6 @@
2021
from random import randint
2122
from string import *
2223
from types import FunctionType, ModuleType
23-
try: # Python2
24-
import __builtin__ as builtins
25-
except ImportError:
26-
import builtins
27-
try: # Python2
28-
from inspect import getfullargspec
29-
except ImportError:
30-
from inspect import getargspec as getfullargspec
31-
try: # Python2
32-
from string import maketrans
33-
except ImportError:
34-
maketrans = str.maketrans
35-
try: # Python3
36-
from importlib import reload
37-
except ImportError:
38-
pass
3924
try:
4025
import re._parser as sre_parse
4126
except ImportError:
@@ -44,6 +29,8 @@
4429
# from Python 3.11, 'sre_parse' is bound as '_parser' ; monkey-patch it for backward-compatibility
4530
re.sre_parse = sre_parse
4631

32+
maketrans = str.maketrans
33+
4734

4835
__all__ = ["add", "add_macro", "add_map", "b", "clear", "codecs", "decode", "encode", "ensure_str", "examples", "guess",
4936
"isb", "generate_strings_from_regex", "get_alphabet_from_mask", "handle_error", "hashlib", "i2s",
@@ -277,6 +264,7 @@ def getregentry(encoding):
277264
# this occurs while m is not None, but possibly no capture group that gives at least 1 group index ;
278265
# in this case, if fenc/fdec is a decorated function, execute it with no arg
279266
if len(args) == 0:
267+
from inspect import getfullargspec
280268
if fenc and len(getfullargspec(fenc).args) == 1:
281269
fenc = fenc()
282270
if fdec and len(getfullargspec(fdec).args) == 1:
@@ -683,7 +671,7 @@ def list_categories():
683671
c.append(d.rstrip("s"))
684672
# particular category, hardcoded from base/_base.py
685673
c += ["base-generic"]
686-
return c
674+
return list(set(c))
687675
list_categories()
688676

689677

@@ -767,6 +755,7 @@ def remove(name):
767755

768756
def reset():
769757
""" Reset codext's local registry of search functions and macros. """
758+
from importlib import reload
770759
global __codecs_registry, CODECS_REGISTRY, MACROS, PERS_MACROS # noqa: F824
771760
clear()
772761
d = os.path.dirname(__file__)
@@ -1142,9 +1131,8 @@ def generate_string_from_regex(regex):
11421131

11431132
def generate_strings_from_regex(regex, star_plus_max=STAR_PLUS_MAX, repeat_max=REPEAT_MAX, yield_max=YIELD_MAX):
11441133
""" Utility function to generate strings from a regex pattern. """
1145-
i = 0
1146-
for result in __gen_str_from_re(regex, star_plus_max, repeat_max, yield_max):
1147-
yield result
1134+
for r in __gen_str_from_re(regex, star_plus_max, repeat_max, yield_max):
1135+
yield r
11481136

11491137

11501138
# guess feature objects

src/codext/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def _format_action_invocation(self, action):
170170
listi = sparsers.add_parser("list", help="list items")
171171
lsparsers = listi.add_subparsers(dest="type", help="type of item to be listed", required=True)
172172
liste = lsparsers.add_parser("encodings", help="list encodings")
173-
liste.add_argument("category", nargs="+", help="selected categories")
173+
liste.add_argument("category", nargs="*", help="selected categories")
174174
listm = lsparsers.add_parser("macros", help="list macros")
175175
addm = sparsers.add_parser("add-macro", help="add a macro to the registry")
176176
addm.add_argument("name", help="macro's name")
@@ -198,13 +198,13 @@ def _format_action_invocation(self, action):
198198
# list encodings or macros
199199
elif args.command == "list":
200200
if args.type == "encodings":
201-
cats = args.category or list_categories()
202-
for c in sorted(cats):
203-
l = list_encodings(c)
204-
if len(l) > 0:
205-
if len(cats) > 0:
201+
if args.category:
202+
for c in sorted(args.category):
203+
if len(l := list_encodings(c)) > 0:
206204
print(c.upper() + ":")
207-
__print_tabular(l)
205+
__print_tabular(l)
206+
else:
207+
__print_tabular(list_encodings())
208208
elif args.type == "macros":
209209
l = list_macros()
210210
if len(l) > 0:

0 commit comments

Comments
 (0)