From 25862159c4b946ec789ccc02fd8a075fb9b02fc7 Mon Sep 17 00:00:00 2001 From: dhondta Date: Sun, 22 Mar 2026 23:18:31 +0100 Subject: [PATCH 1/4] Added new codec: vigenere --- README.md | 1 + docs/pages/enc/crypto.md | 18 ++++++++++ src/codext/crypto/__init__.py | 1 + src/codext/crypto/vigenere.py | 65 +++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100755 src/codext/crypto/vigenere.py diff --git a/README.md b/README.md index 45f488c..b1d337f 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba - [X] `rotN`: aka Caesar cipher (*N* belongs to [1,25]) - [X] `scytaleN`: encrypts using the number of letters on the rod (*N* belongs to [1,[) - [X] `shiftN`: shift ordinals (*N* belongs to [1,255]) +- [X] `vigenere`: aka Vigenere Cipher - [X] `xorN`: XOR with a single byte (*N* belongs to [1,255]) > :warning: Crypto functions are of course definitely **NOT** encoding functions ; they are implemented for leveraging the `.encode(...)` API from `codecs`. diff --git a/docs/pages/enc/crypto.md b/docs/pages/enc/crypto.md index 71d89e9..432ac3d 100644 --- a/docs/pages/enc/crypto.md +++ b/docs/pages/enc/crypto.md @@ -202,6 +202,24 @@ This is a dynamic encoding, that is, it can be called with an integer to define ----- +### Vigenere Cipher + +This is a dynamic encoding, that is, it holds the key. There is no default key, meaning that `vigenere` as the encoding scheme throws a `LookupError` indicating that the _key must be a non-empty alphabetic string_. + +**Codec** | **Conversions** | **Aliases** | **Comment** +:---: | :---: | --- | --- +`vigenere` | text <-> Vigenere ciphertext | `vigenere-abcdef`, `vigenere_MySuperSecret` | key only consists of characters, not digits + +```python +>>> codext.encode("This is a test !", "vigenere-abababa") +'Tiit it a tfsu !' +>>> codext.encode("This is a test !", "vigenere_MySuperSecret") +'Ffam xw r liuk !' +>>> codext.decode("Tiit it a tfsu !", "vigenere-abababa") +``` + +----- + ### XOR with 1 byte This is a dynamic encoding, that is, it can be called with an integer to define the ordinal of the byte to XOR with the input text. diff --git a/src/codext/crypto/__init__.py b/src/codext/crypto/__init__.py index 0854db2..21da6d9 100644 --- a/src/codext/crypto/__init__.py +++ b/src/codext/crypto/__init__.py @@ -9,5 +9,6 @@ from .rot import * from .scytale import * from .shift import * +from .vigenere import * from .xor import * diff --git a/src/codext/crypto/vigenere.py b/src/codext/crypto/vigenere.py new file mode 100755 index 0000000..39ac4ce --- /dev/null +++ b/src/codext/crypto/vigenere.py @@ -0,0 +1,65 @@ +# -*- coding: UTF-8 -*- +"""Vigenere Cipher Codec - vigenere content encoding. + +This codec: +- en/decodes strings from str to str +- en/decodes strings from bytes to bytes +- decodes file content to str (read) +- encodes file content from str to bytes (write) +""" +from string import ascii_lowercase as LC, ascii_uppercase as UC + +from ..__common__ import * + + +__examples__ = { + 'enc(vigenere)': None, + 'enc(vigenere-lemon)': {'ATTACKATDAWN': 'LXFOPVEFRNHR'}, + 'enc(vigenere-key)': {'hello': 'rijvs'}, + 'enc(vigenère_key)': {'Hello World': 'Rijvs Uyvjn'}, + 'enc-dec(vigenere-secret)': ['hello world', 'ATTACK AT DAWN', 'Test 1234!'], +} +__guess__ = ["vigenere-key", "vigenere-secret", "vigenere-password"] + + +__char = lambda c, k, i, d=False: (LC if (b := c in LC) else UC)[(ord(c) - ord("Aa"[b]) + \ + [1, -1][d] * (ord(k[i % len(k)]) - ord('a'))) % 26] + + +def __check(key): + key = key.lower() + if not key or not key.isalpha(): + raise LookupError("Bad parameter for encoding 'vigenere': key must be a non-empty alphabetic string") + return key + + +def vigenere_encode(key): + def encode(text, errors="strict"): + result, i, k = [], 0, __check(key) + for c in ensure_str(text): + if c in LC or c in UC: + result.append(__char(c, k, i)) + i += 1 + else: + result.append(c) + r = "".join(result) + return r, len(r) + return encode + + +def vigenere_decode(key): + def decode(text, errors="strict"): + result, i, k = [], 0, __check(key) + for c in ensure_str(text): + if c in LC or c in UC: + result.append(__char(c, k, i, True)) + i += 1 + else: + result.append(c) + r = "".join(result) + return r, len(r) + return decode + + +add("vigenere", vigenere_encode, vigenere_decode, r"vigen[eè]re(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$", penalty=.1) + From 351b15f6b378fc53d849dba11706c32f02b08737 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:11:02 +0000 Subject: [PATCH 2/4] Initial plan From 0164479dcc8621a4c400bfd67e316b5b2467825c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:24:54 +0000 Subject: [PATCH 3/4] Add Playfair cipher codec Co-authored-by: dhondta <9108102+dhondta@users.noreply.github.com> Agent-Logs-Url: https://github.com/dhondta/python-codext/sessions/4b40b433-0112-42bf-ad61-b7d0d380bc9c --- src/codext/crypto/__init__.py | 1 + src/codext/crypto/playfair.py | 138 ++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 src/codext/crypto/playfair.py diff --git a/src/codext/crypto/__init__.py b/src/codext/crypto/__init__.py index 21da6d9..00a2c58 100644 --- a/src/codext/crypto/__init__.py +++ b/src/codext/crypto/__init__.py @@ -4,6 +4,7 @@ from .bacon import * from .barbie import * from .citrix import * +from .playfair import * from .polybius import * from .railfence import * from .rot import * diff --git a/src/codext/crypto/playfair.py b/src/codext/crypto/playfair.py new file mode 100644 index 0000000..1194b37 --- /dev/null +++ b/src/codext/crypto/playfair.py @@ -0,0 +1,138 @@ +# -*- coding: UTF-8 -*- +"""Playfair Cipher Codec - playfair content encoding. + +The Playfair cipher is a symmetric encryption method using polygram substitution +with bigrams (pairs of letters), invented in 1854 by Charles Wheatstone, but +popularized by his friend Lord Playfair. + +This codec: +- en/decodes strings from str to str +- en/decodes strings from bytes to bytes +- decodes file content to str (read) +- encodes file content from str to bytes (write) + +Reference: https://www.dcode.fr/playfair-cipher +""" +from ..__common__ import * + + +__examples__ = { + # Classic example from Wikipedia (key "PLAYFAIR EXAMPLE"): + # the EE in "TREE" is split with an X filler during encoding, so decoding + # exposes the filler: "TREESTUMP" → encoded → decoded as "TREXESTUMP" + 'enc(playfair-playfairexample)': {'HIDETHEGOLDINTHETREESTUMP': 'BMODZBXDNABEKUDMUIXMMOUVIF'}, + 'dec(playfair-playfairexample)': {'BMODZBXDNABEKUDMUIXMMOUVIF': 'HIDETHEGOLDINTHETREXESTUMP'}, + 'enc-dec(playfair-keyword)': ['INSTRUMENT'], +} +__guess__ = ["playfair"] + + +# Standard 5×5 Playfair alphabet (I and J share the same cell) +_DEFAULT_ALPHABET = "ABCDEFGHIKLMNOPQRSTUVWXYZ" + + +def _build_grid(key=None): + """Build the 5×5 Playfair grid from an optional keyword.""" + seen, grid = set(), [] + if key: + for c in key.upper(): + if c == 'J': + c = 'I' + if c.isalpha() and c not in seen: + seen.add(c) + grid.append(c) + for c in _DEFAULT_ALPHABET: + if c not in seen: + seen.add(c) + grid.append(c) + pos = {grid[i]: (i // 5, i % 5) for i in range(25)} + return grid, pos + + +def _filler(c): + """Return the filler character for a given letter (X, or Q when the letter is X).""" + return 'Q' if c == 'X' else 'X' + + +def _make_bigrams(text): + """Convert plaintext to bigrams, inserting fillers for repeated-letter pairs.""" + chars = [] + for c in ensure_str(text).upper(): + if c == 'J': + chars.append('I') + elif c.isalpha(): + chars.append(c) + bigrams = [] + i = 0 + while i < len(chars): + a = chars[i] + if i + 1 < len(chars): + b = chars[i + 1] + if a == b: + bigrams.append((a, _filler(a))) + i += 1 + else: + bigrams.append((a, b)) + i += 2 + else: + bigrams.append((a, _filler(a))) + i += 1 + return bigrams + + +def _encode_bigram(grid, pos, a, b): + r_a, c_a = pos[a] + r_b, c_b = pos[b] + if r_a == r_b: + return grid[r_a * 5 + (c_a + 1) % 5], grid[r_b * 5 + (c_b + 1) % 5] + elif c_a == c_b: + return grid[((r_a + 1) % 5) * 5 + c_a], grid[((r_b + 1) % 5) * 5 + c_b] + else: + return grid[r_a * 5 + c_b], grid[r_b * 5 + c_a] + + +def _decode_bigram(grid, pos, a, b): + r_a, c_a = pos[a] + r_b, c_b = pos[b] + if r_a == r_b: + return grid[r_a * 5 + (c_a - 1) % 5], grid[r_b * 5 + (c_b - 1) % 5] + elif c_a == c_b: + return grid[((r_a - 1) % 5) * 5 + c_a], grid[((r_b - 1) % 5) * 5 + c_b] + else: + return grid[r_a * 5 + c_b], grid[r_b * 5 + c_a] + + +def playfair_encode(key=None): + grid, pos = _build_grid(key) + def encode(text, errors="strict"): + t = ensure_str(text) + result = [] + for a, b in _make_bigrams(t): + ea, eb = _encode_bigram(grid, pos, a, b) + result.extend([ea, eb]) + r = "".join(result) + return r, len(t) + return encode + + +def playfair_decode(key=None): + grid, pos = _build_grid(key) + def decode(text, errors="strict"): + t = ensure_str(text) + chars = [] + for c in t.upper(): + if c == 'J': + chars.append('I') + elif c.isalpha(): + chars.append(c) + result = [] + for i in range(0, len(chars) - 1, 2): + da, db = _decode_bigram(grid, pos, chars[i], chars[i + 1]) + result.extend([da, db]) + r = "".join(result) + return r, len(t) + return decode + + +add("playfair", playfair_encode, playfair_decode, r"^playfair(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$", + printables_rate=1.) From a27258b879b1830c64b3bd3affe5df000150ef7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:54:09 +0000 Subject: [PATCH 4/4] Remove Vigenere additions and document Playfair codec --- README.md | 2 +- docs/pages/enc/crypto.md | 18 +++++----- src/codext/crypto/__init__.py | 2 -- src/codext/crypto/vigenere.py | 65 ----------------------------------- 4 files changed, 10 insertions(+), 77 deletions(-) delete mode 100755 src/codext/crypto/vigenere.py diff --git a/README.md b/README.md index b1d337f..fdaea44 100644 --- a/README.md +++ b/README.md @@ -285,12 +285,12 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba - [X] `bacon`: aka Baconian Cipher - [X] `barbie-N`: aka Barbie Typewriter (*N* belongs to [1, 4]) - [X] `citrix`: aka Citrix CTX1 password encoding +- [X] `playfair`: aka Playfair Cipher - [X] `polybius`: aka Polybius Square Cipher - [X] `railfence`: aka Rail Fence Cipher - [X] `rotN`: aka Caesar cipher (*N* belongs to [1,25]) - [X] `scytaleN`: encrypts using the number of letters on the rod (*N* belongs to [1,[) - [X] `shiftN`: shift ordinals (*N* belongs to [1,255]) -- [X] `vigenere`: aka Vigenere Cipher - [X] `xorN`: XOR with a single byte (*N* belongs to [1,255]) > :warning: Crypto functions are of course definitely **NOT** encoding functions ; they are implemented for leveraging the `.encode(...)` API from `codecs`. diff --git a/docs/pages/enc/crypto.md b/docs/pages/enc/crypto.md index 432ac3d..247fbed 100644 --- a/docs/pages/enc/crypto.md +++ b/docs/pages/enc/crypto.md @@ -202,20 +202,21 @@ This is a dynamic encoding, that is, it can be called with an integer to define ----- -### Vigenere Cipher +### Playfair Cipher -This is a dynamic encoding, that is, it holds the key. There is no default key, meaning that `vigenere` as the encoding scheme throws a `LookupError` indicating that the _key must be a non-empty alphabetic string_. +The Playfair cipher is a symmetric encryption method using polygram substitution with bigrams (pairs of letters), invented in 1854 by Charles Wheatstone, but popularized by Lord Playfair. **Codec** | **Conversions** | **Aliases** | **Comment** :---: | :---: | --- | --- -`vigenere` | text <-> Vigenere ciphertext | `vigenere-abcdef`, `vigenere_MySuperSecret` | key only consists of characters, not digits +`playfair` | text <-> Playfair ciphertext | `playfair`, `playfair-playfairexample`, `playfair-keyword` | Dynamic key parameter ; letters only ; `J` is normalized to `I` ```python ->>> codext.encode("This is a test !", "vigenere-abababa") -'Tiit it a tfsu !' ->>> codext.encode("This is a test !", "vigenere_MySuperSecret") -'Ffam xw r liuk !' ->>> codext.decode("Tiit it a tfsu !", "vigenere-abababa") +>>> codext.encode("HIDETHEGOLDINTHETREESTUMP", "playfair-playfairexample") +'BMODZBXDNABEKUDMUIXMMOUVIF' +>>> codext.decode("BMODZBXDNABEKUDMUIXMMOUVIF", "playfair-playfairexample") +'HIDETHEGOLDINTHETREXESTUMP' +>>> codext.decode(codext.encode("INSTRUMENT", "playfair-keyword"), "playfair-keyword") +'INSTRUMENT' ``` ----- @@ -238,4 +239,3 @@ This is a dynamic encoding, that is, it can be called with an integer to define >>> codext.encode("~bcy*cy*k*~oy~", "xor-10") 'this is a test' ``` - diff --git a/src/codext/crypto/__init__.py b/src/codext/crypto/__init__.py index 00a2c58..438bc75 100644 --- a/src/codext/crypto/__init__.py +++ b/src/codext/crypto/__init__.py @@ -10,6 +10,4 @@ from .rot import * from .scytale import * from .shift import * -from .vigenere import * from .xor import * - diff --git a/src/codext/crypto/vigenere.py b/src/codext/crypto/vigenere.py deleted file mode 100755 index 39ac4ce..0000000 --- a/src/codext/crypto/vigenere.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: UTF-8 -*- -"""Vigenere Cipher Codec - vigenere content encoding. - -This codec: -- en/decodes strings from str to str -- en/decodes strings from bytes to bytes -- decodes file content to str (read) -- encodes file content from str to bytes (write) -""" -from string import ascii_lowercase as LC, ascii_uppercase as UC - -from ..__common__ import * - - -__examples__ = { - 'enc(vigenere)': None, - 'enc(vigenere-lemon)': {'ATTACKATDAWN': 'LXFOPVEFRNHR'}, - 'enc(vigenere-key)': {'hello': 'rijvs'}, - 'enc(vigenère_key)': {'Hello World': 'Rijvs Uyvjn'}, - 'enc-dec(vigenere-secret)': ['hello world', 'ATTACK AT DAWN', 'Test 1234!'], -} -__guess__ = ["vigenere-key", "vigenere-secret", "vigenere-password"] - - -__char = lambda c, k, i, d=False: (LC if (b := c in LC) else UC)[(ord(c) - ord("Aa"[b]) + \ - [1, -1][d] * (ord(k[i % len(k)]) - ord('a'))) % 26] - - -def __check(key): - key = key.lower() - if not key or not key.isalpha(): - raise LookupError("Bad parameter for encoding 'vigenere': key must be a non-empty alphabetic string") - return key - - -def vigenere_encode(key): - def encode(text, errors="strict"): - result, i, k = [], 0, __check(key) - for c in ensure_str(text): - if c in LC or c in UC: - result.append(__char(c, k, i)) - i += 1 - else: - result.append(c) - r = "".join(result) - return r, len(r) - return encode - - -def vigenere_decode(key): - def decode(text, errors="strict"): - result, i, k = [], 0, __check(key) - for c in ensure_str(text): - if c in LC or c in UC: - result.append(__char(c, k, i, True)) - i += 1 - else: - result.append(c) - r = "".join(result) - return r, len(r) - return decode - - -add("vigenere", vigenere_encode, vigenere_decode, r"vigen[eè]re(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$", penalty=.1) -