A MAC is a piece of data that proves a message hasn’t been tampered with and that it came from a specific sender, all without needing to encrypt the message itself.

Imagine you’re sending a secret note across a crowded room. You want to make sure your friend receives the exact same note and knows it’s from you. A MAC is like a special stamp you put on the note. If the note is altered, or if someone else tries to send a note with your stamp, your friend will know something’s up.

Let’s see this in action. We’ll use Python’s hmac module to create and verify a MAC.

import hmac
import hashlib

# A secret key shared between sender and receiver
secret_key = b'my-super-secret-key'

# The message to be authenticated
message = b'This is a secret message.'

# Create the HMAC (Hash-based Message Authentication Code)
# We use SHA256 as the hash function here
mac = hmac.new(secret_key, message, hashlib.sha256).digest()

print(f"Message: {message.decode()}")
print(f"MAC: {mac.hex()}")

# --- Now, let's verify ---

# Receiver gets the message and the MAC
received_message = b'This is a secret message.'
received_mac = mac

# Receiver also has the secret key
# They recalculate the MAC for the received message
calculated_mac = hmac.new(secret_key, received_message, hashlib.sha256).digest()

print(f"\nReceiver calculated MAC: {calculated_mac.hex()}")

# Compare the received MAC with the calculated MAC
if hmac.compare_digest(received_mac, calculated_mac):
    print("Verification successful: Message is authentic and unaltered.")
else:
    print("Verification failed: Message has been tampered with or the key is wrong.")

# --- Let's try with a tampered message ---
tampered_message = b'This is a SECRET message.' # Case changed
tampered_mac = hmac.new(secret_key, tampered_message, hashlib.sha256).digest()

print(f"\nTampered Message: {tampered_message.decode()}")
print(f"Tampered MAC: {tampered_mac.hex()}")

# Receiver recalculates with the tampered message
recalculated_tampered_mac = hmac.new(secret_key, tampered_message, hashlib.sha256).digest()

print(f"Receiver calculated MAC for tampered message: {recalculated_tampered_mac.hex()}")

if hmac.compare_digest(tampered_mac, recalculated_tampered_mac):
    print("Verification successful: Message is authentic and unaltered.")
else:
    print("Verification failed: Message has been tampered with or the key is wrong.")

The output would look something like this:

Message: This is a secret message.
MAC: 8a3b2f0e1c5d4a6b7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d

Receiver calculated MAC: 8a3b2f0e1c5d4a6b7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d
Verification successful: Message is authentic and unaltered.

Tampered Message: This is a SECRET message.
Tampered MAC: c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2
Receiver calculated MAC for tampered message: c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2
Verification failed: Message has been tampered with or the key is wrong.

The core problem MACs solve is ensuring integrity and authenticity without the overhead of encryption. Encryption scrambles data to hide its content; MACs generate a tag based on the data and a shared secret. This tag is short, typically the same size as the hash output (e.g., 32 bytes for SHA-256), and can be sent alongside the plaintext message. The receiver, possessing the same secret key, recomputes the MAC on the received message. If the computed MAC matches the received MAC, it means the message hasn’t changed and was indeed generated by someone with the secret key. This is crucial for protocols where you need to verify the origin and integrity of messages but don’t necessarily need to keep the message content private, like in API requests or session cookies.

The most common type of MAC is HMAC, which stands for Hash-based Message Authentication Code. HMAC isn’t a new algorithm; it’s a specific construction that uses a cryptographic hash function (like SHA-256, SHA-1, or MD5, though MD5 and SHA-1 are generally discouraged for security reasons) and a secret key. The HMAC construction is designed to be resistant to certain attacks that could compromise a naive MAC implementation (e.g., a simple hash of the message concatenated with the key). The HMAC specification, RFC 2104, details this construction: HMAC(K, m) = H((K' XOR opad) || H((K' XOR ipad) || m)), where H is the hash function, K is the secret key, m is the message, K' is the key padded or hashed to the block size of the hash function, ipad is a padding byte 0x36 repeated, and opad is a padding byte 0x5C repeated. This double hashing with XORed keys ensures that even if the underlying hash function has weaknesses, HMAC is generally more robust.

You might think that simply hashing the message with the secret key (e.g., hash(secret_key + message)) would be enough. However, this is vulnerable to "length extension attacks" if the hash function is susceptible to them (like SHA-1 or SHA-256). An attacker could potentially compute the hash of a longer message without knowing the original message, given the hash of a shorter message and the length of the original message. HMAC’s nested structure with XORed padding prevents this. The ipad and opad values, and the way the key is processed, are specifically chosen to thwart these types of attacks, ensuring that the MAC is secure as long as the underlying hash function is secure and the secret key remains secret.

The key takeaway is that a MAC provides authentication and integrity. It doesn’t provide confidentiality. If you need to hide the message content, you must encrypt it in addition to using a MAC. A common pattern is Encrypt-then-MAC, where the message is first encrypted, and then a MAC is computed over the ciphertext. This is generally considered the most secure approach.

The next hurdle you’ll likely encounter is understanding how to securely manage and distribute those secret keys used for HMAC.

Want structured learning?

Take the full Cryptography course →