2

Consider this common example used to demonstrate timing attacks:

async def sign_in(username, password):
  user = await get_user_from_db(username)
  if user is None:
    return False  # early return :(

  password_hash = slow_hash(password)
  return verify(password_hash, user.password_hash)

The usual suggestion is to do the same thing on all execution branches. For example, something like this:

async def sign_in(username, password):
  user = await get_user_from_db(username)
  if user is None:
    actual_password_hash = "foo"
  else:
    actual_password_hash = user.password_hash

  password_hash = slow_hash(password)
  res = verify(password_hash, actual_password_hash)
  return res and user is not None

But I wonder if the following strategy is also useful against timing attacks (not considering other types of side-channel attacks), while not wasting computing resources:

async def sign_in(username, password):
  # Longer than what `sign_in_impl` takes normally
  fixed_duration = ... 

  _, sign_in_result = await asyncio.gather(delay(fixed_duration), sign_in_impl)

  return sign_in_result

# Awaits a certain amount of time
async def delay(duration):
  ...

# This takes variable time
async def sign_in_impl(username, password):
  user = await get_user_from_db(username)
  if user is None:
    return False  # early return :(

  password_hash = slow_hash(password)
  return verify(password_hash, user.password_hash)
Zizheng Tai
  • 131
  • 2

2 Answers2

1

TL;DR

I don't see anything in your provided code that would need to be protected against timing attacks. The purpose of timing attacks is to learn the plaintext secret, but since your code does not handle the plaintext secret (only a hash of it), your code couldn't leak the plaintext password even if it wanted to.

Technically your hash comparison should be constant time, but see below for why I'm not worried about this in practice.


Background on timing attacks

Let's take a step back and ask what timing attacks are trying to prevent. With passwords, as with all of crypto, you are trying to exploit timing differences in the function that is handling secret data in order to learn the secret.

The classic example is testing for equality between a stored password and a provided password using lazy string compare:

boolean compare(String str1, String str2)
   for (i=0; i<len(str1); i++)
      if (str1[i] != str2[i]) return false;
   return true;

Here you can learn the password one character at a time because when you get the first character right, the string compare will take slightly longer.

What about hashed passwords?

What if you are comparing password hashes rather than plaintext? In theory, if an attacker learns the first character of the password hash, they have gained information, so that is technically an information leak. However, in practice, a timing attack against hashed passwords would require the attacker to submit password candidates p where they can control hash(p) one character at a time (ie first a*, then aa* ...). This is hard. In fact, finding a plaintext whose hash has a given prefix is exactly the cryptographic problem that keeps bitcoin and blockchain secure, and that's on SHA-256; doing it on PBKDF2 or Argon2 would be even harder.

Does your code need to be constant time?

Given the argument above about handling password hashes, it doesn't really matter if this line is constant-time or not:

return verify(password_hash, user.password_hash)

It also does not matter at all whether this line is constant-time; who cares if they can learn password through a complex side-channel attack, they just submitted it to you, they already know it.

password_hash = slow_hash(password)

IMO it also doesn't matter whether your DB queries are constant time. The attacker would be learning how full your DB is, how much backend network congestion and server load you have, etc, none of which helps them crack passwords. (if your product security model does want to protect info about backend load, that's a much harder and completely separate question).


Bonus: adding random delays

While not directly mentioned in your question, I've written before about how adding random timing delays increases the number of statistical samples (ie the number of login attempts) that an attacker needs in order to perform the statistical attack, but it's ultimately not a solution.

Mike Ounsworth
  • 57,707
  • 21
  • 150
  • 207
  • "I don't see anything in your provided code that would need to be protected against timing attacks." I'm trying to prevent the attacker from knowing whether a username exists in the database. That should be a valid concern? – Zizheng Tai Nov 09 '20 at 17:59
  • @ZizhengTai LOL, yup, "timing attacks for user enumeration" is a totally different question than the one I answered "timing attacks for password recovery". I guess my answer does not answer the right question. You should edit your question to make that more clear. – Mike Ounsworth Nov 09 '20 at 19:25
-1

Either option would contribute towards mitigating timing attacks. Remember though that some of those methods will take varying amounts of time to complete, specifically the database lookup.

Another way you could contribute towards mitigating timing attacks is to add a random delay(a) to successful login calls(c) and another random delay(b) to failed login calls.

If you make the delay (a) a random(max_jitter_amount) and if you can accurately calculate the average time taken by (c), then your random delay (b) would be something like avg_time(c)+random(max jitter amount). This way every single request will return a certain amount of time + a random jitter amount which would defeat statistical analysis.

Pedro
  • 3,911
  • 11
  • 25
  • Downvoting because given enough statistical samples, you can filter out the random noise. – Mike Ounsworth Nov 09 '20 at 15:38
  • That can be made less feasible (not impossible) by adjusting the intensity of the random jitter added vs the typical delays felt in parsing requests. – Pedro Nov 10 '20 at 16:25