Observed since 20 Oct, 21:04 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
A command-line affine cipher tool for encrypting and decrypting text, with case-preserving transforms and known-key or brute-force decryption.
This repository consists of a Python implementation of the Affine Cipher.
The Affine Cipher is a type of monoalphabetic substitution cipher that encrypts plaintext by substituting one alphabetical character for another. It uses a mathematical function to encrypt the plaintext.
Originally, the structure of the project was the following:
affine-cipher
├──decrypt_affine_cipher.py
└──encrypt_affine_cypher.py
The original scripts were:
python encrypt_affine_cypher.py
python decrypt_affine_cipher.py
No workflows, no linting, no testing, no dependencies installed except colorama and no dependency file (colorama isn't pinned anywhere). There was even a filename inconsistency.
encrypt_affine_cypher.py encrypted with a hardcoded (a=3, b=10) entered via input(). Only uppercase letters were transformed, everything else (including lowercase) passed through unchanged. There was no check that a and 26 were coprime.
decrypt_affine_cipher.py contained the real math, but the script only ever called affine_brute_force. There was no path to decrypt with a known key.
To view the original project, please visit commit a2b3ddb on GitHub.
If you run tree -a -I 'htmlcov|Claude outputs|.coverage|test-notes.md|notes.md|output.txt|git-commit-messages.md|.pytest_cache|.ruff_cache|.mypy_cache|.DS_Store|.git|.venv|pycache|coverage.xml|dist' from the command line, filtering out the noise this diagram excludes, it results in the following:
.
├── .github
│ └── workflows
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .python-version
├── README.md
├── pyproject.toml
├── src
│ └── affine_cipher
│ ├── __init__.py
│ ├── cli.py
│ ├── decrypt.py
│ └── encrypt.py
├── tests
│ ├── test_cli.py
│ ├── test_decrypt.py
│ ├── test_encrypt.py
│ └── test_roundtrip.py
└── uv.lock
6 directories, 15 files
# encrypt_affine_cypher.py
import string
from colorama import init, Fore
# Initialize Colorama
init()
def affine_encryption(plaintext, a, b):
# Define the uppercase alphabet.
alphabet = string.ascii_uppercase
# Get the length of the alphabet
m = len(alphabet)
# Initialize an empty string to store the ciphertext.
ciphertext = ''
# Iterate through each character in the plaintext.
for char in plaintext:
# Check if the character is in the alphabet.
if char in alphabet:
# If it's an alphabet letter, encrypt it.
# Find the index of the character in the alphabet.
p = alphabet.index(char)
# Apply the encryption formula: (a * p + b) mod m.
c = (a * p + b) % m
# Append the encrypted character to the ciphertext.
ciphertext += alphabet[c]
else:
# If the character is not in the alphabet, keep it unchanged.
ciphertext += char
# Return the encrypted ciphertext.
return ciphertext
# Define the plaintext and key components
plaintext = input(f'{Fore.GREEN}[?] Enter text to encrypt: ')
a = 3
b = 10
# Call the affine_crypt function with the specified parameters
encrypted_text = affine_encryption(plaintext, a, b)
# Print the original plaintext, the key components, and the encrypted text
print(f'{Fore.MAGENTA}[+] Plaintext: {plaintext}')
print(f'{Fore.GREEN}[+] Encrypted Text: {encrypted_text} ')
# src/affine_cipher/encrypt.py
import string
from colorama import Fore, init
def affine_encryption(plaintext: str, a: int, b: int) -> str:
# Define the uppercase alphabet.
alphabet = string.ascii_uppercase
# Get the length of the alphabet
m = len(alphabet)
# Initialize an empty string to store the ciphertext.
ciphertext = ""
# Iterate through each character in the plaintext.
for char in plaintext:
# Check if the character is in the alphabet.
if char in alphabet:
# If it's an alphabet letter, encrypt it.
# Find the index of the character in the alphabet.
p = alphabet.index(char)
# Apply the encryption formula: (a * p + b) mod m.
c = (a * p + b) % m
# Append the encrypted character to the ciphertext.
ciphertext += alphabet[c]
else:
# If the character is not in the alphabet, keep it unchanged.
ciphertext += char
# Return the encrypted ciphertext.
return ciphertext
def run_encrypt() -> None:
# Initialize Colorama
init()
# Define the plaintext and key components
plaintext = input(f"{Fore.GREEN}[?] Enter text to encrypt: ")
a = 3
b = 10
# Call the affine_crypt function with the specified parameters
encrypted_text = affine_encryption(plaintext, a, b)
# Print the original plaintext, the key components, and the encrypted text
print(f"{Fore.MAGENTA}[+] Plaintext: {plaintext}")
print(f"{Fore.GREEN}[+] Encrypted Text: {encrypted_text} ")
# decrypt_affine_cipher.py
import string
from colorama import init, Fore
# Initialize colorama
init()
# Function to get the Euclidean Algorithm
def extended_gcd(a, b):
"""
extended Euclidean Algorithm to find the greatest common divisor and coefficients x, y such that ax + by = gcd(a, b)
"""
if a == 0:
return (b, 0, 1)
else:
g, x, y = extended_gcd(b % a, a)
return (g, y - (b // a) * x, x)
# function to get the modular inverse
def modular_inverse(a, m):
"""
Compute the modular multiplicative inverse of a modulo m.
Raises an exception if the modular inverse does not exist.
"""
g, x, y = extended_gcd(a, m)
if g != 1:
raise Exception('Modular inverse does not exist!')
else:
return x % m
# Function to decrypt our message
def affine_decrypt(ciphertext, a, b):
"""
Decrypt a message with the Affine Cipher using the given key components a and b.
"""
alphabet = string.ascii_uppercase
m = len(alphabet)
plaintext = ''
# Compute the multiplicative inverse of a
a_inv = modular_inverse(a, m)
# Iterate through each character in the ciphertext
for char in ciphertext:
# Check if the character is in the alphabet
if char in alphabet:
# If it's an alphabet letter, decrypt it
# Find the index of the character in the alphabet
c = alphabet.index(char)
# Apply the decryption formula: a_inv * (c - b) mod m
p = (a_inv * (c - b)) % m
# Append the decrypted character to the plaintext
plaintext += alphabet[p]
else:
# If the character is not in the alphabet, keep it unchanged
plaintext += char
# Return the decrypted plaintext
return plaintext
# Function to perform brute-force attack
def affine_brute_force(ciphertext):
"""
Brute-force attack to find possible keys for an Affine Cipher and print potential decryptions for manual inspection.
"""
alphabet = string.ascii_uppercase
m = len(alphabet)
# Iterate through possible values for a
for a in range(1, m):
# Ensure a and m are coprime
if extended_gcd(a, m)[0] == 1:
# Iterate through possible values for b
for b in range(0, m):
# Decrypt using the current key
decrypted_text = affine_decrypt(ciphertext, a, b)
# Print potential decryption for manual inspection
print(f'Key a={a}, b={b}: {decrypted_text}')
ciphertext = input(f'{Fore.GREEN}[?] Enter message to decrypt: ')
# Perform a brute-force attack to find a potential decrypted message.
affine_brute_force(ciphertext)
# src/affine_cipher/decrypt.py
import string
from colorama import Fore, init
# Function to get the Euclidean Algorithm
def extended_gcd(a: int, b: int) -> tuple[int, int, int]:
"""
extended Euclidean Algorithm to find the greatest common divisor and
coefficients x, y such that ax + by = gcd(a, b)
"""
if a == 0:
return (b, 0, 1)
else:
g, x, y = extended_gcd(b % a, a)
return (g, y - (b // a) * x, x)
# function to get the modular inverse
def modular_inverse(a: int, m: int) -> int:
"""
Compute the modular multiplicative inverse of a modulo m.
Raises an exception if the modular inverse does not exist.
"""
g, x, y = extended_gcd(a, m)
if g != 1:
raise ValueError("Modular inverse does not exist!")
else:
return x % m
# Function to decrypt our message
def affine_decrypt(ciphertext: str, a: int, b: int) -> str:
"""
Decrypt a message with the Affine Cipher using the given key components a and b.
"""
alphabet = string.ascii_uppercase
m = len(alphabet)
plaintext = ""
# Compute the multiplicative inverse of a
a_inv = modular_inverse(a, m)
# Iterate through each character in the ciphertext
for char in ciphertext:
# Check if the character is in the alphabet
if char in alphabet:
# If it's an alphabet letter, decrypt it
# Find the index of the character in the alphabet
c = alphabet.index(char)
# Apply the decryption formula: a_inv * (c - b) mod m
p = (a_inv * (c - b)) % m
# Append the decrypted character to the plaintext
plaintext += alphabet[p]
else:
# If the character is not in the alphabet, keep it unchanged
plaintext += char
# Return the decrypted plaintext
return plaintext
# Function to perform brute-force attack
def affine_brute_force(ciphertext: str) -> None:
"""
Brute-force attack to find possible keys for an Affine Cipher and print
potential decryptions for manual inspection.
"""
alphabet = string.ascii_uppercase
m = len(alphabet)
# Iterate through possible values for a
for a in range(1, m):
# Ensure a and m are coprime
if extended_gcd(a, m)[0] == 1:
# Iterate through possible values for b
for b in range(0, m):
# Decrypt using the current key
decrypted_text = affine_decrypt(ciphertext, a, b)
# Print potential decryption for manual inspection
print(f"Key a={a}, b={b}: {decrypted_text}")
def run_decrypt() -> None:
# Initialize colorama
init()
ciphertext = input(f"{Fore.GREEN}[?] Enter message to decrypt: ")
# Perform a brute-force attack to find a potential decrypted message.
affine_brute_force(ciphertext)
import argparse
from affine_cipher.decrypt import run_decrypt
from affine_cipher.encrypt import run_encrypt
def main() -> None:
parser = argparse.ArgumentParser(prog="affine-cipher")
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("encrypt")
subparsers.add_parser("decrypt")
args = parser.parse_args()
if args.command == "encrypt":
run_encrypt()
elif args.command == "decrypt":
run_decrypt()
The mathematical function used to encrypt the plaintext:
c = (a * p + b) % m
Where c is the numerical position of the resulting ciphertext character, a must be a coprime1 of m (which represents the size of the alphabet). b can be any integer. p represents the numerical position based on a zero index of the character being encrypted in the alphabet.
Let's say a = 3, and b = 10. a has a valid value, because it is a coprime of 26. Valid values for a are: 1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, and 25. And let's say the plaintext I want to encrypt is "HELLO".
# calculation for H (p = 7):
c = (3 * 7 + 10) % 26
c = 31 % 26
c = 5 -> F
# calculation for E (p = 4):
c = (3 * 4 + 10) % 26
c = (12 + 10) % 26
c = 22 mod 26
c = 22 -> W
# calculation for L (p = 11):
c = (3 * 11 + 10) % 26
c = 43 mod 26
c = 17 -> R
# calculation for L (p = 11):
c = (3 * 11 + 10) % 26
c = 43 mod 26
c = 17 -> R
# calculation for O (p = 14):
c = (3 * 14 + 10) % 26
c = 52 mod 26
c = 0 -> A
Manually decrypting the ciphertext into plaintext is more complicated than encrypting the plaintext into ciphertext.
The mathematical function used to manually decrypt the the ciphertext:
p = a⁻¹ * (c - b) mod 26
What is a⁻¹ and how do I calculate it?
In modular arithmetic, a⁻¹ means the modular multiplicative inverse of an integer.
Basically, I am looking for a whole number that, when multiplied by a, leaves a remainder of 1 when divided by 26.
The mathematical function for calculating the modular inverse is:
(a * x)(mod 26) = 1
Where x is the modular inverse of a.
Let's say a = 3, and b = 10. a has a valid value, because it is a coprime of 26. And let's say the ciphertext I want to decrypt is "FWRRA".
To calculate the modular inverse of a:
(3 * x) (mod 26) = 1
(3 * 9) mod 26 = 1
27 mod 26 = 1
To decrypt the ciphertext character F (c = 5):
p = a⁻¹ * (c - b) mod 26
p = 9 * (5 - 10) mod 26
p = 9 * (-5) mod 26
p = -45 mod 26 = 7 (-45 + 26 + 26 = 7)
7 = H
To decrypt the ciphertext character W (c = 22):
p = a⁻¹ * (c - b) mod 26
p = 9 * (22 - 10) mod 26
p = 9 * (12) mod 26 = 108 mod 26
p = 108 mod 26 = 4 (108 -104)
4 = E
To decrypt the ciphertext R (c = 17):
p = a⁻¹ * (c - b) mod 26
p = 9 * (17 - 10) mod 26
p = 9 * (7) mod 26 = 63 mod 26
p = 63 mod 26 = 2 -> 63 - 52 (26 * 2) = 11
11 = L
To decrypt the ciphertext R (c = 17):
p = a⁻¹ * (c - b) mod 26
p = 9 * (17 - 10) mod 26
p = 9 * (7) mod 26 = 63 mod 26
p = 63 mod 26 = 2 -> 63 - 52 (26 * 2) = 11
11 = L
To decrypt ciphertext A (c = 0):
p = a⁻¹ * (c - b) mod 26
p = 9 * (0 - 10) mod 26
p = 9 * (-10) mod 26
p = -90 mod 26 = (-90 + 26 + 26 + 26 + 26 = 14)
14 = O
git clone https://github.com/interglobalmedia/affine_cipher.git
cd affine_cipher
uv sync
Next, you will want to turn on the virtual environment with the command:
# run this at the root of the project
source .venv/bin/activate
To deactivate the virtual environment, simply run the command:
deactivate
| uv run | source .venv/bin/activate and | |
|---|---|---|
| Example command | uv run python script.py | source .venv/bin/activate + python script.py |
| Activation step | Not required. uv automatically finds, locks, and uses the .venv directory. | Required. Must be typed manually before running any Python files. |
| Terminal State | Temporary. It only exposes the environment to that specific command. | Persistent. Your terminal prompt changes, and all subsequent python commands target the venv. |
| Environment check | Dynamic. It verifies and installs missing or outdated dependencies from your lockfile before running. | Static. It blindly runs whatever state the virtual environment is currently in. |
| Safety | High. Prevents you from accidentally running code in the global python interpreter if you forget to activate. | Moderate. If you open a new tab or forget to source, it defaults back to your global system Python. |
You don't have to encrypt plaintext manually. This project provides a script that will do it for you:
uv run affine-cipher encrypt # then press return
This returns:
[?] Enter text to encrypt: HELLO # then press return
[+] Plaintext: HELLO
[+] Encrypted Text: FWRRA
You don't have to decrypt plaintext manually. This project provides a script that will do it for you:
uv run affine-cipher decrypt # then press return
[?] Enter message to decrypt: FWRRA
# which results in the output:
Key a=1, b=0: FWRRA
Key a=1, b=1: EVQQZ
Key a=1, b=2: DUPPY
Key a=1, b=3: CTOOX
Key a=1, b=4: BSNNW
Key a=1, b=5: ARMMV
Key a=1, b=6: ZQLLU
Key a=1, b=7: YPKKT
Key a=1, b=8: XOJJS
Key a=1, b=9: WNIIR
Key a=1, b=10: VMHHQ
Key a=1, b=11: ULGGP
Key a=1, b=12: TKFFO
Key a=1, b=13: SJEEN
Key a=1, b=14: RIDDM
Key a=1, b=15: QHCCL
Key a=1, b=16: PGBBK
Key a=1, b=17: OFAAJ
Key a=1, b=18: NEZZI
Key a=1, b=19: MDYYH
Key a=1, b=20: LCXXG
Key a=1, b=21: KBWWF
Key a=1, b=22: JAVVE
Key a=1, b=23: IZUUD
Key a=1, b=24: HYTTC
Key a=1, b=25: GXSSB
Key a=3, b=0: TQXXA
Key a=3, b=1: KHOOR
Key a=3, b=2: BYFFI
Key a=3, b=3: SPWWZ
Key a=3, b=4: JGNNQ
Key a=3, b=5: AXEEH
Key a=3, b=6: ROVVY
Key a=3, b=7: IFMMP
Key a=3, b=8: ZWDDG
Key a=3, b=9: QNUUX
Key a=3, b=10: HELLO # the only correct output
Key a=3, b=11: YVCCF
Key a=3, b=12: PMTTW
Key a=3, b=13: GDKKN
Key a=3, b=14: XUBBE
Key a=3, b=15: OLSSV
Key a=3, b=16: FCJJM
Key a=3, b=17: WTAAD
Key a=3, b=18: NKRRU
Key a=3, b=19: EBIIL
Key a=3, b=20: VSZZC
Key a=3, b=21: MJQQT
Key a=3, b=22: DAHHK
Key a=3, b=23: URYYB
Key a=3, b=24: LIPPS
Key a=3, b=25: CZGGJ
Key a=5, b=0: BUTTA
Key a=5, b=1: GZYYF
Key a=5, b=2: LEDDK
Key a=5, b=3: QJIIP
Key a=5, b=4: VONNU
Key a=5, b=5: ATSSZ
Key a=5, b=6: FYXXE
Key a=5, b=7: KDCCJ
Key a=5, b=8: PIHHO
Key a=5, b=9: UNMMT
Key a=5, b=10: ZSRRY
Key a=5, b=11: EXWWD
Key a=5, b=12: JCBBI
Key a=5, b=13: OHGGN
Key a=5, b=14: TMLLS
Key a=5, b=15: YRQQX
Key a=5, b=16: DWVVC
Key a=5, b=17: IBAAH
Key a=5, b=18: NGFFM
Key a=5, b=19: SLKKR
Key a=5, b=20: XQPPW
Key a=5, b=21: CVUUB
Key a=5, b=22: HAZZG
Key a=5, b=23: MFEEL
Key a=5, b=24: RKJJQ
Key a=5, b=25: WPOOV
Key a=7, b=0: XSVVA
Key a=7, b=1: IDGGL
Key a=7, b=2: TORRW
Key a=7, b=3: EZCCH
Key a=7, b=4: PKNNS
Key a=7, b=5: AVYYD
Key a=7, b=6: LGJJO
Key a=7, b=7: WRUUZ
Key a=7, b=8: HCFFK
Key a=7, b=9: SNQQV
Key a=7, b=10: DYBBG
Key a=7, b=11: OJMMR
Key a=7, b=12: ZUXXC
Key a=7, b=13: KFIIN
Key a=7, b=14: VQTTY
Key a=7, b=15: GBEEJ
Key a=7, b=16: RMPPU
Key a=7, b=17: CXAAF
Key a=7, b=18: NILLQ
Key a=7, b=19: YTWWB
Key a=7, b=20: JEHHM
Key a=7, b=21: UPSSX
Key a=7, b=22: FADDI
Key a=7, b=23: QLOOT
Key a=7, b=24: BWZZE
Key a=7, b=25: MHKKP
Key a=9, b=0: POZZA
Key a=9, b=1: MLWWX
Key a=9, b=2: JITTU
Key a=9, b=3: GFQQR
Key a=9, b=4: DCNNO
Key a=9, b=5: AZKKL
Key a=9, b=6: XWHHI
Key a=9, b=7: UTEEF
Key a=9, b=8: RQBBC
Key a=9, b=9: ONYYZ
Key a=9, b=10: LKVVW
Key a=9, b=11: IHSST
Key a=9, b=12: FEPPQ
Key a=9, b=13: CBMMN
Key a=9, b=14: ZYJJK
Key a=9, b=15: WVGGH
Key a=9, b=16: TSDDE
Key a=9, b=17: QPAAB
Key a=9, b=18: NMXXY
Key a=9, b=19: KJUUV
Key a=9, b=20: HGRRS
Key a=9, b=21: EDOOP
Key a=9, b=22: BALLM
Key a=9, b=23: YXIIJ
Key a=9, b=24: VUFFG
Key a=9, b=25: SRCCD
Key a=11, b=0: RCLLA
Key a=11, b=1: YJSSH
Key a=11, b=2: FQZZO
Key a=11, b=3: MXGGV
Key a=11, b=4: TENNC
Key a=11, b=5: ALUUJ
Key a=11, b=6: HSBBQ
Key a=11, b=7: OZIIX
Key a=11, b=8: VGPPE
Key a=11, b=9: CNWWL
Key a=11, b=10: JUDDS
Key a=11, b=11: QBKKZ
Key a=11, b=12: XIRRG
Key a=11, b=13: EPYYN
Key a=11, b=14: LWFFU
Key a=11, b=15: SDMMB
Key a=11, b=16: ZKTTI
Key a=11, b=17: GRAAP
Key a=11, b=18: NYHHW
Key a=11, b=19: UFOOD
Key a=11, b=20: BMVVK
Key a=11, b=21: ITCCR
Key a=11, b=22: PAJJY
Key a=11, b=23: WHQQF
Key a=11, b=24: DOXXM
Key a=11, b=25: KVEET
Key a=15, b=0: JYPPA
Key a=15, b=1: CRIIT
Key a=15, b=2: VKBBM
Key a=15, b=3: ODUUF
Key a=15, b=4: HWNNY
Key a=15, b=5: APGGR
Key a=15, b=6: TIZZK
Key a=15, b=7: MBSSD
Key a=15, b=8: FULLW
Key a=15, b=9: YNEEP
Key a=15, b=10: RGXXI
Key a=15, b=11: KZQQB
Key a=15, b=12: DSJJU
Key a=15, b=13: WLCCN
Key a=15, b=14: PEVVG
Key a=15, b=15: IXOOZ
Key a=15, b=16: BQHHS
Key a=15, b=17: UJAAL
Key a=15, b=18: NCTTE
Key a=15, b=19: GVMMX
Key a=15, b=20: ZOFFQ
Key a=15, b=21: SHYYJ
Key a=15, b=22: LARRC
Key a=15, b=23: ETKKV
Key a=15, b=24: XMDDO
Key a=15, b=25: QFWWH
Key a=17, b=0: LMBBA
Key a=17, b=1: OPEED
Key a=17, b=2: RSHHG
Key a=17, b=3: UVKKJ
Key a=17, b=4: XYNNM
Key a=17, b=5: ABQQP
Key a=17, b=6: DETTS
Key a=17, b=7: GHWWV
Key a=17, b=8: JKZZY
Key a=17, b=9: MNCCB
Key a=17, b=10: PQFFE
Key a=17, b=11: STIIH
Key a=17, b=12: VWLLK
Key a=17, b=13: YZOON
Key a=17, b=14: BCRRQ
Key a=17, b=15: EFUUT
Key a=17, b=16: HIXXW
Key a=17, b=17: KLAAZ
Key a=17, b=18: NODDC
Key a=17, b=19: QRGGF
Key a=17, b=20: TUJJI
Key a=17, b=21: WXMML
Key a=17, b=22: ZAPPO
Key a=17, b=23: CDSSR
Key a=17, b=24: FGVVU
Key a=17, b=25: IJYYX
Key a=19, b=0: DIFFA
Key a=19, b=1: SXUUP
Key a=19, b=2: HMJJE
Key a=19, b=3: WBYYT
Key a=19, b=4: LQNNI
Key a=19, b=5: AFCCX
Key a=19, b=6: PURRM
Key a=19, b=7: EJGGB
Key a=19, b=8: TYVVQ
Key a=19, b=9: INKKF
Key a=19, b=10: XCZZU
Key a=19, b=11: MROOJ
Key a=19, b=12: BGDDY
Key a=19, b=13: QVSSN
Key a=19, b=14: FKHHC
Key a=19, b=15: UZWWR
Key a=19, b=16: JOLLG
Key a=19, b=17: YDAAV
Key a=19, b=18: NSPPK
Key a=19, b=19: CHEEZ
Key a=19, b=20: RWTTO
Key a=19, b=21: GLIID
Key a=19, b=22: VAXXS
Key a=19, b=23: KPMMH
Key a=19, b=24: ZEBBW
Key a=19, b=25: OTQQL
Key a=21, b=0: ZGHHA
Key a=21, b=1: UBCCV
Key a=21, b=2: PWXXQ
Key a=21, b=3: KRSSL
Key a=21, b=4: FMNNG
Key a=21, b=5: AHIIB
Key a=21, b=6: VCDDW
Key a=21, b=7: QXYYR
Key a=21, b=8: LSTTM
Key a=21, b=9: GNOOH
Key a=21, b=10: BIJJC
Key a=21, b=11: WDEEX
Key a=21, b=12: RYZZS
Key a=21, b=13: MTUUN
Key a=21, b=14: HOPPI
Key a=21, b=15: CJKKD
Key a=21, b=16: XEFFY
Key a=21, b=17: SZAAT
Key a=21, b=18: NUVVO
Key a=21, b=19: IPQQJ
Key a=21, b=20: DKLLE
Key a=21, b=21: YFGGZ
Key a=21, b=22: TABBU
Key a=21, b=23: OVWWP
Key a=21, b=24: JQRRK
Key a=21, b=25: ELMMF
Key a=23, b=0: HKDDA
Key a=23, b=1: QTMMJ
Key a=23, b=2: ZCVVS
Key a=23, b=3: ILEEB
Key a=23, b=4: RUNNK
Key a=23, b=5: ADWWT
Key a=23, b=6: JMFFC
Key a=23, b=7: SVOOL
Key a=23, b=8: BEXXU
Key a=23, b=9: KNGGD
Key a=23, b=10: TWPPM
Key a=23, b=11: CFYYV
Key a=23, b=12: LOHHE
Key a=23, b=13: UXQQN
Key a=23, b=14: DGZZW
Key a=23, b=15: MPIIF
Key a=23, b=16: VYRRO
Key a=23, b=17: EHAAX
Key a=23, b=18: NQJJG
Key a=23, b=19: WZSSP
Key a=23, b=20: FIBBY
Key a=23, b=21: ORKKH
Key a=23, b=22: XATTQ
Key a=23, b=23: GJCCZ
Key a=23, b=24: PSLLI
Key a=23, b=25: YBUUR
Key a=25, b=0: VEJJA
Key a=25, b=1: WFKKB
Key a=25, b=2: XGLLC
Key a=25, b=3: YHMMD
Key a=25, b=4: ZINNE
Key a=25, b=5: AJOOF
Key a=25, b=6: BKPPG
Key a=25, b=7: CLQQH
Key a=25, b=8: DMRRI
Key a=25, b=9: ENSSJ
Key a=25, b=10: FOTTK
Key a=25, b=11: GPUUL
Key a=25, b=12: HQVVM
Key a=25, b=13: IRWWN
Key a=25, b=14: JSXXO
Key a=25, b=15: KTYYP
Key a=25, b=16: LUZZQ
Key a=25, b=17: MVAAR
Key a=25, b=18: NWBBS
Key a=25, b=19: OXCCT
Key a=25, b=20: PYDDU
Key a=25, b=21: QZEEV
Key a=25, b=22: RAFFW
Key a=25, b=23: SBGGX
Key a=25, b=24: TCHHY
Key a=25, b=25: UDIIZ
The core definition of cipher correctness is that it's a mathematical property stating that for any valid key (k) and message (m), running decryption on the ciphertext with that same key yields the original message. It also guarantees that the encryption transformation is fully invertible when using the proper matching key.
However, cipher correctness does not mean cipher security. Correctness ensures functionality and reliability (e.g., the message can be read later). Security ensures that unauthorized people cannot read the message even if they intercept it. An algorithm can be completely correct (it encrypts and decrypts fine) but insecure (easy for hackers to break, like the Caesar or Affine Cipher).
That said, originally this project contained the following limitations:
run_decrypt() in decrypt.py only runs affine_brute_force.However, as of September 13, 2026, it was further updated, which resulted in the following changes below.
Running either affine_decrypt or affine_brute_force in run_decrypt() means that a user doesn't just have the option to pass in ciphertext to run_decrypt() as initially. Now, the user can run something like:
affine-cipher decrypt -a 3 -b 10
which returns:
[?] Enter message to decrypt: FWRRA
[+] Decrypted Text: HELLO
Previously, when only affine_brute_force(ciphertext) was called in run_decrypt, and uv run affine-cipher decrypt was run (in the Using a script to decrypt ciphertext section), the user had to go through 312 possible outcomes of the decrypted text to find the right one. Now, a clean Decrypted Text: HELLO is returned instead.
The validate_affine_key function, located in key_validation.py, which is called inside affine_decrypt() in decrypt.py and affine_encryption() in encrypt.py, checks coprime validation. This adds flexibility to the Affine Cipher. Now, if a user inputs a value of a that is not a coprime of 26 (the length of the alphabet), they will know it and can select a value of a until it is actually a coprime of 26. Previously, the values of a and b were hard-coded.
Coprime validation example:
affine-cipher encrypt -a 2 -b 10
#which returns:
[?] Enter text to encrypt: FWRRA
usage: affine-cipher [-h] {encrypt,decrypt} ...
affine-cipher: error: This key is invalid. Try again. 1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25
To add a real known-key decrypt path via the CLI, run_decrypt() needs to call affine_decrypt(), but previously, it only called affine_brute_force() and not affine_decrypt(). Now it calls either affine_decrypt() or affine_brute_force().
I have taken things a step further:
The -a and -b flags are now required for encrypt but optional for decrypt when paired with --brute-force. In other words, either the user can choose -a and -b, or go with --brute-force alone (affine-cipher decrypt --brute-force).
affine-cipher decrypt -a 3 -b 10 --brute-force results in the following:affine-cipher decrypt -a 3 -b 10 --brute-force
usage: affine-cipher [-h] {encrypt,decrypt} ...
affine-cipher: error: -a and -b and --brute-force cannot all be selected together. Choose either -a and -b or --brute-force.
a is provided without -b, the following happens:affine-cipher decrypt -a 2
usage: affine-cipher [-h] {encrypt,decrypt} ...
affine-cipher: error: -a and -b must be given together
b is provided without -a, it returns:affine-cipher decrypt -b 10
usage: affine-cipher [-h] {encrypt,decrypt} ...
affine-cipher: error: -a and -b must be given together
--brute-force is provided, it results in:affine-cipher decrypt --brute-force
# which results in:
[?] Enter message to decrypt: FWRRA
Key a=1, b=0: FWRRA
Key a=1, b=1: EVQQZ
Key a=1, b=2: DUPPY
Key a=1, b=3: CTOOX
Key a=1, b=4: BSNNW
Key a=1, b=5: ARMMV
Key a=1, b=6: ZQLLU
Key a=1, b=7: YPKKT
Key a=1, b=8: XOJJS
Key a=1, b=9: WNIIR
Key a=1, b=10: VMHHQ
Key a=1, b=11: ULGGP
Key a=1, b=12: TKFFO
Key a=1, b=13: SJEEN
Key a=1, b=14: RIDDM
Key a=1, b=15: QHCCL
Key a=1, b=16: PGBBK
Key a=1, b=17: OFAAJ
Key a=1, b=18: NEZZI
Key a=1, b=19: MDYYH
Key a=1, b=20: LCXXG
Key a=1, b=21: KBWWF
Key a=1, b=22: JAVVE
Key a=1, b=23: IZUUD
Key a=1, b=24: HYTTC
Key a=1, b=25: GXSSB
Key a=3, b=0: TQXXA
Key a=3, b=1: KHOOR
Key a=3, b=2: BYFFI
Key a=3, b=3: SPWWZ
Key a=3, b=4: JGNNQ
Key a=3, b=5: AXEEH
Key a=3, b=6: ROVVY
Key a=3, b=7: IFMMP
Key a=3, b=8: ZWDDG
Key a=3, b=9: QNUUX
Key a=3, b=10: HELLO
Key a=3, b=11: YVCCF
Key a=3, b=12: PMTTW
Key a=3, b=13: GDKKN
Key a=3, b=14: XUBBE
Key a=3, b=15: OLSSV
Key a=3, b=16: FCJJM
Key a=3, b=17: WTAAD
Key a=3, b=18: NKRRU
Key a=3, b=19: EBIIL
Key a=3, b=20: VSZZC
Key a=3, b=21: MJQQT
Key a=3, b=22: DAHHK
Key a=3, b=23: URYYB
Key a=3, b=24: LIPPS
Key a=3, b=25: CZGGJ
Key a=5, b=0: BUTTA
Key a=5, b=1: GZYYF
Key a=5, b=2: LEDDK
Key a=5, b=3: QJIIP
Key a=5, b=4: VONNU
Key a=5, b=5: ATSSZ
Key a=5, b=6: FYXXE
Key a=5, b=7: KDCCJ
Key a=5, b=8: PIHHO
Key a=5, b=9: UNMMT
Key a=5, b=10: ZSRRY
Key a=5, b=11: EXWWD
Key a=5, b=12: JCBBI
Key a=5, b=13: OHGGN
Key a=5, b=14: TMLLS
Key a=5, b=15: YRQQX
Key a=5, b=16: DWVVC
Key a=5, b=17: IBAAH
Key a=5, b=18: NGFFM
Key a=5, b=19: SLKKR
Key a=5, b=20: XQPPW
Key a=5, b=21: CVUUB
Key a=5, b=22: HAZZG
Key a=5, b=23: MFEEL
Key a=5, b=24: RKJJQ
Key a=5, b=25: WPOOV
Key a=7, b=0: XSVVA
Key a=7, b=1: IDGGL
Key a=7, b=2: TORRW
Key a=7, b=3: EZCCH
Key a=7, b=4: PKNNS
Key a=7, b=5: AVYYD
Key a=7, b=6: LGJJO
Key a=7, b=7: WRUUZ
Key a=7, b=8: HCFFK
Key a=7, b=9: SNQQV
Key a=7, b=10: DYBBG
Key a=7, b=11: OJMMR
Key a=7, b=12: ZUXXC
Key a=7, b=13: KFIIN
Key a=7, b=14: VQTTY
Key a=7, b=15: GBEEJ
Key a=7, b=16: RMPPU
Key a=7, b=17: CXAAF
Key a=7, b=18: NILLQ
Key a=7, b=19: YTWWB
Key a=7, b=20: JEHHM
Key a=7, b=21: UPSSX
Key a=7, b=22: FADDI
Key a=7, b=23: QLOOT
Key a=7, b=24: BWZZE
Key a=7, b=25: MHKKP
Key a=9, b=0: POZZA
Key a=9, b=1: MLWWX
Key a=9, b=2: JITTU
Key a=9, b=3: GFQQR
Key a=9, b=4: DCNNO
Key a=9, b=5: AZKKL
Key a=9, b=6: XWHHI
Key a=9, b=7: UTEEF
Key a=9, b=8: RQBBC
Key a=9, b=9: ONYYZ
Key a=9, b=10: LKVVW
Key a=9, b=11: IHSST
Key a=9, b=12: FEPPQ
Key a=9, b=13: CBMMN
Key a=9, b=14: ZYJJK
Key a=9, b=15: WVGGH
Key a=9, b=16: TSDDE
Key a=9, b=17: QPAAB
Key a=9, b=18: NMXXY
Key a=9, b=19: KJUUV
Key a=9, b=20: HGRRS
Key a=9, b=21: EDOOP
Key a=9, b=22: BALLM
Key a=9, b=23: YXIIJ
Key a=9, b=24: VUFFG
Key a=9, b=25: SRCCD
Key a=11, b=0: RCLLA
Key a=11, b=1: YJSSH
Key a=11, b=2: FQZZO
Key a=11, b=3: MXGGV
Key a=11, b=4: TENNC
Key a=11, b=5: ALUUJ
Key a=11, b=6: HSBBQ
Key a=11, b=7: OZIIX
Key a=11, b=8: VGPPE
Key a=11, b=9: CNWWL
Key a=11, b=10: JUDDS
Key a=11, b=11: QBKKZ
Key a=11, b=12: XIRRG
Key a=11, b=13: EPYYN
Key a=11, b=14: LWFFU
Key a=11, b=15: SDMMB
Key a=11, b=16: ZKTTI
Key a=11, b=17: GRAAP
Key a=11, b=18: NYHHW
Key a=11, b=19: UFOOD
Key a=11, b=20: BMVVK
Key a=11, b=21: ITCCR
Key a=11, b=22: PAJJY
Key a=11, b=23: WHQQF
Key a=11, b=24: DOXXM
Key a=11, b=25: KVEET
Key a=15, b=0: JYPPA
Key a=15, b=1: CRIIT
Key a=15, b=2: VKBBM
Key a=15, b=3: ODUUF
Key a=15, b=4: HWNNY
Key a=15, b=5: APGGR
Key a=15, b=6: TIZZK
Key a=15, b=7: MBSSD
Key a=15, b=8: FULLW
Key a=15, b=9: YNEEP
Key a=15, b=10: RGXXI
Key a=15, b=11: KZQQB
Key a=15, b=12: DSJJU
Key a=15, b=13: WLCCN
Key a=15, b=14: PEVVG
Key a=15, b=15: IXOOZ
Key a=15, b=16: BQHHS
Key a=15, b=17: UJAAL
Key a=15, b=18: NCTTE
Key a=15, b=19: GVMMX
Key a=15, b=20: ZOFFQ
Key a=15, b=21: SHYYJ
Key a=15, b=22: LARRC
Key a=15, b=23: ETKKV
Key a=15, b=24: XMDDO
Key a=15, b=25: QFWWH
Key a=17, b=0: LMBBA
Key a=17, b=1: OPEED
Key a=17, b=2: RSHHG
Key a=17, b=3: UVKKJ
Key a=17, b=4: XYNNM
Key a=17, b=5: ABQQP
Key a=17, b=6: DETTS
Key a=17, b=7: GHWWV
Key a=17, b=8: JKZZY
Key a=17, b=9: MNCCB
Key a=17, b=10: PQFFE
Key a=17, b=11: STIIH
Key a=17, b=12: VWLLK
Key a=17, b=13: YZOON
Key a=17, b=14: BCRRQ
Key a=17, b=15: EFUUT
Key a=17, b=16: HIXXW
Key a=17, b=17: KLAAZ
Key a=17, b=18: NODDC
Key a=17, b=19: QRGGF
Key a=17, b=20: TUJJI
Key a=17, b=21: WXMML
Key a=17, b=22: ZAPPO
Key a=17, b=23: CDSSR
Key a=17, b=24: FGVVU
Key a=17, b=25: IJYYX
Key a=19, b=0: DIFFA
Key a=19, b=1: SXUUP
Key a=19, b=2: HMJJE
Key a=19, b=3: WBYYT
Key a=19, b=4: LQNNI
Key a=19, b=5: AFCCX
Key a=19, b=6: PURRM
Key a=19, b=7: EJGGB
Key a=19, b=8: TYVVQ
Key a=19, b=9: INKKF
Key a=19, b=10: XCZZU
Key a=19, b=11: MROOJ
Key a=19, b=12: BGDDY
Key a=19, b=13: QVSSN
Key a=19, b=14: FKHHC
Key a=19, b=15: UZWWR
Key a=19, b=16: JOLLG
Key a=19, b=17: YDAAV
Key a=19, b=18: NSPPK
Key a=19, b=19: CHEEZ
Key a=19, b=20: RWTTO
Key a=19, b=21: GLIID
Key a=19, b=22: VAXXS
Key a=19, b=23: KPMMH
Key a=19, b=24: ZEBBW
Key a=19, b=25: OTQQL
Key a=21, b=0: ZGHHA
Key a=21, b=1: UBCCV
Key a=21, b=2: PWXXQ
Key a=21, b=3: KRSSL
Key a=21, b=4: FMNNG
Key a=21, b=5: AHIIB
Key a=21, b=6: VCDDW
Key a=21, b=7: QXYYR
Key a=21, b=8: LSTTM
Key a=21, b=9: GNOOH
Key a=21, b=10: BIJJC
Key a=21, b=11: WDEEX
Key a=21, b=12: RYZZS
Key a=21, b=13: MTUUN
Key a=21, b=14: HOPPI
Key a=21, b=15: CJKKD
Key a=21, b=16: XEFFY
Key a=21, b=17: SZAAT
Key a=21, b=18: NUVVO
Key a=21, b=19: IPQQJ
Key a=21, b=20: DKLLE
Key a=21, b=21: YFGGZ
Key a=21, b=22: TABBU
Key a=21, b=23: OVWWP
Key a=21, b=24: JQRRK
Key a=21, b=25: ELMMF
Key a=23, b=0: HKDDA
Key a=23, b=1: QTMMJ
Key a=23, b=2: ZCVVS
Key a=23, b=3: ILEEB
Key a=23, b=4: RUNNK
Key a=23, b=5: ADWWT
Key a=23, b=6: JMFFC
Key a=23, b=7: SVOOL
Key a=23, b=8: BEXXU
Key a=23, b=9: KNGGD
Key a=23, b=10: TWPPM
Key a=23, b=11: CFYYV
Key a=23, b=12: LOHHE
Key a=23, b=13: UXQQN
Key a=23, b=14: DGZZW
Key a=23, b=15: MPIIF
Key a=23, b=16: VYRRO
Key a=23, b=17: EHAAX
Key a=23, b=18: NQJJG
Key a=23, b=19: WZSSP
Key a=23, b=20: FIBBY
Key a=23, b=21: ORKKH
Key a=23, b=22: XATTQ
Key a=23, b=23: GJCCZ
Key a=23, b=24: PSLLI
Key a=23, b=25: YBUUR
Key a=25, b=0: VEJJA
Key a=25, b=1: WFKKB
Key a=25, b=2: XGLLC
Key a=25, b=3: YHMMD
Key a=25, b=4: ZINNE
Key a=25, b=5: AJOOF
Key a=25, b=6: BKPPG
Key a=25, b=7: CLQQH
Key a=25, b=8: DMRRI
Key a=25, b=9: ENSSJ
Key a=25, b=10: FOTTK
Key a=25, b=11: GPUUL
Key a=25, b=12: HQVVM
Key a=25, b=13: IRWWN
Key a=25, b=14: JSXXO
Key a=25, b=15: KTYYP
Key a=25, b=16: LUZZQ
Key a=25, b=17: MVAAR
Key a=25, b=18: NWBBS
Key a=25, b=19: OXCCT
Key a=25, b=20: PYDDU
Key a=25, b=21: QZEEV
Key a=25, b=22: RAFFW
Key a=25, b=23: SBGGX
Key a=25, b=24: TCHHY
Key a=25, b=25: UDIIZ
affine-cipher decrypt is provided:affine-cipher decrypt
[?] Enter message to decrypt: FWRRA
usage: affine-cipher [-h] {encrypt,decrypt} ...
affine-cipher: error: Please either supply a key or --brute-force.
The coprimality validation error is triggered when a user does not provide a value of a that shares only the integer 1 as a positive integer factor in common with the value of m, which is 26.
This key is invalid. Try again. 1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25.Now, unlike previously, it is not necessary to only input plaintext uppercase letters when encrypting or ciphertext uppercase letters when decrypting. Both uppercase and lowercase letters are accepted.
case.py contains the restore_char_case() function, which preserves each character's original case in the output. In the original code, if a character was not uppercase, it would not be encrypted. For example:Hello World! 123 -> Fello Yorld! 123
Now:
uv run affine-cipher encrypt -a 3 -b 10
[?] Enter text to encrypt: Winter weather
[+] Plaintext: Winter weather
[+] Encrypted Text: Yixpwj ywkpfwj
| Command | Result |
|---|---|
affine-cipher decrypt -a 3 -b 10 | Known-key decrypt (direct) |
affine-cipher decrypt --brute-force | Brute-force all coprime keys |
affine-cipher decrypt -a 3 -b 10 --brute-force | Error: mutually exclusive |
affine-cipher decrypt -a 2 | Error: -a/-b must be given together |
affine-cipher decrypt -b 10 | Error: -a/-b must be given together |
affine-cipher decrypt | Error: must supply a key or --brute-force |
| Tool | Purpose | Configured in |
|---|---|---|
ruff | lint + format | pyproject.toml/ [tool.ruff] target-version = "py314" line-length = 88 [tool.ruff.lint] select = ["E", "F", "I", "UP", "B"] |
mypy | checks/verifies standard type hints in Python code | pyproject.toml/ [tool.mypy] python_version = "3.14" disallow_untyped_defs = true |
| Task | uv run pytest | pytest2 |
|---|---|---|
| Run the whole suite | uv run pytest | pytest |
| Run one file | uv run pytest tests/test_encrypt.py | pytest tests/test_encrypt.py |
| Run one test function | uv run pytest tests/test_encrypt.py::test_run_encrypt | pytest tests/test_encrypt.py::test_run_encrypt |
| Run tests matching a keyword | uv run pytest -k "coprime" | pytest -k "coprime" |
| Coverage, terminal | uv run pytest --cov=affine_cipher --cov-report=term-missing | pytest --cov=affine_cipher --cov-report=term-missing |
| Coverage, terminal + html | uv run pytest --cov=affine_cipher --cov-report=term-missing --cov-report=html | pytest --cov=affine_cipher --cov-report=term-missing --cov-report=html |
| Coverage, matches CI exactly | uv run pytest --cov=affine_cipher --cov-report=term-missing --cov-report=xml | pytest --cov=affine_cipher --cov-report=term-missing --cov-report=xml |
| Debug a failure interactively | uv run pytest -x --pdb | pytest -x --pdb |
| Tool | Purpose | Configured in |
|---|---|---|
bandit | security-focused static analysis tool | .pre-commit-config.yaml/ - id: bandit args: ["-r", "src"] pass_filenames: false |
pip-audit | scans Python environments and dependency trees for packages with known security vulnerabilities | .pre-commit-config.yaml/ - id: pip-audit name: pip-audit entry: uv run pip-audit language: system pass_filenames: false files: ^(pyproject.toml|uv.lock)$ |
| Hook | Purpose | Configured in |
|---|---|---|
ruff-check | primary command used to run the Ruff Linter on Python projects | .pre-commit-config.yaml/ - id: ruff-check args: [--fix] |
ruff-format | automatically formats Python code | .pre-commit-config.yaml/ - id: ruff-format |
mypy | checks/verifies standard type hints in Python code | .pre-commit-config.yaml/ - id: mypy additional_dependencies: [types-colorama] |
pytest | runs Python tests against the Python code | .pre-commit-config.yaml/ - id: pytest name: pytest entry: uv run pytest language: system pass_filenames: false always_run: true |
bandit | security-focused static analysis tool | .pre-commit-config.yaml/ - id: bandit args: ["-r", "src"] pass_filenames: false |
pip-audit | scans Python environments and dependency trees for packages with known security vulnerabilities | .pre-commit-config.yaml/ - id: pip-audit name: pip-audit entry: uv run pip-audit language: system pass_filenames: false files: ^(pyproject.toml|uv.lock)$ |
GitHub Actions runs on every push and pull request to main (.github/workflows/ci.yml), via two jobs:
lint-and-test: runs the checks from the tables above against every commit. ruff check and ruff format --check (lint & format), mypy . (type checking), pytest --cov=affine_cipher --cov-report=term-missing --cov-report=xml (tests + coverage), bandit -r src (security static analysis), and pip-audit (dependency vulnerability scanning). Environment setup uses astral-sh/setup-uv + uv sync --all-extras --dev --frozen for a reproducible install.build: runs uv build to produce a wheel, as a preview of the (currently deferred) Publish to PyPI work.permissions: contents: read scopes the workflow's token down to read-only, and a concurrency group cancels stale runs when new commits land on the same branch.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.1.0 | 15 Sept 2026 | 1 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 21:04 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:80be3275ef9077b966175494440cf1f32c5620b0faa3475c653e01ec08984d02