How to make map access safe from timing attack?

Basically, I’d like to make the following code safe from timing attack:

api_keys = %{
  "secret1" => [1, 2, 3],
  "secret2" => [1, 2, 3],
}

def api_endpoint(...) do
  Map.fetch(api_keys, conn.assign.api_keys)
  |> case do
    {:ok, resources} -> "you got access"
    :error -> "sorry, no access"
  end
end

Usually, I would pass the provided secrets into a password hashing function, this is actually a cache to make an API very fast, so I cannot introduce an overhead of something like argon2 or similar.

As a quick reminded, what I mean by timing attack: A Lesson In Timing Attacks (or, Don’t use MessageDigest.isEquals) | codahale.com

Something like this should work.

Enum.reduce(api_keys, :error, fn 
  {key, value}, :error -> 
    if key == conn.assign.api_keys do
      {:ok, value}
    else
      :error
    end

  {key, _value}, acc -> 
    key == conn.assign.api_keys
    acc
end)

I feel like that obscures the plain equality check, but it seems like that’d still be vulnerable. I would think you’d want to replace == with a constant time equality checker like Plug.Crypto.secure_compare/2.

Erlef’s page on timing attacks suggests newer versions of Erlang have a crypto:equal_const_time/2, but I’m not seeing it or finding anything about it.

Instead of accessing by API key, you can access by API client and then compare the application password with a secure function such as Plug.Crypto.secure_compare/2.

Your map would look like:

api_keys = %{
  "client1" => %{secret: "secret1", data: [1, 2, 3]},
  "client2" => %{secret: "secret2", data: [1, 2, 3]},
}

The problem with your current implementation is that you have no control on how the map is accessed by key.

1 Like

Are you issuing these API keys? If you have control of the key structure one solution would be doing more of an id + secret pairing (whether that’s multiple params, or combined into a string, or like basic auth), looking up the key by id, then doing a timing-safe compare for the secret.

Yeah I guess, the ID+SECRET approach is better, as @tangui suggested.

I’ll do that instead.

Thanks