Building Own MAC: Part 1 - Encrypted, but Not Trusted
Why encryption alone is not enough
Huston, we have a problem
All right, now we have a super-duper cipher — AES. (You don’t? 0_o That means you missed the previous series of articles where we, with paper, glue, and a bit of magic, built our own AES cipher from scratch. Stop reading. Go grab it: Building Own Block Cipher: Part 3 - AES)
We can encrypt any message, and only the person who knows the secret key can decrypt it. The rest of the world would need a billion years to break it.
Does this mean we’re fine now? Did we finish cryptography? Is there nothing left to worry about?
Let’s make a small experiment.
We created an API for the bank. And one simplified endpoint looks like this:
POST: /api/transfer
BODY
user=$NAME
transaction=$AMOUNT
Assume the bank can decrypt the request. Ignore key management for now — this story is not about that.
Allice wants to send Bob 10$.
She prepares the message:
user=Bob; transaction=10
She encrypts the message with AES and sent to the bank.
POST: /api/transfer
BODY:
"a94f4aabdf..."
In a few minutes she received a notification from the bank:
Your transaction of 10 000$ to the Bob is successful.
0_o…
That was…unexpected.
What just happened?
A hacker has been quietly listening to Alice’s network traffic for weeks.
He cannot decrypt anything. AES is doing its job.
But he can see many encrypted transfers going back and forth.
Over time, he notices something interesting:
small, predictable changes in the encrypted data cause predictable changes after decryption.
So he modifies the encrypted message — just a little.
The bank decrypts the message successfully.
And that is the problem.
The message was decrypted…successfully 🤦🏼♂️
But it was not authentic!!!
Yes, this example is simplified. Real attacks look different.
But the idea is very real and very dangerous.
We can no longer blindly trust encrypted data.
Houston, we have a problem.
It’s a Cipher… It’s a Hash… It’s SuperMAC
So.
We encrypted the message.
AES did its job.
The attacker still won.
That should feel wrong.
Let’s rewind a bit.
What encryption actually promised us
Encryption is very honest.
It promises exactly one thing:
“If you don’t know the key, you can’t read this message.”
That’s it.
No hidden features. No bonus guarantees.
And to be fair — it kept that promise perfectly.
The hacker never learned:
who Alice paid
how much she paid
what the message even says
AES was innocent.
Then why did everything break?
Because we quietly assumed something else.
We assumed that:
“If the message decrypts — it must be OK.”
And that assumption is false.
Encryption does not promise that:
the message wasn’t modified
the message was constructed intentionally
the message makes sense
the message is safe to execute
It only promises secrecy.
And yes — secrecy is still necessary.
We absolutely want the attacker to stay blind.
But secrecy alone is not enough.
What the bank actually needed
The bank needed one more answer.
Not a complicated one.
Just this:
“Was this message created by someone who knows the secret — and was it changed on the way?”
Encryption cannot answer that question.
So we don’t throw encryption away.
We add something next to it.
Not to hide the message.
But to protect it.
“Can’t we just hash it?”
At this point, many people say:
“Okay, fine. Let’s just hash the message.”
Hashes are nice.
change one bit → completely different output
fast
simple
But there’s a problem.
Hashes have no secrets.
Anyone can compute them.
Which means anyone can fake them.
So hashes alone don’t help.
So… SuperMAC?
Let’s make a small trick.
Alice is still sending a message to the bank.
She already knows how to encrypt it.
We don’t change that part.
Now we add one more step.
Before sending the message, Alice takes:
the message itself
a secret key (shared with the bank)
And she computes a small extra value.
Call it a tag.
This tag is not encrypted data.
It does not hide anything.
It’s just a short fingerprint that depends on:
the message
and the secret key
Now Alice sends two things to the bank:
the encrypted message
the tag
That’s it.
What does the bank do?
The bank receives:
the encrypted message
the tag
It decrypts the message.
Then it does the same computation itself:
same message
same secret key
If the newly computed tag matches the received one — good.
The message was not modified.
If it doesn’t — something is wrong.
The message is rejected.
No guessing.
No “maybe it’s fine”.
Just a hard yes or no.
So what is a MAC, finally?
This is it.
That tag we just computed
is the Message Authentication Code.
Nothing more.
Nothing less.
A MAC is:
a small piece of data
computed from the message
using a secret key
and verified on the other side
If the tag matches — the message is authentic.
If it doesn’t — the message is rejected.
That’s the whole mechanism.
Important clarification
A MAC does not replace encryption.
We still need encryption.
We still want secrecy.
The MAC adds something else.
It adds:
integrity — the message was not modified
authenticity — the message was created by someone who knows the secret
So the picture now looks like this:
Encryption hides the message
MAC protects the message
Two different tools.
Two different guarantees.
Used together.
Why this extra layer matters
Without a MAC:
modified messages can slip through
decryption can succeed on garbage
the system has no way to say “stop”
With a MAC:
every modification is detected
every forged message is rejected
the system can finally trust what it decrypts
This is why real systems don’t use encryption alone.
They use encryption + authentication.
Final thoughts — and what comes next
Over my career, I’ve learned one important thing.
If you can detect the problem and then define it correctly — you already solved about 60% of it.
Another 38% is designing the right architecture.
And the remaining 2% is implementation.
In this article, we focused on the biggest and most underestimated part of the problem:
trusting encrypted data.
We saw that encryption alone is not enough.
We saw why “it decrypts successfully” is not a security guarantee.
And we saw what kind of extra property we are missing.
Deliberately, we stopped there.
Why we stop here
Because a MAC is not a single algorithm.
It is not a cipher.
It is not a trick.
It is not one formula.
A MAC is a family of designs.
And before touching any code, it’s much more important to understand:
how MACs are built
which architectures exist
and which building blocks they rely on
That’s what the next article is about.
What’s next
In the next article, we will:
take a close look at the two main architectures used to build MACs
zoom in on the primitives used inside those architectures
understand why these designs work — and why others don’t
Only after that, in the final part, we’ll move to implementation.
We’ll:
implement the primitives (starting with SHA-256)
and then build a MAC on top of them
completely from scratch
No black boxes.
No “just trust the library”.
No magic.


