Cryptography
Encryption vs Encoding vs Hashing: What Each One Does
Separate encoding, encryption and hashing with Base64, SHA-256 and OpenSSL labs. Learn why Base64 is not protection and hashes are one-way.
Build a Caesar cipher in Python, test it with unittest, and see why this shift cipher gives no real secrecy.
You will write a caesar() function. It shifts English letters by a number of positions, leaves every other character alone, and reverses the shift when you pass a negative number. Then you will run a small test suite and read what it checks.
You need Python 3 and nothing else. If the idea of a shift cipher is new, read What Is the Caesar Cipher? first.
Install Python 3 from python.org. The code uses only the standard library, so there is nothing to install with pip. Create a folder for the lab and save the files there.
The output in this article came from Python 3.12.10 on Windows 11. I did not test other versions.
Python's ord() returns the Unicode code point of a character. chr() does the reverse and turns a code point back into a character. For the uppercase letter A, the code point is 65.
Run this in a terminal:
python -I -c "print(ord('A'), chr(65))"Output:
65 AThe -I flag runs Python in isolated mode. Environment variables that start with PYTHON are ignored, and the current folder is not added to the module search path. Use it when you run code from a folder you did not write yourself.
A shift works on a zero-based position. Subtracting 65 turns A into 0 and Z into 25. The % 26 step wraps the result back into the alphabet, and adding 65 restores the uppercase range:
chr((ord(letter) - 65 + shift) % 26 + 65)Lowercase letters start at 97, so the same idea uses 97 instead of 65.
Python's % operator never returns a negative number when the divisor is positive. That is why a negative shift works without extra code:
python -I -c "print((0 - 3) % 26)"Output:
23Zero minus three wraps to 23, which is the position of X.
Save this as caesar_cipher.py. It is the same code as the lab file.
"""Educational Caesar cipher only. NOT secure encryption.
Shift ASCII English A-Z/a-z; preserve every other Unicode character as-is.
"""
def caesar(text: str, shift: int) -> str:
shift %= 26
result = []
for character in text:
if 'A' <= character <= 'Z':
result.append(chr((ord(character) - 65 + shift) % 26 + 65))
elif 'a' <= character <= 'z':
result.append(chr((ord(character) - 97 + shift) % 26 + 97))
else:
result.append(character)
return ''.join(result)
if __name__ == '__main__':
print(caesar('HELLO, World!', 3)) # KHOOR, Zruog!
print(caesar('KHOOR, Zruog!', -3)) # HELLO, World!The first line, shift %= 26, reduces any shift to the range 0 to 25. A shift of 29 behaves like a shift of 3, and a shift of -3 becomes 23.
Run the file:
python -I caesar_cipher.pyOutput:
KHOOR, Zruog!
HELLO, World!The function changes only the letters A to Z and a to z. Digits, spaces, punctuation and accented letters pass through unchanged. That keeps the code short. A cipher that handled accented letters would need a rule for each of them, and this lab does not define one.
The lab also includes test_caesar_cipher.py. Open a terminal in the folder that holds both files and run:
python -I -m unittest discover -p 'test_*.py' -vOutput:
test_large_shift (test_caesar_cipher.CaesarCipherTest.test_large_shift) ... ok
test_lower (test_caesar_cipher.CaesarCipherTest.test_lower) ... ok
test_negative (test_caesar_cipher.CaesarCipherTest.test_negative) ... ok
test_roundtrip (test_caesar_cipher.CaesarCipherTest.test_roundtrip) ... ok
test_upper (test_caesar_cipher.CaesarCipherTest.test_upper) ... ok
test_wrap (test_caesar_cipher.CaesarCipherTest.test_wrap) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OKEach test checks one behavior:
test_upper and test_lower confirm that HELLO becomes KHOOR and attack becomes dwwdfn.test_wrap confirms that Z moves forward to C, which only works if the wraparound is correct.test_large_shift confirms that a shift of 29 gives the same result as a shift of 3.test_negative checks decryption by shifting DEF by -3.test_roundtrip encrypts text with a shift of 7, decrypts it with -7, and expects the original back. The sample text includes punctuation, a digit and á, so it also checks that non-letters survive.If you change the code and a test fails, the failure message shows the input, the expected value and the actual value. Read that message before you change anything else.
A Caesar cipher has only 25 useful shifts. Shift 0 and shift 26 both return the original text. A person can try every shift by hand, and a program can try them all in an instant.
Frequency analysis is a second problem. In English, letters such as E and T appear more often than Q or Z. On a longer message, the most common letters in the ciphertext often point to the most common letters in the plaintext.
So this cipher provides no real confidentiality. Do not use it for passwords, customer records, tokens or anything else that matters. If you need real encryption, use a well-tested library and a standard algorithm. The OWASP Cryptographic Storage Cheat Sheet lists the basic rules for protecting stored data.
Treat this exercise as a way to learn modulo arithmetic, character codes and the difference between a toy transformation and a real cipher.
After the tests pass, you can ask a language model to explain the modulo line, suggest edge cases, or write more tests. A useful prompt names the limits up front:
Review this educational Caesar cipher. What happens with negative shifts, accented characters and non-Latin text? Do not describe it as secure encryption.
Run any test the model proposes before you accept it. Models make mistakes, and a test that fails tells you more than a confident explanation.
Once the lab works, keep the folder in version control so you can track changes. The next article, What Is GitHub?, explains how to do that.
Cryptography
Separate encoding, encryption and hashing with Base64, SHA-256 and OpenSSL labs. Learn why Base64 is not protection and hashes are one-way.
Cryptography
Why fast hashes fail for passwords, what salts and work factors do, and how to hash and verify with Argon2id in Python using argon2-cffi.
Cryptography
Public and private keys from zero: what a signature proves, why the private key stays secret, and an Ed25519 lab where a changed file fails.