networkluki-cipher 0.1.0__tar.gz
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.
- networkluki_cipher-0.1.0/LICENSE +21 -0
- networkluki_cipher-0.1.0/PKG-INFO +100 -0
- networkluki_cipher-0.1.0/README.md +78 -0
- networkluki_cipher-0.1.0/pyproject.toml +36 -0
- networkluki_cipher-0.1.0/setup.cfg +4 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher/__init__.py +25 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher/__main__.py +6 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher/cipher.py +79 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher/cli.py +97 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher.egg-info/PKG-INFO +100 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher.egg-info/SOURCES.txt +13 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher.egg-info/dependency_links.txt +1 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher.egg-info/entry_points.txt +2 -0
- networkluki_cipher-0.1.0/src/networkluki_cipher.egg-info/top_level.txt +1 -0
- networkluki_cipher-0.1.0/tests/test_crypto.py +94 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 networkluki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: networkluki-cipher
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Educational monoalphabetic substitution cipher library and CLI
|
|
5
|
+
Author: NetworkLuki
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/networkluki/cryptography
|
|
8
|
+
Project-URL: Repository, https://github.com/networkluki/cryptography
|
|
9
|
+
Project-URL: Issues, https://github.com/networkluki/cryptography/issues
|
|
10
|
+
Keywords: cipher,substitution,education,cryptography,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Education
|
|
16
|
+
Classifier: Topic :: Security :: Cryptography
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# NetworkLuki Cipher
|
|
24
|
+
|
|
25
|
+
[](https://github.com/networkluki/cryptography/actions/workflows/ci.yml)
|
|
26
|
+
|
|
27
|
+
An educational monoalphabetic substitution cipher, available as a Python library and command-line application. This project also contains cryptography and network research notes.
|
|
28
|
+
|
|
29
|
+
> **Security warning:** This is a classical substitution cipher with a fixed default alphabet. It is trivial to break with frequency analysis and **must not** be used for passwords, private messages, authentication, file encryption, or any sensitive information. For real encryption, use a vetted cryptographic library and standard authenticated encryption.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
After the first PyPI release:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install networkluki-cipher
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
From a checked-out repository, for development:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m pip install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10 or newer. The runtime has no third-party dependencies.
|
|
46
|
+
|
|
47
|
+
## CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
networkluki-cipher show
|
|
51
|
+
networkluki-cipher show-map
|
|
52
|
+
networkluki-cipher enc "Hello, World!"
|
|
53
|
+
networkluki-cipher dec "Oryyz, Mztyc!"
|
|
54
|
+
printf '%s\n' 'Hello' | networkluki-cipher enc
|
|
55
|
+
python -m networkluki_cipher show
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`show` prints the plaintext alphabet and substitution alphabet on two lines. `show-map` prints all 26 mappings. `enc` and `dec` accept a single text argument or read standard input. A single unrecognized positional argument is treated as text to decrypt, for compatibility with the original script.
|
|
59
|
+
|
|
60
|
+
The CLI enforces a 1 MiB UTF-8 input limit; input exceeding it fails with exit code 2. Non-ASCII characters and nonletters are preserved.
|
|
61
|
+
|
|
62
|
+
The old local command remains available from the repository root:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python3 src/crypto.py show
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Python library
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from networkluki_cipher import build_maps, encrypt_text, decrypt_text
|
|
72
|
+
|
|
73
|
+
enc, dec = build_maps()
|
|
74
|
+
ciphertext = encrypt_text("Hello, World!", enc)
|
|
75
|
+
assert decrypt_text(ciphertext, dec) == "Hello, World!"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`build_maps(cipher=...)` can accept a custom permutation of the 26 ASCII letters for demonstrations. Changing the substitution alphabet **does not** make the cipher secure.
|
|
79
|
+
|
|
80
|
+
## Development and testing
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python -m pip install -e .
|
|
84
|
+
python -m unittest discover -s tests -v
|
|
85
|
+
python -m pip install build twine
|
|
86
|
+
python -m build
|
|
87
|
+
python -m twine check --strict dist/*
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Release steps and PyPI Trusted Publishing setup: [docs/PUBLISHING.md](docs/PUBLISHING.md).
|
|
91
|
+
|
|
92
|
+
## Research repository
|
|
93
|
+
|
|
94
|
+
Other material in this repository covers cryptography-focused research, network-layer concepts, visualizations, and reproducible experiments. Research and production-grade cryptography are separate from the educational cipher package.
|
|
95
|
+
|
|
96
|
+
For each study, document the problem statement, related work, method, results, limitations, and conclusions. Explicitly record cryptographic assumptions and threat models. Cite relevant RFC, NIST, ISO, or peer-reviewed references. Include the commit hash when citing this repository as a research artifact.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# NetworkLuki Cipher
|
|
2
|
+
|
|
3
|
+
[](https://github.com/networkluki/cryptography/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
An educational monoalphabetic substitution cipher, available as a Python library and command-line application. This project also contains cryptography and network research notes.
|
|
6
|
+
|
|
7
|
+
> **Security warning:** This is a classical substitution cipher with a fixed default alphabet. It is trivial to break with frequency analysis and **must not** be used for passwords, private messages, authentication, file encryption, or any sensitive information. For real encryption, use a vetted cryptographic library and standard authenticated encryption.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
After the first PyPI release:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python -m pip install networkluki-cipher
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
From a checked-out repository, for development:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Python 3.10 or newer. The runtime has no third-party dependencies.
|
|
24
|
+
|
|
25
|
+
## CLI
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
networkluki-cipher show
|
|
29
|
+
networkluki-cipher show-map
|
|
30
|
+
networkluki-cipher enc "Hello, World!"
|
|
31
|
+
networkluki-cipher dec "Oryyz, Mztyc!"
|
|
32
|
+
printf '%s\n' 'Hello' | networkluki-cipher enc
|
|
33
|
+
python -m networkluki_cipher show
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`show` prints the plaintext alphabet and substitution alphabet on two lines. `show-map` prints all 26 mappings. `enc` and `dec` accept a single text argument or read standard input. A single unrecognized positional argument is treated as text to decrypt, for compatibility with the original script.
|
|
37
|
+
|
|
38
|
+
The CLI enforces a 1 MiB UTF-8 input limit; input exceeding it fails with exit code 2. Non-ASCII characters and nonletters are preserved.
|
|
39
|
+
|
|
40
|
+
The old local command remains available from the repository root:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python3 src/crypto.py show
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Python library
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from networkluki_cipher import build_maps, encrypt_text, decrypt_text
|
|
50
|
+
|
|
51
|
+
enc, dec = build_maps()
|
|
52
|
+
ciphertext = encrypt_text("Hello, World!", enc)
|
|
53
|
+
assert decrypt_text(ciphertext, dec) == "Hello, World!"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`build_maps(cipher=...)` can accept a custom permutation of the 26 ASCII letters for demonstrations. Changing the substitution alphabet **does not** make the cipher secure.
|
|
57
|
+
|
|
58
|
+
## Development and testing
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m pip install -e .
|
|
62
|
+
python -m unittest discover -s tests -v
|
|
63
|
+
python -m pip install build twine
|
|
64
|
+
python -m build
|
|
65
|
+
python -m twine check --strict dist/*
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Release steps and PyPI Trusted Publishing setup: [docs/PUBLISHING.md](docs/PUBLISHING.md).
|
|
69
|
+
|
|
70
|
+
## Research repository
|
|
71
|
+
|
|
72
|
+
Other material in this repository covers cryptography-focused research, network-layer concepts, visualizations, and reproducible experiments. Research and production-grade cryptography are separate from the educational cipher package.
|
|
73
|
+
|
|
74
|
+
For each study, document the problem statement, related work, method, results, limitations, and conclusions. Explicitly record cryptographic assumptions and threat models. Cite relevant RFC, NIST, ISO, or peer-reviewed references. Include the commit hash when citing this repository as a research artifact.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "networkluki-cipher"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Educational monoalphabetic substitution cipher library and CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "NetworkLuki"}]
|
|
14
|
+
keywords = ["cipher", "substitution", "education", "cryptography", "cli"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Education",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Education",
|
|
21
|
+
"Topic :: Security :: Cryptography",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/networkluki/cryptography"
|
|
27
|
+
Repository = "https://github.com/networkluki/cryptography"
|
|
28
|
+
Issues = "https://github.com/networkluki/cryptography/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
networkluki-cipher = "networkluki_cipher.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
include = ["networkluki_cipher*"]
|
|
36
|
+
namespaces = false
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Educational substitution cipher API (not suitable for protecting secrets)."""
|
|
2
|
+
|
|
3
|
+
from .cipher import (
|
|
4
|
+
CIPHER,
|
|
5
|
+
MAX_STDIN_BYTES,
|
|
6
|
+
PLAIN,
|
|
7
|
+
build_maps,
|
|
8
|
+
check_input_size,
|
|
9
|
+
decrypt_text,
|
|
10
|
+
encrypt_text,
|
|
11
|
+
read_bounded_stdin,
|
|
12
|
+
validate_key,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CIPHER",
|
|
17
|
+
"MAX_STDIN_BYTES",
|
|
18
|
+
"PLAIN",
|
|
19
|
+
"build_maps",
|
|
20
|
+
"check_input_size",
|
|
21
|
+
"decrypt_text",
|
|
22
|
+
"encrypt_text",
|
|
23
|
+
"read_bounded_stdin",
|
|
24
|
+
"validate_key",
|
|
25
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Educational monoalphabetic substitution cipher.
|
|
2
|
+
|
|
3
|
+
This is not secure cryptography. Do not use it for secrets or sensitive data.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import string
|
|
9
|
+
from typing import Dict, Mapping, TextIO, Tuple
|
|
10
|
+
|
|
11
|
+
PLAIN = string.ascii_lowercase
|
|
12
|
+
CIPHER = "PAQCRIXOVEHYJUZGSTKLNFMDBW"
|
|
13
|
+
MAX_STDIN_BYTES = 1_048_576
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_key(cipher: str) -> None:
|
|
17
|
+
"""Require a permutation of the 26 ASCII letters (case-insensitive)."""
|
|
18
|
+
if not isinstance(cipher, str):
|
|
19
|
+
raise ValueError("Cipher key must be a string.")
|
|
20
|
+
if len(cipher) != 26:
|
|
21
|
+
raise ValueError("CIPHER must be exactly 26 characters long.")
|
|
22
|
+
if not cipher.isascii() or not cipher.isalpha():
|
|
23
|
+
raise ValueError("CIPHER must contain only letters A-Z.")
|
|
24
|
+
cipher_upper = cipher.upper()
|
|
25
|
+
if set(cipher_upper) != set(string.ascii_uppercase):
|
|
26
|
+
raise ValueError("CIPHER must contain each letter A-Z exactly once.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_maps(cipher: str = CIPHER) -> Tuple[Dict[str, str], Dict[str, str]]:
|
|
30
|
+
"""Build plaintext-to-cipher and cipher-to-plaintext substitution maps."""
|
|
31
|
+
validate_key(cipher)
|
|
32
|
+
cipher_upper = cipher.upper()
|
|
33
|
+
enc = dict(zip(PLAIN, cipher_upper))
|
|
34
|
+
dec = {encrypted.lower(): plain for plain, encrypted in enc.items()}
|
|
35
|
+
return enc, dec
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def encrypt_text(text: str, enc: Mapping[str, str]) -> str:
|
|
39
|
+
"""Substitute ASCII letters, keeping case and non-ASCII text unchanged."""
|
|
40
|
+
out: list[str] = []
|
|
41
|
+
for ch in text:
|
|
42
|
+
if ch in string.ascii_letters:
|
|
43
|
+
mapped = enc[ch.lower()]
|
|
44
|
+
out.append(mapped if ch.isupper() else mapped.lower())
|
|
45
|
+
else:
|
|
46
|
+
out.append(ch)
|
|
47
|
+
return "".join(out)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def decrypt_text(text: str, dec: Mapping[str, str]) -> str:
|
|
51
|
+
"""Reverse a substitution, keeping case and non-ASCII text unchanged."""
|
|
52
|
+
out: list[str] = []
|
|
53
|
+
for ch in text:
|
|
54
|
+
if ch in string.ascii_letters:
|
|
55
|
+
mapped = dec[ch.lower()]
|
|
56
|
+
out.append(mapped.upper() if ch.isupper() else mapped)
|
|
57
|
+
else:
|
|
58
|
+
out.append(ch)
|
|
59
|
+
return "".join(out)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_input_size(text: str, limit: int = MAX_STDIN_BYTES) -> str:
|
|
63
|
+
"""Return text after enforcing a UTF-8 byte limit."""
|
|
64
|
+
if not isinstance(limit, int) or limit < 0:
|
|
65
|
+
raise ValueError("Input limit must be a nonnegative integer.")
|
|
66
|
+
try:
|
|
67
|
+
byte_count = len(text.encode("utf-8"))
|
|
68
|
+
except UnicodeError as exc:
|
|
69
|
+
raise ValueError("Input is not valid UTF-8 text.") from exc
|
|
70
|
+
if byte_count > limit:
|
|
71
|
+
raise ValueError(f"Input exceeds maximum size of {limit} bytes.")
|
|
72
|
+
return text
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def read_bounded_stdin(stdin: TextIO, limit: int = MAX_STDIN_BYTES) -> str:
|
|
76
|
+
"""Read at most limit + 1 characters, then enforce the UTF-8 byte limit."""
|
|
77
|
+
if not isinstance(limit, int) or limit < 0:
|
|
78
|
+
raise ValueError("Input limit must be a nonnegative integer.")
|
|
79
|
+
return check_input_size(stdin.read(limit + 1), limit)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Command-line interface for the educational substitution cipher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import string
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from .cipher import (
|
|
11
|
+
CIPHER,
|
|
12
|
+
MAX_STDIN_BYTES,
|
|
13
|
+
PLAIN,
|
|
14
|
+
build_maps,
|
|
15
|
+
check_input_size,
|
|
16
|
+
decrypt_text,
|
|
17
|
+
encrypt_text,
|
|
18
|
+
read_bounded_stdin,
|
|
19
|
+
validate_key,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cmd_show() -> int:
|
|
24
|
+
print(PLAIN)
|
|
25
|
+
print(CIPHER)
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cmd_show_map() -> int:
|
|
30
|
+
for plain, encrypted in zip(string.ascii_uppercase, CIPHER):
|
|
31
|
+
print(f"{plain} -> {encrypted}")
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main(stdin_limit: int = MAX_STDIN_BYTES, argv: Sequence[str] | None = None) -> int:
|
|
36
|
+
"""Run the CLI and return a process exit status."""
|
|
37
|
+
try:
|
|
38
|
+
validate_key(CIPHER)
|
|
39
|
+
except ValueError as exc:
|
|
40
|
+
print(f"Key error: {exc}", file=sys.stderr)
|
|
41
|
+
return 2
|
|
42
|
+
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
prog="networkluki-cipher",
|
|
45
|
+
description="Educational monoalphabetic substitution cipher (NOT secure encryption).",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"cmd",
|
|
49
|
+
nargs="?",
|
|
50
|
+
help="show | show-map | enc | dec; other single values are decrypted",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"text",
|
|
54
|
+
nargs="?",
|
|
55
|
+
help="Input text; for enc/dec, omit to read stdin",
|
|
56
|
+
)
|
|
57
|
+
args = parser.parse_args(argv)
|
|
58
|
+
|
|
59
|
+
if args.cmd == "show":
|
|
60
|
+
if args.text is not None:
|
|
61
|
+
parser.error("show does not accept a text argument")
|
|
62
|
+
return cmd_show()
|
|
63
|
+
if args.cmd == "show-map":
|
|
64
|
+
if args.text is not None:
|
|
65
|
+
parser.error("show-map does not accept a text argument")
|
|
66
|
+
return cmd_show_map()
|
|
67
|
+
|
|
68
|
+
enc, dec = build_maps()
|
|
69
|
+
if args.cmd in ("enc", "dec"):
|
|
70
|
+
try:
|
|
71
|
+
data = (
|
|
72
|
+
check_input_size(args.text, stdin_limit)
|
|
73
|
+
if args.text is not None
|
|
74
|
+
else read_bounded_stdin(sys.stdin, stdin_limit)
|
|
75
|
+
)
|
|
76
|
+
except (ValueError, UnicodeError, OSError) as exc:
|
|
77
|
+
print(f"Input error: {exc}", file=sys.stderr)
|
|
78
|
+
return 2
|
|
79
|
+
output = encrypt_text(data, enc) if args.cmd == "enc" else decrypt_text(data, dec)
|
|
80
|
+
print(output)
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
if args.cmd is not None and args.text is None:
|
|
84
|
+
try:
|
|
85
|
+
data = check_input_size(args.cmd, stdin_limit)
|
|
86
|
+
except ValueError as exc:
|
|
87
|
+
print(f"Input error: {exc}", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
print(decrypt_text(data, dec))
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
parser.print_help()
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: networkluki-cipher
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Educational monoalphabetic substitution cipher library and CLI
|
|
5
|
+
Author: NetworkLuki
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/networkluki/cryptography
|
|
8
|
+
Project-URL: Repository, https://github.com/networkluki/cryptography
|
|
9
|
+
Project-URL: Issues, https://github.com/networkluki/cryptography/issues
|
|
10
|
+
Keywords: cipher,substitution,education,cryptography,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Education
|
|
16
|
+
Classifier: Topic :: Security :: Cryptography
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# NetworkLuki Cipher
|
|
24
|
+
|
|
25
|
+
[](https://github.com/networkluki/cryptography/actions/workflows/ci.yml)
|
|
26
|
+
|
|
27
|
+
An educational monoalphabetic substitution cipher, available as a Python library and command-line application. This project also contains cryptography and network research notes.
|
|
28
|
+
|
|
29
|
+
> **Security warning:** This is a classical substitution cipher with a fixed default alphabet. It is trivial to break with frequency analysis and **must not** be used for passwords, private messages, authentication, file encryption, or any sensitive information. For real encryption, use a vetted cryptographic library and standard authenticated encryption.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
After the first PyPI release:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install networkluki-cipher
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
From a checked-out repository, for development:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m pip install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10 or newer. The runtime has no third-party dependencies.
|
|
46
|
+
|
|
47
|
+
## CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
networkluki-cipher show
|
|
51
|
+
networkluki-cipher show-map
|
|
52
|
+
networkluki-cipher enc "Hello, World!"
|
|
53
|
+
networkluki-cipher dec "Oryyz, Mztyc!"
|
|
54
|
+
printf '%s\n' 'Hello' | networkluki-cipher enc
|
|
55
|
+
python -m networkluki_cipher show
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`show` prints the plaintext alphabet and substitution alphabet on two lines. `show-map` prints all 26 mappings. `enc` and `dec` accept a single text argument or read standard input. A single unrecognized positional argument is treated as text to decrypt, for compatibility with the original script.
|
|
59
|
+
|
|
60
|
+
The CLI enforces a 1 MiB UTF-8 input limit; input exceeding it fails with exit code 2. Non-ASCII characters and nonletters are preserved.
|
|
61
|
+
|
|
62
|
+
The old local command remains available from the repository root:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python3 src/crypto.py show
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Python library
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from networkluki_cipher import build_maps, encrypt_text, decrypt_text
|
|
72
|
+
|
|
73
|
+
enc, dec = build_maps()
|
|
74
|
+
ciphertext = encrypt_text("Hello, World!", enc)
|
|
75
|
+
assert decrypt_text(ciphertext, dec) == "Hello, World!"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`build_maps(cipher=...)` can accept a custom permutation of the 26 ASCII letters for demonstrations. Changing the substitution alphabet **does not** make the cipher secure.
|
|
79
|
+
|
|
80
|
+
## Development and testing
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python -m pip install -e .
|
|
84
|
+
python -m unittest discover -s tests -v
|
|
85
|
+
python -m pip install build twine
|
|
86
|
+
python -m build
|
|
87
|
+
python -m twine check --strict dist/*
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Release steps and PyPI Trusted Publishing setup: [docs/PUBLISHING.md](docs/PUBLISHING.md).
|
|
91
|
+
|
|
92
|
+
## Research repository
|
|
93
|
+
|
|
94
|
+
Other material in this repository covers cryptography-focused research, network-layer concepts, visualizations, and reproducible experiments. Research and production-grade cryptography are separate from the educational cipher package.
|
|
95
|
+
|
|
96
|
+
For each study, document the problem statement, related work, method, results, limitations, and conclusions. Explicitly record cryptographic assumptions and threat models. Cite relevant RFC, NIST, ISO, or peer-reviewed references. Include the commit hash when citing this repository as a research artifact.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/networkluki_cipher/__init__.py
|
|
5
|
+
src/networkluki_cipher/__main__.py
|
|
6
|
+
src/networkluki_cipher/cipher.py
|
|
7
|
+
src/networkluki_cipher/cli.py
|
|
8
|
+
src/networkluki_cipher.egg-info/PKG-INFO
|
|
9
|
+
src/networkluki_cipher.egg-info/SOURCES.txt
|
|
10
|
+
src/networkluki_cipher.egg-info/dependency_links.txt
|
|
11
|
+
src/networkluki_cipher.egg-info/entry_points.txt
|
|
12
|
+
src/networkluki_cipher.egg-info/top_level.txt
|
|
13
|
+
tests/test_crypto.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
networkluki_cipher
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Unit and regression tests for the public API and command-line interface."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import unittest
|
|
5
|
+
from contextlib import redirect_stdout
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
from networkluki_cipher import (
|
|
9
|
+
CIPHER,
|
|
10
|
+
build_maps,
|
|
11
|
+
decrypt_text,
|
|
12
|
+
encrypt_text,
|
|
13
|
+
read_bounded_stdin,
|
|
14
|
+
validate_key,
|
|
15
|
+
)
|
|
16
|
+
from networkluki_cipher.cli import main
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CipherTests(unittest.TestCase):
|
|
20
|
+
def test_default_key_is_a_valid_permutation(self):
|
|
21
|
+
validate_key(CIPHER)
|
|
22
|
+
|
|
23
|
+
def test_validate_key_rejects_duplicate_letters(self):
|
|
24
|
+
with self.assertRaisesRegex(ValueError, "exactly once"):
|
|
25
|
+
validate_key("A" * 26)
|
|
26
|
+
|
|
27
|
+
def test_validate_key_rejects_non_ascii_letters(self):
|
|
28
|
+
with self.assertRaisesRegex(ValueError, "only letters A-Z"):
|
|
29
|
+
validate_key("ABCDEFGHIJKLMNOPQRSTUVWXYÅ")
|
|
30
|
+
|
|
31
|
+
def test_validate_key_rejects_wrong_type(self):
|
|
32
|
+
with self.assertRaises(ValueError):
|
|
33
|
+
validate_key(None)
|
|
34
|
+
|
|
35
|
+
def test_build_maps_accepts_custom_permutation(self):
|
|
36
|
+
enc, dec = build_maps("ZYXWVUTSRQPONMLKJIHGFEDCBA")
|
|
37
|
+
self.assertEqual(encrypt_text("Abc", enc), "Zyx")
|
|
38
|
+
self.assertEqual(decrypt_text("Zyx", dec), "Abc")
|
|
39
|
+
|
|
40
|
+
def test_roundtrip_preserves_case_unicode_and_symbols(self):
|
|
41
|
+
enc, dec = build_maps()
|
|
42
|
+
original = "Hello, World! 123 åäö🛡️"
|
|
43
|
+
self.assertEqual(decrypt_text(encrypt_text(original, enc), dec), original)
|
|
44
|
+
|
|
45
|
+
def test_non_letters_unchanged(self):
|
|
46
|
+
enc, dec = build_maps()
|
|
47
|
+
original = "123-_=+åßé"
|
|
48
|
+
self.assertEqual(encrypt_text(original, enc), original)
|
|
49
|
+
self.assertEqual(decrypt_text(original, dec), original)
|
|
50
|
+
|
|
51
|
+
def test_read_bounded_stdin_rejects_oversized_text(self):
|
|
52
|
+
with self.assertRaisesRegex(ValueError, "maximum size"):
|
|
53
|
+
read_bounded_stdin(io.StringIO("abcd"), limit=3)
|
|
54
|
+
|
|
55
|
+
def test_read_bounded_stdin_counts_utf8_bytes(self):
|
|
56
|
+
with self.assertRaisesRegex(ValueError, "maximum size"):
|
|
57
|
+
read_bounded_stdin(io.StringIO("å"), limit=1)
|
|
58
|
+
|
|
59
|
+
def test_main_rejects_oversized_stdin(self):
|
|
60
|
+
with (
|
|
61
|
+
patch("sys.stdin", io.StringIO("abcd")),
|
|
62
|
+
patch("sys.stderr", new_callable=io.StringIO) as stderr,
|
|
63
|
+
):
|
|
64
|
+
self.assertEqual(main(stdin_limit=3, argv=["enc"]), 2)
|
|
65
|
+
self.assertIn("maximum size of 3 bytes", stderr.getvalue())
|
|
66
|
+
|
|
67
|
+
def test_main_rejects_oversized_argument(self):
|
|
68
|
+
with patch("sys.stderr", new_callable=io.StringIO) as stderr:
|
|
69
|
+
self.assertEqual(main(stdin_limit=3, argv=["enc", "abcd"]), 2)
|
|
70
|
+
self.assertIn("maximum size of 3 bytes", stderr.getvalue())
|
|
71
|
+
|
|
72
|
+
def test_cli_show_produces_exactly_two_lines(self):
|
|
73
|
+
stdout = io.StringIO()
|
|
74
|
+
with redirect_stdout(stdout):
|
|
75
|
+
self.assertEqual(main(argv=["show"]), 0)
|
|
76
|
+
self.assertEqual(stdout.getvalue().splitlines(), ["abcdefghijklmnopqrstuvwxyz", CIPHER])
|
|
77
|
+
|
|
78
|
+
def test_cli_show_map_produces_26_lines(self):
|
|
79
|
+
stdout = io.StringIO()
|
|
80
|
+
with redirect_stdout(stdout):
|
|
81
|
+
self.assertEqual(main(argv=["show-map"]), 0)
|
|
82
|
+
self.assertEqual(len(stdout.getvalue().splitlines()), 26)
|
|
83
|
+
|
|
84
|
+
def test_cli_implicit_decrypt(self):
|
|
85
|
+
enc, _ = build_maps()
|
|
86
|
+
encrypted = encrypt_text("Hello", enc)
|
|
87
|
+
stdout = io.StringIO()
|
|
88
|
+
with redirect_stdout(stdout):
|
|
89
|
+
self.assertEqual(main(argv=[encrypted]), 0)
|
|
90
|
+
self.assertEqual(stdout.getvalue(), "Hello\n")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
unittest.main()
|