How to build a "tamperproof cookie"

How to build a "tamperproof cookie"

All right! It’s been a minute! Tamperproof cookies, I needed one, it’s pretty simple after thinking it through.

I feel like I always say it’s been a while since my last post… but this time it has in fact been a while.

The problem

So… working on a multi factor authentication setup for a freelance client. Have the multi factor one time password all set up, but now I need a way to implement a “remember me for 30 days” feature. That should be relatively simple, right? Can’t we just use cookies?

Not so fast there past me prior to thinking it through! Cookies can easily be modified by the client, because they’re stored on the client local storage and aren’t signed with a digital signature!

Here’s some context into the situation:

Context from the Coding Blocks Slack

Basically, I needed a way to store local to the user a secure (tamper proof) means of indicating the user has been dual factor authenticated for 30 days. The simplest way to do this would seemingly be to store a cookie with a value of the expiration date and user name. However, this first idea I had falls apart quickly, since a user could just change the cookie value (either expiration date and/or user name) and be able to get past the second factor. The second factor one time password (OTP) entry should only be presented to the user if the cookie is valid, for the specific user, and is not yet expired (30 days from the cookie creation).

Outline of Requirements

There are a few things that are needed for a client side solution:

  • Something stored client side that can be securely used as a means of indicating the user does not need to enter a OTP
    • Should have some sort of unique attribution to the user - either through a user ID, email address, or something similar
    • Should have an expiration date that is taken into account when checking the validity of the thing
    • Should not validate successfully in instances where the user information or the expiration date is changed

The solution

Once documenting the above requirements coupled with the above screenshot, it became pretty clear to me that some sort of cryptography could be used to ensure the tamperproof part of the thing being stored on the client side - in this case a cookie.

HMAC - Hash-base Message Authentication Code - is a cryptographic operation that utilizes a “key” and “message” to produce a “mac” which is (more or less) the combination of the two pieces. The general hmac signature looks something like this:

digest = hmac(key, message)

The key portion is secret between one or more parties - in my case one party, and the message is data that is to be validated as genuine. In my case, the key will be kept secret from the user, as that key is what verifies the message is what we put into the cookie.

What’s a good key to use you may be asking yourself? Well, it’s not the user name, though it’s something similar. The user already knows their own username, so it’s not a good candidate for a key. There is however, their password, specifically their password hash. The user would not know the hash of their password that is stored in the remote system, so it is a good potential candidate for a key.

The user’s password is computed basically like this:

password_digest = H(salt||plaintext_password, work_factor)

Where password_digest is the output of running salt concatenated with plaintext_password through a one way function H, work_factor times. The password_digest is not something the user would know, as their salt changes with each password change, nor do they know the underlying one way function applied to their plaintext_password.

So, now we have a key candidate, what about the actual message? The message is thankfully very straightforward, and we can use the datetime of the OTP cookie expiration. The full HMAC call will end up being: cookie_hash_content = hmac(H(password_hash), expiration_date_string).

This entire cookie_hash_content can be stored in the body of the cookie, though we will also need to stored the expiration_date_string within the cookie as well, so that the server side can reconstruct the inputs into the hmac call, and verify they match.

Thankfully, this is easy enough to do with a pipe (or other) delimited set of fields within the cookie. I will be using this format:

{expiration_date_string}|{cookie_hash_content}

Validating the above cookie at authentication time is as simple as:

  • Try to split the cookie value into two pieces, if less than two pieces exist, return false
  • Try to parse the first element in the split as a date, if fails, return false
  • Ensure the parsed date is after the current datetime, if it is not, return false
  • Validate the cookie_hash_content by comparing the cookie’s second part of the split, to a newly generated hmac(H(password_hash), expiration_date_string)
    • if they don’t match, return false
    • if they match, return true

The above pseudocode also has the added benefit if the user changes their password, they will immediately fail the OTP cookie check, as the mac using their old password hash will no longer match the newly generated/checked password hash upon login. This in a few ways makes things just a bit more secure.

Here is a quick run through of the HMAC (and a few unit tests) using fake expireDates and passwordHashes: https://replit.com/@Kritner/TamperProofCookies#main.py

Final thoughts

  • This works because the key is not known (or knowable) to the end user
  • The salts, while not necessarily secret, are not known to the end user, so they would have no chance of being able to recreate the same conditions to come up with the key being utilized
  • We verify the cookie_hash_content by re-computing the value based on the values stored in the cookie itself, along with the additional data of key that isn’t in the cookie, and can’t be known by the user, so there’s no chance the user would be able to come up with “the right information” that could pass the verify step.

Resources:

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×