# -*- coding: UTF-8 -*-
"""
Author: Jacek Kotlarski --<szumak@virthost.pl>
Created: 16.10.2023
Purpose: Provide simple, string-centric cryptographic helpers.
This module offers lightweight encoding and cipher helpers intended for quick
experiments and utilities that operate on user-provided keyboard input.
"""
import string
import unicodedata
from typing import Dict
from base64 import b64decode, b64encode
from binascii import Error as BinasciiError
from codecs import decode
from inspect import currentframe
from secrets import randbelow
from ..attribtool import NoDynamicAttributes
from ..raisetool import Raise
# https://www.tutorialspoint.com/cryptography_with_python/cryptography_with_python_xor_process.htm
# https://teachen.info/cspp/unit4/lab04-02.html
[docs]
class SimpleCrypto(NoDynamicAttributes):
"""Utility class for lightweight string encryption and encoding routines."""
[docs]
@staticmethod
def chars_table_generator() -> str:
"""Build the character table used by Caesar-style transformations.
### Returns:
[str] - Ordered set of printable and selected Unicode characters used for mapping.
"""
base_chars: str = string.printable
unicode_blocks: tuple[range, ...] = (
range(0x00A0, 0x024F + 1),
range(0x0370, 0x03FF + 1),
range(0x0400, 0x052F + 1),
range(0x0530, 0x058F + 1),
range(0x0590, 0x05FF + 1),
range(0x0600, 0x06FF + 1),
)
extra_chars = [
chr(code)
for block in unicode_blocks
for code in block
if unicodedata.category(chr(code))[0] != "C"
]
combined: str = base_chars + "".join(extra_chars)
return "".join(dict.fromkeys(combined))
[docs]
@classmethod
def salt_generator(cls, length: int = 8) -> int:
"""Generate a numeric salt with the requested number of digits.
### Arguments:
* length: int - Desired digit count for the generated salt.
### Returns:
[int] - Random salt constrained to the given length.
### Raises:
* ValueError: Provided length is less than 1.
"""
if length < 1:
raise Raise.error(
f"...{length}",
ValueError,
cls.__qualname__,
currentframe(),
)
min_value: int = 10 ** (length - 1)
range_size: int = 10**length - min_value
return min_value + randbelow(range_size)
[docs]
@classmethod
def caesar_encrypt(cls, salt: int, message: str) -> str:
"""Encode a message using a Caesar cipher over the generated table.
### Arguments:
* salt: int - Value used to derive the rotation offset.
* message: str - Plain-text message to encode.
### Returns:
[str] - Encoded message.
### Raises:
* TypeError: Salt is not an integer.
* TypeError: Message is not a string instance.
"""
if not isinstance(salt, int):
raise Raise.error(
"Expected 'salt' as integer.",
TypeError,
cls.__qualname__,
currentframe(),
)
if not isinstance(message, str):
raise Raise.error(
"Expected 'message' as str type.",
TypeError,
cls.__qualname__,
currentframe(),
)
chars: str = cls.chars_table_generator()
chars_len: int = len(chars)
shift: int = salt % chars_len
trans_table: Dict = str.maketrans(chars, chars[shift:] + chars[:shift])
return message.translate(trans_table)
[docs]
@classmethod
def caesar_decrypt(cls, salt: int, message: str) -> str:
"""Decode a Caesar-encrypted message using the generated table.
### Arguments:
* salt: int - Value used to derive the rotation offset.
* message: str - Encoded message to decode.
### Returns:
[str] - Plain-text message.
### Raises:
* TypeError: Salt is not an integer.
* TypeError: Message is not a string instance.
"""
if not isinstance(salt, int):
raise Raise.error(
"Expected 'salt' as integer.",
TypeError,
cls.__qualname__,
currentframe(),
)
if not isinstance(message, str):
raise Raise.error(
"Expected 'message' as str type.",
TypeError,
cls.__qualname__,
currentframe(),
)
chars: str = cls.chars_table_generator()
chars_len: int = len(chars)
shift: int = chars_len - (salt % chars_len)
trans_table: Dict = str.maketrans(chars, chars[shift:] + chars[:shift])
return message.translate(trans_table)
[docs]
@classmethod
def rot13_codec(cls, message: str) -> str:
"""Perform ROT13 translation for ASCII-compatible text.
### Arguments:
* message: str - Text to encode or decode using ROT13.
### Returns:
[str] - Transformed message.
### Raises:
* TypeError: Message is not a string instance.
"""
if not isinstance(message, str):
raise Raise.error(
"Expected 'message' as str type.",
TypeError,
cls.__qualname__,
currentframe(),
)
return decode(message, "rot_13")
[docs]
@classmethod
def b64_encrypt(cls, message: str) -> str:
"""Encode a string as Base64 using UTF-8 bytes.
### Arguments:
* message: str - Text to encode.
### Returns:
[str] - Base64-encoded representation.
### Raises:
* TypeError: Message is not a string instance.
"""
if not isinstance(message, str):
raise Raise.error(
"Expected 'message' as str type.",
TypeError,
cls.__qualname__,
currentframe(),
)
return b64encode(message.encode("utf-8")).decode("ascii")
[docs]
@classmethod
def b64_decrypt(cls, message: str) -> str:
"""Decode a Base64 string assuming UTF-8 payload.
### Arguments:
* message: str - Base64 text to decode.
### Returns:
[str] - Decoded plain-text message.
### Raises:
* TypeError: Message is not a string instance.
* ValueError: Provided payload is not valid Base64.
"""
if not isinstance(message, str):
raise Raise.error(
"Expected 'message' as str type.",
TypeError,
cls.__qualname__,
currentframe(),
)
try:
raw_bytes = b64decode(message.encode("ascii"), validate=True)
except (BinasciiError, UnicodeEncodeError) as exc:
raise Raise.error(
"Invalid Base64 payload.",
ValueError,
cls.__qualname__,
currentframe(),
) from exc
return raw_bytes.decode("utf-8")
[docs]
@classmethod
def multiple_encrypt(cls, salt: int, message: str) -> str:
"""Apply Caesar, ROT13, and Base64 encoders sequentially.
The method first performs ROT13, then a Caesar shift, and finally Base64
encoding to produce a layered transformation.
### Arguments:
* salt: int - Value used to derive the Caesar translation offset.
* message: str - Plain-text message to encode.
### Returns:
[str] - Result of the chained encoders.
### Raises:
* TypeError: Message is not a string instance.
"""
if not isinstance(message, str):
raise Raise.error(
"Expected 'message' as str type.",
TypeError,
cls.__qualname__,
currentframe(),
)
return cls.b64_encrypt(cls.caesar_encrypt(salt, cls.rot13_codec(message)))
[docs]
@classmethod
def multiple_decrypt(cls, salt: int, message: str) -> str:
"""Reverse the layered encoding applied by `multiple_encrypt`.
### Arguments:
* salt: int - Value used to derive the Caesar translation offset.
* message: str - Encoded message produced by `multiple_encrypt`.
### Returns:
[str] - Original plain-text message.
### Raises:
* TypeError: Message is not a string instance.
* ValueError: Nested Base64 payload is invalid.
"""
if not isinstance(message, str):
raise Raise.error(
"Expected 'message' as str type.",
TypeError,
cls.__qualname__,
currentframe(),
)
return cls.rot13_codec(cls.caesar_decrypt(salt, cls.b64_decrypt(message)))
# #[EOF]#######################################################################