computer-science5 min read

Cryptography Tutorial: Learn Security from Scratch (2026)

Cryptography Tutorial: Learn Security from Scratch (2026)

Published:  |  Category: Computer Science  |  Reading time: ~15 min
Cryptography Tutorial: Learn Security from Scratch (2026)

Cryptography is the science of secure communication in the presence of adversaries. It underpins everything from HTTPS to cryptocurrency to signal encryption. My journey through cryptography began with implementing basic ciphers and evolved to designing secure systems — learning the hard way that a single implementation mistake can render the strongest algorithm worthless. This tutorial covers symmetric and asymmetric encryption, hash functions, digital signatures, and real-world protocols like TLS.

We will emphasize practical security: how to use cryptographic libraries correctly, common pitfalls (nonce reuse, padding oracle attacks, timing side channels), and the rationale behind modern recommendations. Theory is grounded in code and attack demonstrations.

Symmetric Encryption: AES and Modes of Operation

Symmetric encryption uses the same key for encryption and decryption. AES (Advanced Encryption Standard) operates on 128-bit blocks with key sizes of 128, 192, or 256 bits. ECB mode encrypts each block independently — identical plaintext blocks produce identical ciphertext blocks, leaking patterns. CBC mode chains blocks via XOR with the previous ciphertext block and requires an initialization vector (IV). Counter mode (CTR) turns the block cipher into a stream cipher by encrypting incrementing counters, enabling parallel encryption. AEAD modes like GCM combine encryption with authentication.

from Crypto.Cipher import AES
import os

def aes_gcm_encrypt(key, plaintext):
    cipher = AES.new(key, AES.MODE_GCM)
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)
    return cipher.nonce, ciphertext, tag

def aes_gcm_decrypt(key, nonce, ciphertext, tag):
    cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    try:
        plaintext = cipher.decrypt_and_verify(ciphertext, tag)
        return plaintext
    except ValueError:
        return "Authentication failed"

key = os.urandom(32)
nonce, ct, tag = aes_gcm_encrypt(key, b"Secret message")

Asymmetric Encryption: RSA and ECC

Asymmetric (public-key) cryptography uses mathematically linked key pairs: a public key for encryption and a private key for decryption. RSA relies on the difficulty of factoring large composites — security depends on the key size (2048+ bits recommended). Elliptic Curve Cryptography (ECC) offers equivalent security with smaller keys (256-bit ECC ~ 3072-bit RSA) by operating on the elliptic curve discrete logarithm problem. ECDH (Elliptic Curve Diffie-Hellman) enables two parties to establish a shared secret over an insecure channel.

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

def rsa_encrypt(public_key_pem, plaintext):
    key = RSA.import_key(public_key_pem)
    cipher = PKCS1_OAEP.new(key)
    return cipher.encrypt(plaintext)

def rsa_decrypt(private_key_pem, ciphertext):
    key = RSA.import_key(private_key_pem)
    cipher = PKCS1_OAEP.new(key)
    return cipher.decrypt(ciphertext)

key = RSA.generate(2048)
public_key = key.publickey().export_key()
private_key = key.export_key()

ct = rsa_encrypt(public_key, b"Hello secure world")

Hash Functions and HMAC

Cryptographic hash functions map arbitrary input to fixed-size output (digest) with three critical properties: preimage resistance (cannot invert), second preimage resistance (cannot find different input with same hash), and collision resistance (cannot find two inputs with same hash). SHA-256 produces a 256-bit digest and is widely used. MD5 and SHA-1 are broken due to collision attacks. HMAC (Hash-based Message Authentication Code) combines a secret key with a hash function to authenticate messages, preventing length-extension attacks on plain hashes.

import hashlib
import hmac

def sha256_hash(data):
    return hashlib.sha256(data).hexdigest()

def hmac_sha256(key, message):
    return hmac.new(key, message, hashlib.sha256).hexdigest()

data = b"Critical document"
print(f"SHA-256: {sha256_hash(data)}")
key = b"supersecret"
print(f"HMAC: {hmac_sha256(key, data)}")

Digital Signatures

Digital signatures provide authenticity, integrity, and non-repudiation. The signer uses their private key to sign a message; anyone with the corresponding public key can verify the signature. DSA (Digital Signature Algorithm) and ECDSA are common standards. The typical process hashes the message to a digest, then encrypts the digest with the private key (conceptually). Ed25519 is a modern signature scheme using Curve25519 that offers high security, fast verification, and compact signatures (64 bytes).

from Crypto.Signature import DSS
from Crypto.Hash import SHA256
from Crypto.PublicKey import DSA

def sign_message(private_key, message):
    h = SHA256.new(message)
    signer = DSS.new(private_key, 'fips-186-3')
    return signer.sign(h)

def verify_signature(public_key, message, signature):
    h = SHA256.new(message)
    verifier = DSS.new(public_key, 'fips-186-3')
    try:
        verifier.verify(h, signature)
        return True
    except ValueError:
        return False

key = DSA.generate(1024)
sig = sign_message(key, b"Important message")
assert verify_signature(key.publickey(), b"Important message", sig)

TLS Protocol and Certificate Authorities

Transport Layer Security (TLS) is the most widely deployed cryptographic protocol, securing HTTPS, email, and many other applications. The TLS handshake negotiates cipher suites, authenticates the server (optionally the client) using X.509 certificates, and establishes session keys via Diffie-Hellman key exchange. Certificate Authorities (CAs) issue certificates verifying domain ownership. The chain of trust: root CAs are trusted by browsers, they issue intermediate CAs, which issue leaf certificates. Certificate Transparency logs prevent CAs from issuing fraudulent certificates without detection.

# Simplified TLS 1.3 handshake

class TLSHandshake:
    def __init__(self):
        self.cipher_suite = "TLS_AES_256_GCM_SHA384"
        self.ephemeral_key = None

    def client_hello(self):
        return {
            "version": "1.3",
            "cipher_suites": [self.cipher_suite],
            "key_share": generate_ecdh_keypair()
        }

    def server_hello(self, client_hello):
        self.ephemeral_key = generate_ecdh_keypair()
        shared = ecdh_shared_secret(client_hello['key_share'], self.ephemeral_key.private)
        return {"cipher_suite": self.cipher_suite,
                "key_share": self.ephemeral_key.public,
                "certificate": load_cert()}

    def finish(self):
        print("TLS 1.3 handshake complete, encrypted channel established")

Side-Channel Attacks and Countermeasures

Side-channel attacks exploit physical leakages rather than mathematical weaknesses. Timing attacks measure how long operations take — a constant-time comparison avoids leaking whether the first differing byte occurs early in the string. Power analysis monitors power consumption; cache-timing attacks (like Flush+Reload) observe which memory locations are accessed by a victim. Constant-time programming avoids secret-dependent branches and memory accesses. For example, comparing HMAC tags should use a constant-time function rather than strcmp, which short-circuits on the first mismatch.

# Timing-safe comparison (constant-time)
def constant_time_equal(a, b):
    if len(a) != len(b):
        return False
    result = 0
    for x, y in zip(a, b):
        result |= x ^ y
    return result == 0

# Unsafe comparison (leaks position of first difference via timing)
def unsafe_equal(a, b):
    if len(a) != len(b):
        return False
    for x, y in zip(a, b):
        if x != y:
            return False
    return True

Frequently Asked Questions

What is the difference between encryption and hashing?

Encryption is reversible — ciphertext can be decrypted back to plaintext with the correct key. Hashing is one-way — you cannot recover the original input from the digest. Hashing is used for integrity verification and password storage; encryption is used for confidentiality.

Why should I never roll my own cryptography?

Cryptographic implementation errors are subtle and catastrophic. Side-channel leaks, improper random number generation, nonce reuse, and padding oracle vulnerabilities are hard to detect. Use well-audited libraries like libsodium or the standard library's crypto module.

What is forward secrecy?

Forward secrecy ensures that session keys are not compromised if the long-term private key is leaked. Ephemeral Diffie-Hellman (DHE/ECDHE) generates per-session keys that are discarded after use. Even if an attacker records all traffic and later obtains the server's private key, past sessions remain secure.

How does a man-in-the-middle attack work against HTTPS?

An attacker intercepts the TLS handshake and presents a fake certificate. The attack fails if the client validates the certificate chain properly — verifying the signature against a trusted root CA. Self-signed certificates trigger browser warnings precisely because they cannot be verified against a trusted CA.

Originally published on Ayodhyyya. Last updated June 1, 2026.