substitutionciphers 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .griffinere import Griffinere
2
+
3
+ __all__ = ['Griffinere']
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import math
5
+ from dataclasses import dataclass, field
6
+ from typing import Dict, List
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class Griffinere:
11
+ key: str
12
+ alphabet: str | None = None
13
+
14
+ _alphabet: List[str] = field(init=False, repr=False)
15
+ _alphabet_length: int = field(init=False, repr=False)
16
+ _alphabet_position_map: Dict[str, int] = field(init=False, repr=False)
17
+ _key_chars: List[str] = field(init=False, repr=False)
18
+
19
+ def __post_init__(self) -> None:
20
+ default_alphabet = (
21
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
22
+ "abcdefghijklmnopqrstuvwxyz"
23
+ "0123456789"
24
+ )
25
+ alphabet_str = self.alphabet or default_alphabet
26
+ self._alphabet = self._validate_alphabet(alphabet_str, self.key)
27
+ self._alphabet_length = len(self._alphabet)
28
+ self._alphabet_position_map = {ch: idx for idx, ch in enumerate(self._alphabet)}
29
+ self._key_chars = list(self.key)
30
+
31
+ def encrypt_string(self, plain_text: str, minimum_response_length: int | None = None) -> str:
32
+ if not plain_text or plain_text.isspace():
33
+ return ""
34
+ if minimum_response_length is None:
35
+ return self._encrypt_segments(plain_text)
36
+ if minimum_response_length < 1:
37
+ raise ValueError("minimum_response_length must be greater than zero")
38
+ need_to_add = minimum_response_length - len(plain_text)
39
+ if need_to_add <= 0:
40
+ return self._encrypt_segments(plain_text)
41
+ pull_from_front = math.ceil(need_to_add / 1.25)
42
+ pull_from_back = need_to_add - pull_from_front
43
+ contiguous = plain_text.replace(" ", "") or plain_text
44
+ string_to_front = self._cycle_take(contiguous, pull_from_front, True)
45
+ string_to_back = self._cycle_take(contiguous, pull_from_back, False)
46
+ fragments_front = f"{self._encrypt_segments(string_to_front[::-1])}." if string_to_front else ""
47
+ fragments_back = f".{self._encrypt_segments(string_to_back)}" if string_to_back else ""
48
+ core = self._encrypt_segments(plain_text)
49
+ return f"{fragments_front}{core}{fragments_back}"
50
+
51
+ def decrypt_string(self, cipher_text: str) -> str:
52
+ if not cipher_text or cipher_text.isspace():
53
+ return ""
54
+ if "." in cipher_text:
55
+ parts = cipher_text.split(".")
56
+ if len(parts) > 2:
57
+ cipher_text = parts[1]
58
+ return self._decrypt_segments(cipher_text)
59
+
60
+ @staticmethod
61
+ def _validate_alphabet(alphabet: str, key: str) -> List[str]:
62
+ if "." in alphabet:
63
+ raise ValueError("Alphabet must not contain '.'")
64
+ unique: List[str] = []
65
+ seen = set()
66
+ for ch in alphabet:
67
+ if ch in seen:
68
+ raise ValueError(f"Duplicate character '{ch}' in provided alphabet.")
69
+ seen.add(ch)
70
+ unique.append(ch)
71
+ for ch in key:
72
+ if ch not in seen:
73
+ raise ValueError(f"Alphabet does not contain the character '{ch}' supplied in the key.")
74
+ return unique
75
+
76
+ @staticmethod
77
+ def _cycle_take(source: str, count: int, front: bool) -> str:
78
+ if count <= 0 or not source:
79
+ return ""
80
+ result: List[str] = []
81
+ length = len(source)
82
+ idx = 0
83
+ while len(result) < count:
84
+ result.append(source[idx % length] if front else source[-1 - (idx % length)])
85
+ idx += 1
86
+ return "".join(result)
87
+
88
+ def _encrypt_segments(self, text: str) -> str:
89
+ return " ".join(self._encrypt_word(word) if word else "" for word in text.split(" "))
90
+
91
+ def _decrypt_segments(self, text: str) -> str:
92
+ return " ".join(self._decrypt_word(word) if word else "" for word in text.split(" "))
93
+
94
+ def _encrypt_word(self, word: str) -> str:
95
+ segment_chars = self._to_base64_char_list(word)
96
+ key_chars = self._get_key(segment_chars)
97
+ encrypted = [self._shift_positive(kc, sc) for kc, sc in zip(key_chars, segment_chars)]
98
+ return "".join(encrypted)
99
+
100
+ def _decrypt_word(self, word: str) -> str:
101
+ segment_chars = list(word)
102
+ key_chars = self._get_key(segment_chars)
103
+ decrypted = [self._shift_negative(kc, sc) for kc, sc in zip(key_chars, segment_chars)]
104
+ return self._from_base64_char_list(decrypted)
105
+
106
+ def _shift_positive(self, key_char: str, text_char: str) -> str:
107
+ key_pos = self._alphabet_position_map.get(key_char)
108
+ text_pos = self._alphabet_position_map.get(text_char)
109
+ if key_pos is None or text_pos is None:
110
+ return text_char
111
+ return self._alphabet[(key_pos + text_pos) % self._alphabet_length]
112
+
113
+ def _shift_negative(self, key_char: str, text_char: str) -> str:
114
+ key_pos = self._alphabet_position_map.get(key_char)
115
+ text_pos = self._alphabet_position_map.get(text_char)
116
+ if key_pos is None or text_pos is None:
117
+ return text_char
118
+ return self._alphabet[(text_pos - key_pos + self._alphabet_length) % self._alphabet_length]
119
+
120
+ def _get_key(self, segment: List[str]) -> List[str]:
121
+ if not segment:
122
+ return []
123
+ key = list(self._key_chars)
124
+ while len(key) < len(segment):
125
+ key.extend(self._key_chars)
126
+ return key[: len(segment)]
127
+
128
+ @staticmethod
129
+ def _to_base64_char_list(text: str) -> List[str]:
130
+ if text is None:
131
+ raise ValueError("text cannot be None")
132
+ if text == "":
133
+ return []
134
+ encoded = base64.b64encode(text.encode()).decode().rstrip("=")
135
+ return list(encoded)
136
+
137
+ @staticmethod
138
+ def _from_base64_char_list(chars: List[str]) -> str:
139
+ if not chars:
140
+ return ""
141
+ encoded = "".join(chars)
142
+ padding_needed = (-len(encoded)) % 4
143
+ encoded += "=" * padding_needed
144
+ decoded_bytes = base64.b64decode(encoded)
145
+ return decoded_bytes.decode()
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: substitutionciphers
3
+ Version: 1.0.0
4
+ Summary: The Griffinere cipher is a custom encryption algorithm in C# designed for reversible, base64-normalized encryption using a repeating key. Inspired by the Vigenère cipher, it adds configurable alphabet support, input validation, and padding-based encryption length enforcement.
5
+ Project-URL: Homepage, https://github.com/RileyG00/Ciphers
6
+ Project-URL: Source, https://github.com/RileyG00/Ciphers/tree/add-python
7
+ Author-email: Riley Griffin <riley.griffin00@outlook.com>
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Security :: Cryptography
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Griffinere Cipher 🔐 — Python Edition
16
+
17
+ The **Griffinere** cipher is a reversible, Base‑64‑normalised encryption algorithm implemented in pure **Python**.
18
+ Inspired by the classic Vigenère cipher, it adds:
19
+
20
+ * **Configurable alphabets** (use any character set you like)
21
+ * **Input validation** for safer usage
22
+ * **Padding‑based length enforcement** so encrypted strings meet a minimum length
23
+
24
+ ---
25
+
26
+ ## 📦 Installation
27
+
28
+ ```bash
29
+ pip install substitutionciphers
30
+ ```
31
+
32
+ In your code:
33
+
34
+ ```python
35
+ from griffinere import Griffinere
36
+ ```
37
+
38
+ ---
39
+
40
+ ## ✨ Features
41
+
42
+ * 🔐 Encrypts & decrypts alphanumeric or **custom‑alphabet** strings
43
+ * 🧩 Define your **own alphabet** (emoji? Cyrillic? go ahead!)
44
+ * 📏 Optional **minimum‑length** padding for fixed‑width ciphertext
45
+ * ✅ Strong validation of both alphabet and key integrity
46
+ * 🧪 Unit‑tested with **pytest**
47
+
48
+ ---
49
+
50
+ ## 🧰 Usage
51
+
52
+ ### 1 Create a cipher
53
+
54
+ #### 1.1 Default alphabet
55
+
56
+ ```python
57
+ key = "YourSecureKey"
58
+ cipher = Griffinere(key)
59
+ ```
60
+
61
+ The built‑in alphabet is:
62
+
63
+ ```
64
+ A‑Z a‑z 0‑9
65
+ ```
66
+
67
+ #### 1.2 Custom alphabet
68
+
69
+ ```python
70
+ custom_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ12345"
71
+ key = "YOURKEY"
72
+ cipher = Griffinere(key, custom_alphabet)
73
+ ```
74
+
75
+ ##### Alphabet rules
76
+
77
+ 1. **Must not contain `.`** (dot)
78
+ 2. **All characters must be unique**
79
+ 3. **Every character in the key must appear in the alphabet**
80
+
81
+ ---
82
+
83
+ ### 2 Encrypt & decrypt
84
+
85
+ #### 2.1 Encrypt a string
86
+
87
+ ```python
88
+ plain_text = "Hello World 123"
89
+ encrypted = cipher.encrypt_string(plain_text)
90
+ # e.g. 'LUKsbK8 OK9ybKJ FC3z'
91
+ ```
92
+
93
+ #### 2.2 Encrypt with a minimum length
94
+
95
+ ```python
96
+ encrypted = cipher.encrypt_string(plain_text, minimum_response_length=24)
97
+ # e.g. 'cm9JbAxsIJg.LUKsbK8 OK9ybKJ FC3z.Fw'
98
+ ```
99
+
100
+ #### 3.1 Decrypt
101
+
102
+ ```python
103
+ decrypted = cipher.decrypt_string(encrypted)
104
+ assert decrypted == plain_text
105
+ ```
106
+
107
+ ---
108
+
109
+ ## ⚠️ Exceptions & validation
110
+
111
+ | Condition | Exception |
112
+ | ----------------------------------------------- | ------------ |
113
+ | Alphabet contains `.` | `ValueError` |
114
+ | Duplicate characters in alphabet | `ValueError` |
115
+ | Key contains characters not present in alphabet | `ValueError` |
116
+ | `minimum_response_length` < 1 | `ValueError` |
117
+
118
+ ---
119
+
120
+ ## 📄 License
121
+
122
+ MIT License © 2025 Riley Griffin
@@ -0,0 +1,5 @@
1
+ substitutionciphers/__init__.py,sha256=btBU77-Vz-YOHIhV7nmCucMJpddkt69IgDI6vpecvRE,62
2
+ substitutionciphers/griffinere.py,sha256=VnmhYS1zt6a41ip1_lvXAJAV1us4HdtqfiIFkb8hQmM,6059
3
+ substitutionciphers-1.0.0.dist-info/METADATA,sha256=8nJbaL7zF5FloBNhKJLEfe9eL3mbzMyTxjUE5ZMSSag,3091
4
+ substitutionciphers-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ substitutionciphers-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any