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,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,,
|