{"id":6897,"date":"2025-01-06T10:30:57","date_gmt":"2025-01-06T10:30:57","guid":{"rendered":"https:\/\/algocademy.com\/blog\/how-to-implement-simple-encryption-and-decryption\/"},"modified":"2026-10-15T10:34:49","modified_gmt":"2026-10-15T10:34:49","slug":"how-to-implement-simple-encryption-and-decryption","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/how-to-implement-simple-encryption-and-decryption\/","title":{"rendered":"How to Implement Simple Encryption and Decryption in Python"},"content":{"rendered":"<p>The simplest encryption you can write yourself is a Caesar cipher (shift each letter by a fixed amount) or an XOR cipher (combine each byte with a key byte). Both take under ten lines of Python, and both are good for learning and useless for protecting real data. Below are working implementations, a demonstration of how quickly each breaks, and the short, correct way to encrypt something for real.<\/p>\n<h2>Encryption in one paragraph<\/h2>\n<p>Encryption turns <strong>plaintext<\/strong> into <strong>ciphertext<\/strong> using an algorithm and a <strong>key<\/strong>; decryption reverses it with the key. With <strong>symmetric<\/strong> encryption the same key does both jobs. With <strong>asymmetric<\/strong> encryption (RSA, elliptic curves) there is a public key to encrypt and a private key to decrypt. Everything here is symmetric. Two other things get confused with encryption: <strong>encoding<\/strong> (Base64, hex), which is reversible by anyone and uses no key, and <strong>hashing<\/strong> (SHA-256), which is one-way.<\/p>\n<h2>The Caesar cipher<\/h2>\n<p>Each letter moves a fixed number of places along the alphabet, wrapping from Z to A. The shift is the key. Julius Caesar is reported to have used a shift of 3.<\/p>\n<pre><code class=\"language-python\">def caesar(text, shift):\n    out = []\n    for ch in text:\n        if ch.isascii() and ch.isalpha():\n            base = ord(\"A\") if ch.isupper() else ord(\"a\")\n            out.append(chr((ord(ch) - base + shift) % 26 + base))\n        else:\n            out.append(ch)          # keep digits, spaces, punctuation\n    return \"\".join(out)\n\nsecret = caesar(\"Meet me at noon, Cafe 9!\", 3)\nprint(secret)\nprint(caesar(secret, -3))<\/code><\/pre>\n<p>Output:<\/p>\n<pre><code class=\"language-text\">Phhw ph dw qrrq, Fdih 9!\nMeet me at noon, Cafe 9!<\/code><\/pre>\n<p>Three details matter. The <code>% 26<\/code> handles wrap-around, and Python&#8217;s <code>%<\/code> returns a non-negative result for a positive modulus, so a negative shift also works and decryption is just encryption with <code>-shift<\/code>. The <code>ch.isascii()<\/code> check matters because <code>\"\u00e9\".isalpha()<\/code> is true in Python, and without the check an accented letter would be mapped to garbage. And case is preserved by choosing the base letter per character.<\/p>\n<h3>Why it is broken<\/h3>\n<p>There are only 25 useful keys, so an attacker tries them all:<\/p>\n<pre><code class=\"language-python\">for k in range(1, 26):\n    print(k, caesar(\"Khoor, Zruog!\", -k))<\/code><\/pre>\n<p>Among the 25 lines, shift 3 prints <code>Hello, World!<\/code>, and a human or a dictionary check spots it at once. Even if the key were unknown and the text long, letter frequencies give it away, because &#8220;e&#8221; stays the most common letter, just relabeled.<\/p>\n<h2>XOR cipher<\/h2>\n<p>XOR (<code>^<\/code>) has the property <code>(a ^ k) ^ k == a<\/code>. So the same function encrypts and decrypts. Repeat the key to cover the message length:<\/p>\n<pre><code class=\"language-python\">def xor_bytes(data: bytes, key: bytes) -&gt; bytes:\n    return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))\n\nmsg = \"XOR is symmetric, caf\u00e9!\".encode(\"utf-8\")\nkey = b\"SECRET\"\nenc = xor_bytes(msg, key)\nprint(enc.hex())\nprint(xor_bytes(enc, key).decode(\"utf-8\"))<\/code><\/pre>\n<p>Output:<\/p>\n<pre><code class=\"language-text\">0b0a11722c2773363a3f283127372a31697430242591ec75\nXOR is symmetric, caf\u00e9!<\/code><\/pre>\n<p>Work with <code>bytes<\/code>, not strings, and show ciphertext as hex or Base64. A common mistake (and a bug in many tutorials) is to call <code>.decode()<\/code> on the XOR result: the output bytes are arbitrary and usually are not valid UTF-8, so decoding raises <code>UnicodeDecodeError<\/code> or corrupts the data.<\/p>\n<h3>Why it is broken<\/h3>\n<ul>\n<li><strong>Known plaintext.<\/strong> If an attacker knows or guesses part of the message (a file header, &#8220;Dear&#8221;, a JSON key), XORing it with the ciphertext reveals the key: <code>xor_bytes(enc[:6], msg[:6])<\/code> prints <code>b'SECRET'<\/code>.<\/li>\n<li><strong>Key reuse.<\/strong> XORing two ciphertexts made with the same key cancels the key and leaves the XOR of the two plaintexts, which is easy to pick apart.<\/li>\n<li><strong>Short repeating keys<\/strong> create a repeating pattern that can be found statistically (this is a Vigen\u00e8re-style cipher).<\/li>\n<\/ul>\n<p>XOR is secure in exactly one case: the <strong>one-time pad<\/strong>, where the key is truly random, as long as the message, and never used twice. <code>secrets.token_bytes(len(msg))<\/code> gives a suitable key, but you have to share a key as big as the data safely, so it is rarely practical. Real stream ciphers such as ChaCha20 use a short key to generate a long keystream that does not repeat, and they add authentication.<\/p>\n<h2>Base64 is not encryption<\/h2>\n<p>Base64 turns bytes into text made of 64 safe characters. It is used to put binary data into JSON, URLs or emails. Anyone can reverse it:<\/p>\n<pre><code class=\"language-python\">import base64\ns = base64.b64encode(b\"hello\").decode()\nprint(s, base64.b64decode(s))   # aGVsbG8= b'hello'<\/code><\/pre>\n<p>Storing a password or token as Base64 is the same as storing it in plain text. This mistake appears in real code reviews and security reports.<\/p>\n<h2>What to use for real data<\/h2>\n<p>Use a well-reviewed library with a high-level interface. In Python the <code>cryptography<\/code> package (install with <code>pip install cryptography<\/code>) offers Fernet, which handles the algorithm choice (AES-128-CBC with an HMAC), random initialization vectors and tamper detection for you:<\/p>\n<pre><code class=\"language-python\">from cryptography.fernet import Fernet\n\nkey = Fernet.generate_key()      # store this securely, not in your repo\nf = Fernet(key)\ntoken = f.encrypt(b\"my secret\")\nprint(f.decrypt(token))          # b'my secret'\n\ntry:\n    Fernet(Fernet.generate_key()).decrypt(token)   # wrong key\nexcept Exception as e:\n    print(type(e).__name__)      # InvalidToken<\/code><\/pre>\n<p>A wrong key, or a modified token, raises <code>InvalidToken<\/code> instead of returning garbage. That integrity check is what the toy ciphers lack. For other cases:<\/p>\n<ul>\n<li><strong>Passwords:<\/strong> never encrypt them; hash them with a slow, salted function such as scrypt, bcrypt or Argon2 (<code>hashlib.scrypt<\/code> is in the standard library).<\/li>\n<li><strong>Data in transit:<\/strong> use TLS (HTTPS); do not roll your own protocol.<\/li>\n<li><strong>Keys from passwords:<\/strong> derive them with a key derivation function (PBKDF2, scrypt, Argon2), not by using the password directly.<\/li>\n<li><strong>Random values<\/strong> for keys and tokens: use <code>secrets<\/code> or <code>os.urandom<\/code>, never <code>random<\/code>.<\/li>\n<li><strong>Modes:<\/strong> if you use AES directly, choose an authenticated mode such as GCM and never reuse a nonce with the same key.<\/li>\n<\/ul>\n<h2>Rules worth remembering<\/h2>\n<ol>\n<li>Do not invent your own cryptography for anything that matters; security comes from public, heavily scrutinized algorithms and a secret key, not from a secret algorithm.<\/li>\n<li>The key is the weak point. Keep it out of source control, in environment variables or a secrets manager, and plan for rotation.<\/li>\n<li>Encryption without authentication allows undetected tampering. Prefer authenticated encryption.<\/li>\n<li>Keep the three ideas straight: encoding is not encryption, and encryption is not hashing.<\/li>\n<\/ol>\n<h2>Practice<\/h2>\n<p>Good exercises: write a Caesar cracker that picks the most plausible shift by letter frequency, extend the XOR cipher to recover a repeating key from a known phrase, or implement the Vigen\u00e8re cipher and then break it. Caesar-style problems also appear in coding interviews as string manipulation. The Python lessons on <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=accessing-characters&amp;solution=1\">accessing characters<\/a> and <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=string-immutability&amp;solution=1\">string immutability<\/a> cover what you need, and our overview of <a href=\"https:\/\/algocademy.com\/blog\/string-algorithms-searching-and-manipulation\/\">string algorithms<\/a> goes further. For real systems, read the documentation of the <code>cryptography<\/code> library before writing any code.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Write a Caesar cipher and XOR cipher in Python, see why both are easy to break, and learn what to use instead (Fernet\/AES) for real data.<\/p>\n","protected":false},"author":1,"featured_media":6896,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-6897","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/6897"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=6897"}],"version-history":[{"count":1,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/6897\/revisions"}],"predecessor-version":[{"id":8708,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/6897\/revisions\/8708"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/6896"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=6897"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=6897"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=6897"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}