The surprising truth about modern cryptography is that it’s mostly a house of cards built on two fundamentally different, yet complementary, mathematical problems: factoring large numbers and finding discrete logarithms.

Let’s see this in action. Imagine Alice wants to send a secret message to Bob.

First, Alice generates a public/private key pair using an algorithm like RSA. Her public key, e_A, is shared widely, while her private key, d_A, is kept secret.

# Alice's key generation (conceptual, actual tools like openssl are used)
private_key_A = generate_rsa_private_key(bits=2048)
public_key_A = derive_public_key_from(private_key_A)

If Bob wants to send Alice a secret message, he uses her public key e_A to encrypt it.

message = "This is a secret!"
encrypted_message = encrypt(message, public_key_A)

Only Alice, with her corresponding private key d_A, can decrypt it.

decrypted_message = decrypt(encrypted_message, private_key_A)
print(decrypted_message) # Output: "This is a secret!"

This is asymmetric cryptography: one key encrypts, the other decrypts. It’s powerful for key exchange and digital signatures, but computationally expensive for large amounts of data.

For bulk data encryption, we use symmetric cryptography. Alice and Bob agree on a shared secret key beforehand.

# Alice and Bob agree on a shared secret key (e.g., a 256-bit AES key)
shared_secret_key = generate_aes_key(bits=256)

Alice then encrypts her message using this shared key and an algorithm like AES.

message = "This is a large file."
iv = generate_initialization_vector() # For modes like CBC
encrypted_data = aes_encrypt(message, shared_secret_key, iv, mode="AES-GCM")

Bob receives the encrypted data and uses the exact same shared secret key and IV to decrypt it.

decrypted_data = aes_decrypt(encrypted_data, shared_secret_key, iv, mode="AES-GCM")
print(decrypted_data) # Output: "This is a large file."

Symmetric encryption is much faster, making it ideal for encrypting files and network traffic. The challenge is securely sharing that shared_secret_key in the first place – which is where asymmetric cryptography often comes in.

To ensure data integrity and authenticity, we use hashing and Message Authentication Codes (MACs).

A hash function takes an input of any size and produces a fixed-size output, called a hash or digest. It’s a one-way street: easy to compute the hash from the data, but practically impossible to recover the data from the hash.

data = "This is the original data."
hash_digest = sha256(data.encode('utf-8')).hexdigest()
print(hash_digest) # e.g., "a1b2c3d4e5f6..."

If even a single bit of the data changes, the hash_digest will be completely different. This is great for verifying file integrity: if you download a file and its hash matches the one provided by the source, you know the file hasn’t been tampered with.

However, hashing alone doesn’t tell you who sent the data. That’s where MACs come in. A MAC is like a hash, but it uses a secret key in addition to the data.

message = "Please transfer $1000."
secret_key_for_mac = generate_hmac_key()

# Alice computes the MAC using her secret key and the message
mac_tag = hmac_sha256(message.encode('utf-8'), secret_key_for_mac)

# Alice sends the message and the MAC tag to Bob

Bob, who also knows secret_key_for_mac, can recompute the MAC on the received message. If his computed MAC matches the mac_tag Alice sent, he knows two things: the message hasn’t been altered (integrity), and it must have come from Alice (authenticity), because only she has secret_key_for_mac.

The critical insight for MACs is that they are keyed hash functions. While SHA-256 is deterministic, HMAC-SHA256 is keyed. You can’t derive the MAC tag without the secret key, and you can’t forge a valid MAC tag for a message you didn’t send if you don’t possess the secret key.

A common misconception is that a hash function is a MAC. It’s not. A hash function provides integrity verification based on the data itself. A MAC provides integrity and authenticity because it incorporates a secret key that only the sender and receiver possess. The security of a MAC relies on the secrecy of the key, not just the one-way nature of the underlying hash function.

The next logical step is understanding how these primitives are combined in protocols like TLS to secure your web browsing.

Want structured learning?

Take the full Cryptography course →