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..2a8ee58 100644 --- a/src/codext/crypto/__init__.py +++ b/src/codext/crypto/__init__.py @@ -1,6 +1,7 @@ # -*- coding: UTF-8 -*- from .affine import * from .atbash import * +from .autoclave import * from .bacon import * from .barbie import * from .citrix import * @@ -9,5 +10,6 @@ from .rot import * from .scytale import * from .shift import * +from .vigenere import * from .xor import * diff --git a/src/codext/crypto/autoclave.py b/src/codext/crypto/autoclave.py new file mode 100644 index 0000000..b1c1f53 --- /dev/null +++ b/src/codext/crypto/autoclave.py @@ -0,0 +1,84 @@ +# -*- coding: UTF-8 -*- +"""Autoclave Cipher Codec - autoclave content encoding. + +The Autoclave (Autokey) cipher is a variant of the Vigenere cipher where the key +is extended by appending the plaintext to the initial key (key+plaintext), making +the key stream as long as the message itself. + +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/autoclave-cipher +""" +from string import ascii_lowercase as LC, ascii_uppercase as UC + +from ..__common__ import * + + +__examples__ = { + 'enc(autoclave)': None, + 'enc(autoclave-queenly)': {'ATTACKATDAWN': 'QNXEPVYTWTWP'}, + 'enc-dec(autoclave-key)': ['hello world', 'ATTACK AT DAWN', 'Test 1234!', 'Mixed Case 123'], +} +__guess__ = ["autoclave-key", "autoclave-secret", "autoclave-password"] + + +def __check(key): + key = key.lower() + if not key or not key.isalpha(): + raise LookupError("Bad parameter for encoding 'autoclave': key must be a non-empty alphabetic string") + return key + + +def autoclave_encode(key): + def encode(text, errors="strict"): + k = __check(key) + text_str = ensure_str(text) + alpha_chars = [c.lower() for c in text_str if c in LC or c in UC] + key_stream = k + "".join(alpha_chars) + result = [] + ki = 0 + for c in text_str: + if c in LC: + result.append(LC[(ord(c) - ord('a') + ord(key_stream[ki]) - ord('a')) % 26]) + ki += 1 + elif c in UC: + result.append(UC[(ord(c) - ord('A') + ord(key_stream[ki]) - ord('a')) % 26]) + ki += 1 + else: + result.append(c) + r = "".join(result) + return r, len(r) + return encode + + +def autoclave_decode(key): + def decode(text, errors="strict"): + k = __check(key) + text_str = ensure_str(text) + result = [] + key_stream = list(k) + ki = 0 + for c in text_str: + if c in LC: + dec_c = LC[(ord(c) - ord('a') - (ord(key_stream[ki]) - ord('a'))) % 26] + result.append(dec_c) + key_stream.append(dec_c) + ki += 1 + elif c in UC: + dec_c = UC[(ord(c) - ord('A') - (ord(key_stream[ki]) - ord('a'))) % 26] + result.append(dec_c) + key_stream.append(dec_c.lower()) + ki += 1 + else: + result.append(c) + r = "".join(result) + return r, len(r) + return decode + + +add("autoclave", autoclave_encode, autoclave_decode, + r"auto(?:clave|key)(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$", penalty=.1) 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) +