Another case of "Hey, I have an idea" but someone already invented it 30 years ago.
That will be true in general. The key is that, while the idea is important, the implementation of the idea is more meaningful. For example, idea of capacitive touch screens was invented over forty years ago, but it wasn't until recently that they become good enough to be used ubiquitously. A better example, Bitcoin. Cryptomoney has been around for quite awhile as well, but it was never implemented as successfully as Bitcoin (having a blockchain being the breakthrough).
I understand that HMAC would be better, but what's wrong with using the regular hash?
Sorry for the terse response, I was in a rush at the time. Let's create a more general example, so I can better demonstrate why using SHA256 as a MAC is insecure. This is going to be a long one, but by your request I wrote it out long-wise to be educational.
Suppose you have a chatroom server, and it receives messages from clients. The clients send along their username, and message. You want to ensure the messages actually belong to the given username. If you didn't, users could impersonate one another. So you establish a shared secret, unique to each user, and give it to each user when they register. Now, when clients send a message, they send their username, message, and a MAC. MAC is calculated like so:
mac = SHA256 (secret + message)
On the server side, the server can lookup the secret for the specified username, calculate what the MAC would be for the given message, and compare the calculated MAC to the one given by the client. If they match, all is well. Seems reasonable. Given that SHA256 is a one-way function, how could anyone forge a MAC? Well ...
Suppose a client, named Alice, sends the message "Hello" to the server. Mallory, an attacker, intercepts this message and its MAC. Now Mallory can use this information to impersonate Alice. Recall the SHA256 algorithm:
def sha256 (message):
data = pre_processing (message) # Does a specific kind of padding to bring the data length to a multiple of 512-bits
blocks = split (message) # Split into 512-bit blocks
state = initial_state # Specified by SHA-2 standard
for block in blocks:
state = process_block (state, block)
return state
For the message "Hello", Alice would call sha256 ("password" + "Hello") to calculate the MAC. The data given to SHA256 is 104 bits long. So the code boils down to this:
mac = process_block (initial_state, "password" + "Hello" + 408_bits_of_padding)
The server would do the same thing, and verify the message's owner. How about Mallory? Mallory doesn't know "password", but he does know "Hello", he knows the MAC Alice calculated, and suppose he knows the length of "password" (it could be a fixed length key, like in TOTP). So, time for a fun trick!
state = mac
new_mac = process_block (state, ". I'm stupid" + 416_bits_of_padding)
new_message = "Hello" + 408_bits_of_padding + ". I'm stupid"
Now, Mallory sends new_message and new_mac to the server. What will the server do? sha256 ("password" + new_message). Let's follow how it will execute the SHA256 algorithm:
data = pre_processing ("password" + new_message)
blocks = split (data)
state = initial_state
for block in blocks:
state = process_block (state, block)
if state == new_mac:
return True
return False
We know ("password" + new_message) is 608 bits long, thus requiring 416 bits of padding, so we can simplify the code:
data = "password" + new_message + 416_bits_of_padding
blocks = split (data) # Will be two blocks
state = process_block (initial_state, blocks[0])
state = process_block (state, blocks[1])
if state == new_mac:
return True
return False
Perhaps things are beginning to look suspicious, but let's keep going, given that we know what new_message is, and given that split just splits along 512-bit boundaries:
blocks[0] = "password" + "Hello" + 408_bits_of_padding
blocks[1] = ". I'm stupid" + 416_bits_of_padding
state = process_block (initial_state, blocks[0])
state = process_block (state, blocks[1])
...
Uh oh ... look back a bit and look at how Alice calculated mac. "mac = process_block (initial_state, "password" + "Hello" + 408_bits_of_padding)". See how the data passed to process_block in Alice's calculation is equal to blocks[0] in the server-side calculation on Mallory's message?
state = process_block (initial_state, "password" + "Hello" + 408_bits_of_padding)
state = process_block (state, blocks[1])
...
So, the first state the server calculates is equal to the mac that Alice sent awhile ago. The same mac Mallory intercepted and used in this attack. So we simplify the server-side code again:
state = process_block (mac, ". I'm stupid" + 416_bits_of_padding)
...
Which, if you look back again, you'll see is equal to the calculation that Mallory performed. Oh dear ...
Therefore, the server calculates the same MAC that Mallory sent for his manipulated message. Therefore, the server accepts the message, and makes the world think that Alice is stupid.
As you can see, using SHA256 as a MAC function leaves one vulnerable to a message extension attack. As you can also see, the message extension attack has a certain form: Mallory must always first append padding to his manipulated message, before tacking on his desired extra data. So, Mallory can't send any arbitrary message. That means this attack isn't always viable. In your case, for TOTP, it
probably can't be used. That said, I still wouldn't use SHA256 in a TOTP implementation. It's bad practice and asking for trouble.
Also, if I recall correctly, the HMAC construct has been shown to strengthen the underlying hash function. For example, I think HMAC-SHA1 is not vulnerable to the same attacks that SHA1 has fallen prey to in recent years (SHA1 has been "broken"; as of 2012 it costs ~$3 million to break a SHA1 hash).
One of the golden rules in cryptography is to use an algorithm only for its intended purpose, never anywhere else without extensive research and vetting. If you use sledgehammer to put a nail into a wall, it'll probably work, but more often than not you'll find yourself with a much bigger problem than a nail in the wall...
Looking for a good book?
Cryptography for Developers. Ain't no developer got time for a cryptography degree.