A symmetric cipher uses the same key to encrypt and decrypt. The sender and receiver must both know the secret. Every cipher in this chapter is breakable by modern standards, but they introduce the core idea: transform plaintext into ciphertext using a shared secret, and reverse the transformation with the same secret.
XOR cipher — the simplest encryption
XOR each byte of the plaintext with a key byte. XOR again with the same key to decrypt. It is its own inverse: a XOR k XOR k = a. With a truly random key as long as the message (used once), this is the one-time pad, which is information-theoretically secure. With a short repeating key, it is trivially breakable.
Scheme
; XOR cipher: encrypt and decrypt are the same operation; XOR each character code with the key byte
(define (xor-cipher text key-byte)
(list->string
(map (lambda (ch)
(integer->char
(bitwise-xor (char->integer ch) key-byte)))
(string->list text))))
(define plaintext "HELLO")
(define key 42)
(define encrypted (xor-cipher plaintext key))
(display "Encrypted: ")
(display encrypted) (newline)
; Decrypt by XOR-ing again with the same key
(define decrypted (xor-cipher encrypted key))
(display "Decrypted: ")
(display decrypted) (newline)
; Self-inverse property: a XOR k XOR k = a
(display "Round-trip works? ")
(display (equal? plaintext decrypted))
Python
# XOR cipher in Pythondef xor_cipher(text, key_byte):
return''.join(chr(ord(c) ^ key_byte) for c in text)
plaintext = "HELLO"
key = 42
encrypted = xor_cipher(plaintext, key)
print(f"Encrypted: {encrypted}")
decrypted = xor_cipher(encrypted, key)
print(f"Decrypted: {decrypted}")
print(f"Round-trip works? {plaintext == decrypted}")
Caesar cipher — shift by a fixed amount
Replace each letter with the letter k positions later in the alphabet (wrapping around). The key is a single number from 0 to 25. Only 26 possible keys: brute force takes 26 tries. Caesar reportedly used k=3.
Scheme
; Caesar cipher: shift each letter by k positions
(define (caesar-char ch k)
(if (char-alphabetic? ch)
(let* ((base (if (char-upper-case? ch) 6597))
(shifted (modulo (+ (- (char->integer ch) base) k) 26)))
(integer->char (+ base shifted)))
ch))
(define (caesar text k)
(list->string
(map (lambda (ch) (caesar-char ch k))
(string->list text))))
(define plaintext "ATTACK AT DAWN")
(define key 3)
(define encrypted (caesar plaintext key))
(display "Encrypted: ")
(display encrypted) (newline)
; Decrypt: shift by (26 - k)
(define decrypted (caesar encrypted (- 26 key)))
(display "Decrypted: ")
(display decrypted) (newline)
; Brute force: try all 26 keys
(display "--- Brute force ---") (newline)
(let loop ((k 0))
(when (< k 5)
(display k) (display ": ")
(display (caesar encrypted (- 26 k))) (newline)
(loop (+ k 1))))
(display "... (26 total)")
Python
# Caesar cipher in Pythondef caesar(text, k):
result = []
for ch in text:
if ch.isalpha():
base = ord('A') if ch.isupper() else ord('a')
result.append(chr((ord(ch) - base + k) % 26 + base))
else:
result.append(ch)
return''.join(result)
plaintext = "ATTACK AT DAWN"
key = 3
encrypted = caesar(plaintext, key)
print(f"Encrypted: {encrypted}")
print(f"Decrypted: {caesar(encrypted, 26 - key)}")
# Brute forceprint("--- Brute force ---")
for k inrange(5):
print(f"{k}: {caesar(encrypted, 26 - k)}")
print("... (26 total)")
Substitution cipher — arbitrary permutation
Map each letter to a different letter via a permutation of the alphabet. The keyspace is 26! (about 4 x 1026), far too large to brute-force. But letter frequency analysis breaks it: English has known frequencies (E is the most common), and the cipher preserves those frequencies.
The security of a cipher depends on how many keys an attacker must try. Caesar: 26 keys. Substitution: 26! keys. XOR with a 1-byte key: 256 keys. XOR with a 128-bit key: 2128 keys.
Modern ciphers aim for keyspaces so large that brute force is physically impossible.